diff --git a/macos/Ghostty.sdef b/macos/Ghostty.sdef
index 47fae636f..7cbe898a1 100644
--- a/macos/Ghostty.sdef
+++ b/macos/Ghostty.sdef
@@ -182,6 +182,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/macos/Sources/Features/AppleScript/ScriptCloseCommand.swift b/macos/Sources/Features/AppleScript/ScriptCloseCommand.swift
index e8fdbb0a9..3f240cf6e 100644
--- a/macos/Sources/Features/AppleScript/ScriptCloseCommand.swift
+++ b/macos/Sources/Features/AppleScript/ScriptCloseCommand.swift
@@ -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
+ }
+}
diff --git a/macos/Sources/Features/AppleScript/ScriptFocusCommand.swift b/macos/Sources/Features/AppleScript/ScriptFocusCommand.swift
index 024aee4de..edda43a45 100644
--- a/macos/Sources/Features/AppleScript/ScriptFocusCommand.swift
+++ b/macos/Sources/Features/AppleScript/ScriptFocusCommand.swift
@@ -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
+ }
+}
diff --git a/macos/Sources/Features/AppleScript/ScriptTab.swift b/macos/Sources/Features/AppleScript/ScriptTab.swift
index aa643db5a..bda694ebc 100644
--- a/macos/Sources/Features/AppleScript/ScriptTab.swift
+++ b/macos/Sources/Features/AppleScript/ScriptTab.swift
@@ -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.
diff --git a/macos/Sources/Features/AppleScript/ScriptWindow.swift b/macos/Sources/Features/AppleScript/ScriptWindow.swift
index 089ea5e18..97822e973 100644
--- a/macos/Sources/Features/AppleScript/ScriptWindow.swift
+++ b/macos/Sources/Features/AppleScript/ScriptWindow.swift
@@ -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 })