Update ghostty to v1.3.0 (#1142)

* Update ghostty to v1.3.0

* Add bell handling and AppleScript support

* Add zsh shell integration handoff test

* Fix Ghostty zsh integration handoff in cmux

* Add terminal keypress notification dismissal test

* Dismiss terminal notifications on keypress

* Address PR review feedback

* Tighten notification dismissal regression test

* Pin GhosttyKit checksum for latest ghostty
This commit is contained in:
Lawrence Chen 2026-03-09 21:32:54 -07:00 committed by GitHub
parent 55b619b538
commit dea60ea71c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1542 additions and 9 deletions

View file

@ -0,0 +1,705 @@
import AppKit
private enum AppleScriptStrings {
static let disabled = String(
localized: "applescript.error.disabled",
defaultValue: "AppleScript is disabled by the macos-applescript configuration."
)
static let missingAction = String(
localized: "applescript.error.missingAction",
defaultValue: "Missing action string."
)
static let missingInputText = String(
localized: "applescript.error.missingInputText",
defaultValue: "Missing input text."
)
static let missingTerminalTarget = String(
localized: "applescript.error.missingTerminalTarget",
defaultValue: "Missing terminal target."
)
static let missingSplitDirection = String(
localized: "applescript.error.missingSplitDirection",
defaultValue: "Missing or unknown split direction."
)
static let windowUnavailable = String(
localized: "applescript.error.windowUnavailable",
defaultValue: "Window is no longer available."
)
static let workspaceUnavailable = String(
localized: "applescript.error.workspaceUnavailable",
defaultValue: "Workspace is no longer available."
)
static let terminalUnavailable = String(
localized: "applescript.error.terminalUnavailable",
defaultValue: "Terminal is no longer available."
)
static let failedToCreateWindow = String(
localized: "applescript.error.failedToCreateWindow",
defaultValue: "Failed to create window."
)
static let failedToCreateWorkspace = String(
localized: "applescript.error.failedToCreateWorkspace",
defaultValue: "Failed to create workspace."
)
static let failedToCreateSplit = String(
localized: "applescript.error.failedToCreateSplit",
defaultValue: "Failed to create split."
)
}
private extension String {
var fourCharCode: UInt32 {
utf8.reduce(0) { ($0 << 8) + UInt32($1) }
}
}
private extension Workspace {
func scriptingTerminalPanels() -> [TerminalPanel] {
var results: [TerminalPanel] = []
var seen: Set<UUID> = []
for panelId in sidebarOrderedPanelIds() {
guard seen.insert(panelId).inserted,
let terminal = terminalPanel(for: panelId) else {
continue
}
results.append(terminal)
}
let remaining = panels.values
.compactMap { $0 as? TerminalPanel }
.sorted { $0.id.uuidString < $1.id.uuidString }
for terminal in remaining where seen.insert(terminal.id).inserted {
results.append(terminal)
}
return results
}
}
@MainActor
extension NSApplication {
var isAppleScriptEnabled: Bool {
GhosttyApp.shared.appleScriptAutomationEnabled()
}
@discardableResult
func validateScript(command: NSScriptCommand) -> Bool {
guard isAppleScriptEnabled else {
command.scriptErrorNumber = errAEEventNotPermitted
command.scriptErrorString = AppleScriptStrings.disabled
return false
}
return true
}
@objc(scriptWindows)
var scriptWindows: [ScriptWindow] {
guard isAppleScriptEnabled,
let appDelegate = AppDelegate.shared else {
return []
}
return appDelegate.scriptableMainWindows().map { ScriptWindow(windowId: $0.windowId) }
}
@objc(frontWindow)
var frontWindow: ScriptWindow? {
scriptWindows.first
}
@objc(valueInScriptWindowsWithUniqueID:)
func valueInScriptWindows(uniqueID: String) -> ScriptWindow? {
guard isAppleScriptEnabled,
let windowId = UUID(uuidString: uniqueID),
let appDelegate = AppDelegate.shared,
appDelegate.scriptableMainWindow(windowId: windowId) != nil else {
return nil
}
return ScriptWindow(windowId: windowId)
}
@objc(terminals)
var terminals: [ScriptTerminal] {
guard isAppleScriptEnabled,
let appDelegate = AppDelegate.shared else {
return []
}
return appDelegate.scriptableMainWindows()
.flatMap { state in
state.tabManager.tabs.flatMap { workspace in
workspace.scriptingTerminalPanels().map {
ScriptTerminal(workspaceId: workspace.id, terminalId: $0.id)
}
}
}
}
@objc(valueInTerminalsWithUniqueID:)
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
guard isAppleScriptEnabled,
let terminalId = UUID(uuidString: uniqueID),
let appDelegate = AppDelegate.shared else {
return nil
}
for state in appDelegate.scriptableMainWindows() {
for workspace in state.tabManager.tabs where workspace.terminalPanel(for: terminalId) != nil {
return ScriptTerminal(workspaceId: workspace.id, terminalId: terminalId)
}
}
return nil
}
@objc(handlePerformActionScriptCommand:)
func handlePerformActionScriptCommand(_ command: NSScriptCommand) -> NSNumber? {
guard validateScript(command: command) else { return nil }
guard let action = command.directParameter as? String else {
command.scriptErrorNumber = errAEParamMissed
command.scriptErrorString = AppleScriptStrings.missingAction
return nil
}
guard let terminal = command.evaluatedArguments?["on"] as? ScriptTerminal else {
command.scriptErrorNumber = errAEParamMissed
command.scriptErrorString = AppleScriptStrings.missingTerminalTarget
return nil
}
return NSNumber(value: terminal.perform(action: action))
}
@objc(handleNewWindowScriptCommand:)
func handleNewWindowScriptCommand(_ command: NSScriptCommand) -> ScriptWindow? {
guard validateScript(command: command) else { return nil }
guard let appDelegate = AppDelegate.shared else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.failedToCreateWindow
return nil
}
let windowId = appDelegate.createMainWindow()
return ScriptWindow(windowId: windowId)
}
@objc(handleNewTabScriptCommand:)
func handleNewTabScriptCommand(_ command: NSScriptCommand) -> ScriptTab? {
guard validateScript(command: command) else { return nil }
guard let appDelegate = AppDelegate.shared else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.failedToCreateWorkspace
return nil
}
if let targetWindow = command.evaluatedArguments?["window"] as? ScriptWindow {
guard let workspaceId = appDelegate.addWorkspace(windowId: targetWindow.windowId, bringToFront: false) else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.failedToCreateWorkspace
return nil
}
return ScriptTab(windowId: targetWindow.windowId, tabId: workspaceId)
}
if let frontWindow = scriptWindows.first,
let workspaceId = appDelegate.addWorkspace(windowId: frontWindow.windowId, bringToFront: false) {
return ScriptTab(windowId: frontWindow.windowId, tabId: workspaceId)
}
let windowId = appDelegate.createMainWindow()
return ScriptWindow(windowId: windowId).selectedTab
}
@objc(handleQuitScriptCommand:)
func handleQuitScriptCommand(_ command: NSScriptCommand) {
guard validateScript(command: command) else { return }
terminate(nil)
}
}
@MainActor
@objc(CmuxScriptWindow)
final class ScriptWindow: NSObject {
let windowId: UUID
init(windowId: UUID) {
self.windowId = windowId
}
private var state: AppDelegate.ScriptableMainWindowState? {
AppDelegate.shared?.scriptableMainWindow(windowId: windowId)
}
@objc(id)
var idValue: String {
guard NSApp.isAppleScriptEnabled else { return "" }
return windowId.uuidString
}
@objc(title)
var title: String {
guard NSApp.isAppleScriptEnabled,
let state else {
return ""
}
let windowTitle = state.window?.title.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !windowTitle.isEmpty {
return windowTitle
}
return state.tabManager.selectedWorkspace?.title ?? ""
}
@objc(tabs)
var tabs: [ScriptTab] {
guard NSApp.isAppleScriptEnabled,
let state else {
return []
}
return state.tabManager.tabs.map { ScriptTab(windowId: windowId, tabId: $0.id) }
}
@objc(selectedTab)
var selectedTab: ScriptTab? {
guard NSApp.isAppleScriptEnabled,
let selectedId = state?.tabManager.selectedTabId else {
return nil
}
return ScriptTab(windowId: windowId, tabId: selectedId)
}
@objc(terminals)
var terminals: [ScriptTerminal] {
guard NSApp.isAppleScriptEnabled,
let state else {
return []
}
return state.tabManager.tabs.flatMap { workspace in
workspace.scriptingTerminalPanels().map {
ScriptTerminal(workspaceId: workspace.id, terminalId: $0.id)
}
}
}
@objc(valueInTabsWithUniqueID:)
func valueInTabs(uniqueID: String) -> ScriptTab? {
guard NSApp.isAppleScriptEnabled,
let tabId = UUID(uuidString: uniqueID),
let state,
state.tabManager.tabs.contains(where: { $0.id == tabId }) else {
return nil
}
return ScriptTab(windowId: windowId, tabId: tabId)
}
@objc(valueInTerminalsWithUniqueID:)
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
guard NSApp.isAppleScriptEnabled,
let terminalId = UUID(uuidString: uniqueID),
let state else {
return nil
}
for workspace in state.tabManager.tabs where workspace.terminalPanel(for: terminalId) != nil {
return ScriptTerminal(workspaceId: workspace.id, terminalId: terminalId)
}
return nil
}
@objc(handleActivateWindowCommand:)
func handleActivateWindow(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard AppDelegate.shared?.focusScriptableMainWindow(windowId: windowId, bringToFront: true) == true else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.windowUnavailable
return nil
}
return nil
}
@objc(handleCloseWindowCommand:)
func handleCloseWindow(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard let window = state?.window else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.windowUnavailable
return nil
}
window.performClose(nil)
return nil
}
override var objectSpecifier: NSScriptObjectSpecifier? {
guard NSApp.isAppleScriptEnabled,
let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else {
return nil
}
return NSUniqueIDSpecifier(
containerClassDescription: appClassDescription,
containerSpecifier: nil,
key: "scriptWindows",
uniqueID: windowId.uuidString
)
}
}
@MainActor
@objc(CmuxScriptTab)
final class ScriptTab: NSObject {
let windowId: UUID
let tabId: UUID
init(windowId: UUID, tabId: UUID) {
self.windowId = windowId
self.tabId = tabId
}
private var state: AppDelegate.ScriptableMainWindowState? {
AppDelegate.shared?.scriptableMainWindow(windowId: windowId)
}
private var workspace: Workspace? {
state?.tabManager.tabs.first(where: { $0.id == tabId })
}
private var window: ScriptWindow {
ScriptWindow(windowId: windowId)
}
@objc(id)
var idValue: String {
guard NSApp.isAppleScriptEnabled else { return "" }
return tabId.uuidString
}
@objc(title)
var title: String {
guard NSApp.isAppleScriptEnabled else { return "" }
return workspace?.title ?? ""
}
@objc(index)
var index: Int {
guard NSApp.isAppleScriptEnabled,
let state,
let idx = state.tabManager.tabs.firstIndex(where: { $0.id == tabId }) else {
return 0
}
return idx + 1
}
@objc(selected)
var selected: Bool {
guard NSApp.isAppleScriptEnabled else { return false }
return state?.tabManager.selectedTabId == tabId
}
@objc(focusedTerminal)
var focusedTerminal: ScriptTerminal? {
guard NSApp.isAppleScriptEnabled,
let terminalId = workspace?.focusedTerminalPanel?.id else {
return nil
}
return ScriptTerminal(workspaceId: tabId, terminalId: terminalId)
}
@objc(terminals)
var terminals: [ScriptTerminal] {
guard NSApp.isAppleScriptEnabled,
let workspace else {
return []
}
return workspace.scriptingTerminalPanels().map {
ScriptTerminal(workspaceId: tabId, terminalId: $0.id)
}
}
@objc(valueInTerminalsWithUniqueID:)
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
guard NSApp.isAppleScriptEnabled,
let workspace,
let terminalId = UUID(uuidString: uniqueID),
workspace.terminalPanel(for: terminalId) != nil else {
return nil
}
return ScriptTerminal(workspaceId: tabId, terminalId: terminalId)
}
@objc(handleSelectTabCommand:)
func handleSelectTab(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard let state,
let workspace else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.workspaceUnavailable
return nil
}
state.tabManager.selectWorkspace(workspace)
return nil
}
@objc(handleCloseTabCommand:)
func handleCloseTab(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard let state,
let workspace else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.workspaceUnavailable
return nil
}
if state.tabManager.tabs.count > 1 {
state.tabManager.closeWorkspace(workspace)
return nil
}
guard let window = state.window else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.windowUnavailable
return nil
}
window.performClose(nil)
return nil
}
override var objectSpecifier: NSScriptObjectSpecifier? {
guard NSApp.isAppleScriptEnabled,
let windowClassDescription = window.classDescription as? NSScriptClassDescription,
let windowSpecifier = window.objectSpecifier else {
return nil
}
return NSUniqueIDSpecifier(
containerClassDescription: windowClassDescription,
containerSpecifier: windowSpecifier,
key: "tabs",
uniqueID: tabId.uuidString
)
}
}
@MainActor
@objc(CmuxScriptTerminal)
final class ScriptTerminal: NSObject {
let workspaceId: UUID
let terminalId: UUID
init(workspaceId: UUID, terminalId: UUID) {
self.workspaceId = workspaceId
self.terminalId = terminalId
}
private var state: AppDelegate.ScriptableMainWindowState? {
AppDelegate.shared?.scriptableMainWindowForTab(workspaceId)
}
private var workspace: Workspace? {
state?.tabManager.tabs.first(where: { $0.id == workspaceId })
}
private var terminal: TerminalPanel? {
workspace?.terminalPanel(for: terminalId)
}
@objc(id)
var stableID: String {
guard NSApp.isAppleScriptEnabled else { return "" }
return terminalId.uuidString
}
@objc(title)
var title: String {
guard NSApp.isAppleScriptEnabled else { return "" }
return terminal?.displayTitle ?? ""
}
@objc(workingDirectory)
var workingDirectory: String {
guard NSApp.isAppleScriptEnabled else { return "" }
return terminal?.directory ?? ""
}
func input(text: String) -> Bool {
guard NSApp.isAppleScriptEnabled,
let terminal else {
return false
}
terminal.sendText(text)
return true
}
func perform(action: String) -> Bool {
guard NSApp.isAppleScriptEnabled else { return false }
return terminal?.performBindingAction(action) ?? false
}
@objc(handleSplitCommand:)
func handleSplit(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard let directionCode = command.evaluatedArguments?["direction"] as? UInt32,
let direction = ScriptSplitDirection(code: directionCode)?.splitDirection else {
command.scriptErrorNumber = errAEParamMissed
command.scriptErrorString = AppleScriptStrings.missingSplitDirection
return nil
}
guard let state,
let workspace,
terminal != nil else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
return nil
}
guard let newPanelId = state.tabManager.newSplit(tabId: workspaceId, surfaceId: terminalId, direction: direction),
workspace.terminalPanel(for: newPanelId) != nil else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.failedToCreateSplit
return nil
}
return ScriptTerminal(workspaceId: workspaceId, terminalId: newPanelId)
}
@objc(handleFocusCommand:)
func handleFocus(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard let state,
let workspace,
terminal != nil else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
return nil
}
if let app = AppDelegate.shared {
_ = app.focusScriptableMainWindow(windowId: state.windowId, bringToFront: true)
}
state.tabManager.selectWorkspace(workspace)
workspace.focusPanel(terminalId)
return nil
}
@objc(handleCloseCommand:)
func handleClose(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard let state,
let workspace,
terminal != nil else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
return nil
}
if workspace.panels.count == 1 {
if state.tabManager.tabs.count > 1 {
state.tabManager.closeWorkspace(workspace)
return nil
}
guard let window = state.window else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.windowUnavailable
return nil
}
window.performClose(nil)
return nil
}
guard workspace.closePanel(terminalId, force: true) else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = AppleScriptStrings.terminalUnavailable
return nil
}
AppDelegate.shared?.notificationStore?.clearNotifications(forTabId: workspaceId, surfaceId: terminalId)
return nil
}
override var objectSpecifier: NSScriptObjectSpecifier? {
guard NSApp.isAppleScriptEnabled,
let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else {
return nil
}
return NSUniqueIDSpecifier(
containerClassDescription: appClassDescription,
containerSpecifier: nil,
key: "terminals",
uniqueID: terminalId.uuidString
)
}
}
@MainActor
@objc(CmuxScriptInputTextCommand)
final class ScriptInputTextCommand: NSScriptCommand {
override func performDefaultImplementation() -> Any? {
guard NSApp.validateScript(command: self) else { return nil }
guard let text = directParameter as? String else {
scriptErrorNumber = errAEParamMissed
scriptErrorString = AppleScriptStrings.missingInputText
return nil
}
guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else {
scriptErrorNumber = errAEParamMissed
scriptErrorString = AppleScriptStrings.missingTerminalTarget
return nil
}
guard terminal.input(text: text) else {
scriptErrorNumber = errAEEventFailed
scriptErrorString = AppleScriptStrings.terminalUnavailable
return nil
}
return nil
}
}
private enum ScriptSplitDirection {
case right
case left
case down
case up
init?(code: UInt32) {
switch code {
case "GSrt".fourCharCode: self = .right
case "GSlf".fourCharCode: self = .left
case "GSdn".fourCharCode: self = .down
case "GSup".fourCharCode: self = .up
default: return nil
}
}
var splitDirection: SplitDirection {
switch self {
case .right: return .right
case .left: return .left
case .down: return .down
case .up: return .up
}
}
}