macos: Add AppleScript commands for window and tab control

Add scripting dictionary commands for activating windows, selecting tabs,
closing tabs, and closing windows.

Implement the corresponding Cocoa AppleScript command handlers and expose
minimal ScriptWindow/ScriptTab helpers needed to resolve live targets.

Verified by building Ghostty and running osascript commands against the
absolute Debug app path to exercise all four new commands.
This commit is contained in:
Mitchell Hashimoto
2026-03-06 14:31:03 -08:00
parent d271c8ccaa
commit 28b4e2495d
5 changed files with 149 additions and 0 deletions

View File

@@ -182,6 +182,34 @@
</parameter>
</command>
<command name="activate window" code="GhstAcWn" description="Activate a Ghostty window, bringing it to the front.">
<cocoa class="GhosttyScriptActivateWindowCommand"/>
<parameter name="window" code="GAwW" type="window" description="The window to activate.">
<cocoa key="window"/>
</parameter>
</command>
<command name="select tab" code="GhstSlTb" description="Select a tab in its window.">
<cocoa class="GhosttyScriptSelectTabCommand"/>
<parameter name="tab" code="GStT" type="tab" description="The tab to select.">
<cocoa key="tab"/>
</parameter>
</command>
<command name="close tab" code="GhstClTb" description="Close a tab.">
<cocoa class="GhosttyScriptCloseTabCommand"/>
<parameter name="tab" code="GCtT" type="tab" description="The tab to close.">
<cocoa key="tab"/>
</parameter>
</command>
<command name="close window" code="GhstClWn" description="Close a window.">
<cocoa class="GhosttyScriptCloseWindowCommand"/>
<parameter name="window" code="GCwW" type="window" description="The window to close.">
<cocoa key="window"/>
</parameter>
</command>
<command name="input text" code="GhstInTx" description="Input text to a terminal as if it was pasted.">
<cocoa class="GhosttyScriptInputTextCommand"/>
<direct-parameter type="text" description="The text to input."/>

View File

@@ -31,3 +31,63 @@ final class ScriptCloseCommand: NSScriptCommand {
return nil
}
}
/// Handler for the `close tab` AppleScript command defined in `Ghostty.sdef`.
@MainActor
@objc(GhosttyScriptCloseTabCommand)
final class ScriptCloseTabCommand: NSScriptCommand {
override func performDefaultImplementation() -> Any? {
guard let tab = evaluatedArguments?["tab"] as? ScriptTab else {
scriptErrorNumber = errAEParamMissed
scriptErrorString = "Missing tab target."
return nil
}
guard let controller = tab.parentController else {
scriptErrorNumber = errAEEventFailed
scriptErrorString = "Tab is no longer available."
return nil
}
if let terminalController = controller as? TerminalController {
terminalController.closeTabImmediately(registerRedo: false)
return nil
}
guard let targetWindow = tab.parentWindow else {
scriptErrorNumber = errAEEventFailed
scriptErrorString = "Tab window is no longer available."
return nil
}
targetWindow.close()
return nil
}
}
/// Handler for the `close window` AppleScript command defined in `Ghostty.sdef`.
@MainActor
@objc(GhosttyScriptCloseWindowCommand)
final class ScriptCloseWindowCommand: NSScriptCommand {
override func performDefaultImplementation() -> Any? {
guard let window = evaluatedArguments?["window"] as? ScriptWindow else {
scriptErrorNumber = errAEParamMissed
scriptErrorString = "Missing window target."
return nil
}
if let terminalController = window.preferredController as? TerminalController {
terminalController.closeWindowImmediately()
return nil
}
guard let targetWindow = window.preferredParentWindow else {
scriptErrorNumber = errAEEventFailed
scriptErrorString = "Window is no longer available."
return nil
}
targetWindow.close()
return nil
}
}

View File

@@ -31,3 +31,49 @@ final class ScriptFocusCommand: NSScriptCommand {
return nil
}
}
/// Handler for the `activate window` AppleScript command defined in `Ghostty.sdef`.
@MainActor
@objc(GhosttyScriptActivateWindowCommand)
final class ScriptActivateWindowCommand: NSScriptCommand {
override func performDefaultImplementation() -> Any? {
guard let window = evaluatedArguments?["window"] as? ScriptWindow else {
scriptErrorNumber = errAEParamMissed
scriptErrorString = "Missing window target."
return nil
}
guard let targetWindow = window.preferredParentWindow else {
scriptErrorNumber = errAEEventFailed
scriptErrorString = "Window is no longer available."
return nil
}
targetWindow.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return nil
}
}
/// Handler for the `select tab` AppleScript command defined in `Ghostty.sdef`.
@MainActor
@objc(GhosttyScriptSelectTabCommand)
final class ScriptSelectTabCommand: NSScriptCommand {
override func performDefaultImplementation() -> Any? {
guard let tab = evaluatedArguments?["tab"] as? ScriptTab else {
scriptErrorNumber = errAEParamMissed
scriptErrorString = "Missing tab target."
return nil
}
guard let targetWindow = tab.parentWindow else {
scriptErrorNumber = errAEEventFailed
scriptErrorString = "Tab is no longer available."
return nil
}
targetWindow.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return nil
}
}

View File

@@ -63,6 +63,16 @@ final class ScriptTab: NSObject {
return window?.tabIsSelected(controller) ?? false
}
/// Best-effort native window containing this tab.
var parentWindow: NSWindow? {
controller?.window
}
/// Live controller backing this tab wrapper.
var parentController: BaseTerminalController? {
controller
}
/// Exposed as the AppleScript `terminals` element on a tab.
///
/// Returns all terminal surfaces (split panes) within this tab.

View File

@@ -116,6 +116,11 @@ final class ScriptWindow: NSObject {
selectedController?.window ?? controllers.first?.window
}
/// Best-effort controller to use for window-scoped AppleScript commands.
var preferredController: BaseTerminalController? {
selectedController ?? controllers.first
}
/// Resolves a previously generated tab ID back to a live controller.
private func controller(tabID: String) -> BaseTerminalController? {
controllers.first(where: { ScriptTab.stableID(controller: $0) == tabID })