diff --git a/macos/Ghostty.sdef b/macos/Ghostty.sdef
index 95fcea626..95497a04a 100644
--- a/macos/Ghostty.sdef
+++ b/macos/Ghostty.sdef
@@ -44,6 +44,12 @@
+
+
+
+
+
+
@@ -60,6 +66,12 @@
+
+
+
+
+
+
@@ -72,6 +84,15 @@
+
+
+
+
+
+
+
+
+
@@ -155,10 +176,7 @@
-
-
-
-
+
@@ -169,45 +187,27 @@
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
-
-
-
-
+
diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj
index 0f63d654e..3758c325d 100644
--- a/macos/Ghostty.xcodeproj/project.pbxproj
+++ b/macos/Ghostty.xcodeproj/project.pbxproj
@@ -138,15 +138,12 @@
"Features/App Intents/QuickTerminalIntent.swift",
"Features/AppleScript/AppDelegate+AppleScript.swift",
"Features/AppleScript/Ghostty.Input.Mods+AppleScript.swift",
- Features/AppleScript/ScriptCloseCommand.swift,
- Features/AppleScript/ScriptFocusCommand.swift,
Features/AppleScript/ScriptInputTextCommand.swift,
Features/AppleScript/ScriptKeyEventCommand.swift,
Features/AppleScript/ScriptMouseButtonCommand.swift,
Features/AppleScript/ScriptMousePosCommand.swift,
Features/AppleScript/ScriptMouseScrollCommand.swift,
Features/AppleScript/ScriptRecord.swift,
- Features/AppleScript/ScriptSplitCommand.swift,
Features/AppleScript/ScriptSurfaceConfiguration.swift,
Features/AppleScript/ScriptTab.swift,
Features/AppleScript/ScriptTerminal.swift,
diff --git a/macos/Sources/Features/AppleScript/ScriptCloseCommand.swift b/macos/Sources/Features/AppleScript/ScriptCloseCommand.swift
deleted file mode 100644
index b38fb0e62..000000000
--- a/macos/Sources/Features/AppleScript/ScriptCloseCommand.swift
+++ /dev/null
@@ -1,101 +0,0 @@
-import AppKit
-
-/// Handler for the `close` AppleScript command defined in `Ghostty.sdef`.
-///
-/// Cocoa scripting instantiates this class because the command's `` element
-/// specifies `class="GhosttyScriptCloseCommand"`. The runtime calls
-/// `performDefaultImplementation()` to execute the command.
-@MainActor
-@objc(GhosttyScriptCloseCommand)
-final class ScriptCloseCommand: NSScriptCommand {
- override func performDefaultImplementation() -> Any? {
- guard NSApp.validateScript(command: self) else { return nil }
-
- guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else {
- scriptErrorNumber = errAEParamMissed
- scriptErrorString = "Missing terminal target."
- return nil
- }
-
- guard let surfaceView = terminal.surfaceView else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Terminal surface is no longer available."
- return nil
- }
-
- guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Terminal is not in a window."
- return nil
- }
-
- controller.closeSurface(surfaceView, withConfirmation: false)
- return nil
- }
-}
-
-/// Handler for the container-level `close tab` AppleScript command defined in
-/// `Ghostty.sdef`.
-@MainActor
-@objc(GhosttyScriptCloseTabCommand)
-final class ScriptCloseTabCommand: NSScriptCommand {
- override func performDefaultImplementation() -> Any? {
- guard NSApp.validateScript(command: self) else { return nil }
-
- guard let tab = evaluatedArguments?["tab"] as? ScriptTab else {
- scriptErrorNumber = errAEParamMissed
- scriptErrorString = "Missing tab target."
- return nil
- }
-
- guard let tabController = tab.parentController else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Tab is no longer available."
- return nil
- }
-
- if let managedTerminalController = tabController as? TerminalController {
- managedTerminalController.closeTabImmediately(registerRedo: false)
- return nil
- }
-
- guard let tabContainerWindow = tab.parentWindow else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Tab container window is no longer available."
- return nil
- }
-
- tabContainerWindow.close()
- return nil
- }
-}
-
-/// Handler for the container-level `close window` AppleScript command defined in
-/// `Ghostty.sdef`.
-@MainActor
-@objc(GhosttyScriptCloseWindowCommand)
-final class ScriptCloseWindowCommand: NSScriptCommand {
- override func performDefaultImplementation() -> Any? {
- guard NSApp.validateScript(command: self) else { return nil }
-
- guard let window = evaluatedArguments?["window"] as? ScriptWindow else {
- scriptErrorNumber = errAEParamMissed
- scriptErrorString = "Missing window target."
- return nil
- }
-
- if let managedTerminalController = window.preferredController as? TerminalController {
- managedTerminalController.closeWindowImmediately()
- return nil
- }
-
- guard let windowContainer = window.preferredParentWindow else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Window is no longer available."
- return nil
- }
-
- windowContainer.close()
- return nil
- }
-}
diff --git a/macos/Sources/Features/AppleScript/ScriptFocusCommand.swift b/macos/Sources/Features/AppleScript/ScriptFocusCommand.swift
deleted file mode 100644
index 0be3aaf69..000000000
--- a/macos/Sources/Features/AppleScript/ScriptFocusCommand.swift
+++ /dev/null
@@ -1,87 +0,0 @@
-import AppKit
-
-/// Handler for the `focus` AppleScript command defined in `Ghostty.sdef`.
-///
-/// Cocoa scripting instantiates this class because the command's `` element
-/// specifies `class="GhosttyScriptFocusCommand"`. The runtime calls
-/// `performDefaultImplementation()` to execute the command.
-@MainActor
-@objc(GhosttyScriptFocusCommand)
-final class ScriptFocusCommand: NSScriptCommand {
- override func performDefaultImplementation() -> Any? {
- guard NSApp.validateScript(command: self) else { return nil }
-
- guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else {
- scriptErrorNumber = errAEParamMissed
- scriptErrorString = "Missing terminal target."
- return nil
- }
-
- guard let surfaceView = terminal.surfaceView else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Terminal surface is no longer available."
- return nil
- }
-
- guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Terminal is not in a window."
- return nil
- }
-
- controller.focusSurface(surfaceView)
- return nil
- }
-}
-
-/// Handler for the container-level `activate window` AppleScript command
-/// defined in `Ghostty.sdef`.
-@MainActor
-@objc(GhosttyScriptActivateWindowCommand)
-final class ScriptActivateWindowCommand: NSScriptCommand {
- override func performDefaultImplementation() -> Any? {
- guard NSApp.validateScript(command: self) else { return nil }
-
- guard let window = evaluatedArguments?["window"] as? ScriptWindow else {
- scriptErrorNumber = errAEParamMissed
- scriptErrorString = "Missing window target."
- return nil
- }
-
- guard let windowContainer = window.preferredParentWindow else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Window is no longer available."
- return nil
- }
-
- windowContainer.makeKeyAndOrderFront(nil)
- NSApp.activate(ignoringOtherApps: true)
- return nil
- }
-}
-
-/// Handler for the container-level `select tab` AppleScript command defined in
-/// `Ghostty.sdef`.
-@MainActor
-@objc(GhosttyScriptSelectTabCommand)
-final class ScriptSelectTabCommand: NSScriptCommand {
- override func performDefaultImplementation() -> Any? {
- guard NSApp.validateScript(command: self) else { return nil }
-
- guard let tab = evaluatedArguments?["tab"] as? ScriptTab else {
- scriptErrorNumber = errAEParamMissed
- scriptErrorString = "Missing tab target."
- return nil
- }
-
- guard let tabContainerWindow = tab.parentWindow else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Tab is no longer available."
- return nil
- }
-
- tabContainerWindow.makeKeyAndOrderFront(nil)
- NSApp.activate(ignoringOtherApps: true)
- return nil
- }
-}
diff --git a/macos/Sources/Features/AppleScript/ScriptSplitCommand.swift b/macos/Sources/Features/AppleScript/ScriptSplitCommand.swift
deleted file mode 100644
index b3ab0fa73..000000000
--- a/macos/Sources/Features/AppleScript/ScriptSplitCommand.swift
+++ /dev/null
@@ -1,92 +0,0 @@
-import AppKit
-
-/// Handler for the `split` AppleScript command defined in `Ghostty.sdef`.
-///
-/// Cocoa scripting instantiates this class because the command's `` element
-/// specifies `class="GhosttyScriptSplitCommand"`. The runtime calls
-/// `performDefaultImplementation()` to execute the command.
-@MainActor
-@objc(GhosttyScriptSplitCommand)
-final class ScriptSplitCommand: NSScriptCommand {
- override func performDefaultImplementation() -> Any? {
- guard NSApp.validateScript(command: self) else { return nil }
-
- guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else {
- scriptErrorNumber = errAEParamMissed
- scriptErrorString = "Missing terminal target."
- return nil
- }
-
- guard let surfaceView = terminal.surfaceView else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Terminal surface is no longer available."
- return nil
- }
-
- guard let directionCode = evaluatedArguments?["direction"] as? UInt32,
- let direction = ScriptSplitDirection(code: directionCode) else {
- scriptErrorNumber = errAEParamMissed
- scriptErrorString = "Missing or unknown split direction."
- return nil
- }
-
- let baseConfig: Ghostty.SurfaceConfiguration?
- if let scriptRecord = evaluatedArguments?["configuration"] as? NSDictionary {
- do {
- baseConfig = try Ghostty.SurfaceConfiguration(scriptRecord: scriptRecord)
- } catch {
- scriptErrorNumber = errAECoercionFail
- scriptErrorString = error.localizedDescription
- return nil
- }
- } else {
- baseConfig = nil
- }
-
- // Find the window controller that owns this surface.
- guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Terminal is not in a splittable window."
- return nil
- }
-
- guard let newView = controller.newSplit(
- at: surfaceView,
- direction: direction.splitDirection,
- baseConfig: baseConfig
- ) else {
- scriptErrorNumber = errAEEventFailed
- scriptErrorString = "Failed to create split."
- return nil
- }
-
- return ScriptTerminal(surfaceView: newView)
- }
-}
-
-/// Four-character codes matching the `split direction` enumeration in `Ghostty.sdef`.
-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: SplitTree.NewDirection {
- switch self {
- case .right: .right
- case .left: .left
- case .down: .down
- case .up: .up
- }
- }
-}
diff --git a/macos/Sources/Features/AppleScript/ScriptTab.swift b/macos/Sources/Features/AppleScript/ScriptTab.swift
index 467fc3d06..8189cd0fa 100644
--- a/macos/Sources/Features/AppleScript/ScriptTab.swift
+++ b/macos/Sources/Features/AppleScript/ScriptTab.swift
@@ -100,6 +100,48 @@ final class ScriptTab: NSObject {
.map(ScriptTerminal.init)
}
+ /// Handler for `select tab `.
+ @objc(handleSelectTabCommand:)
+ func handleSelectTab(_ command: NSScriptCommand) -> Any? {
+ guard NSApp.validateScript(command: command) else { return nil }
+
+ guard let tabContainerWindow = parentWindow else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Tab is no longer available."
+ return nil
+ }
+
+ tabContainerWindow.makeKeyAndOrderFront(nil)
+ NSApp.activate(ignoringOtherApps: true)
+ return nil
+ }
+
+ /// Handler for `close tab `.
+ @objc(handleCloseTabCommand:)
+ func handleCloseTab(_ command: NSScriptCommand) -> Any? {
+ guard NSApp.validateScript(command: command) else { return nil }
+
+ guard let tabController = parentController else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Tab is no longer available."
+ return nil
+ }
+
+ if let managedTerminalController = tabController as? TerminalController {
+ managedTerminalController.closeTabImmediately(registerRedo: false)
+ return nil
+ }
+
+ guard let tabContainerWindow = parentWindow else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Tab container window is no longer available."
+ return nil
+ }
+
+ tabContainerWindow.close()
+ return nil
+ }
+
/// Provides Cocoa scripting with a canonical "path" back to this object.
override var objectSpecifier: NSScriptObjectSpecifier? {
guard NSApp.isAppleScriptEnabled else { return nil }
diff --git a/macos/Sources/Features/AppleScript/ScriptTerminal.swift b/macos/Sources/Features/AppleScript/ScriptTerminal.swift
index e4500c6b9..2cdde382e 100644
--- a/macos/Sources/Features/AppleScript/ScriptTerminal.swift
+++ b/macos/Sources/Features/AppleScript/ScriptTerminal.swift
@@ -60,6 +60,103 @@ final class ScriptTerminal: NSObject {
return surfaceModel.perform(action: action)
}
+ /// Handler for `split direction `.
+ @objc(handleSplitCommand:)
+ func handleSplit(_ command: NSScriptCommand) -> Any? {
+ guard NSApp.validateScript(command: command) else { return nil }
+
+ guard let surfaceView else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Terminal surface is no longer available."
+ return nil
+ }
+
+ guard let directionCode = command.evaluatedArguments?["direction"] as? UInt32 else {
+ command.scriptErrorNumber = errAEParamMissed
+ command.scriptErrorString = "Missing or unknown split direction."
+ return nil
+ }
+
+ guard let direction = ScriptSplitDirection(code: directionCode)?.splitDirection else {
+ command.scriptErrorNumber = errAEParamMissed
+ command.scriptErrorString = "Missing or unknown split direction."
+ return nil
+ }
+
+ let baseConfig: Ghostty.SurfaceConfiguration?
+ if let scriptRecord = command.evaluatedArguments?["configuration"] as? NSDictionary {
+ do {
+ baseConfig = try Ghostty.SurfaceConfiguration(scriptRecord: scriptRecord)
+ } catch {
+ command.scriptErrorNumber = errAECoercionFail
+ command.scriptErrorString = error.localizedDescription
+ return nil
+ }
+ } else {
+ baseConfig = nil
+ }
+
+ guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Terminal is not in a splittable window."
+ return nil
+ }
+
+ guard let newView = controller.newSplit(
+ at: surfaceView,
+ direction: direction,
+ baseConfig: baseConfig
+ ) else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Failed to create split."
+ return nil
+ }
+
+ return ScriptTerminal(surfaceView: newView)
+ }
+
+ /// Handler for `focus `.
+ @objc(handleFocusCommand:)
+ func handleFocus(_ command: NSScriptCommand) -> Any? {
+ guard NSApp.validateScript(command: command) else { return nil }
+
+ guard let surfaceView else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Terminal surface is no longer available."
+ return nil
+ }
+
+ guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Terminal is not in a window."
+ return nil
+ }
+
+ controller.focusSurface(surfaceView)
+ return nil
+ }
+
+ /// Handler for `close `.
+ @objc(handleCloseCommand:)
+ func handleClose(_ command: NSScriptCommand) -> Any? {
+ guard NSApp.validateScript(command: command) else { return nil }
+
+ guard let surfaceView else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Terminal surface is no longer available."
+ return nil
+ }
+
+ guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Terminal is not in a window."
+ return nil
+ }
+
+ controller.closeSurface(surfaceView, withConfirmation: false)
+ return nil
+ }
+
/// Provides Cocoa scripting with a canonical "path" back to this object.
///
/// Without an object specifier, returned terminal objects can't be reliably
@@ -79,3 +176,31 @@ final class ScriptTerminal: NSObject {
)
}
}
+
+/// Converts four-character codes from the `split direction` enumeration in `Ghostty.sdef`
+/// to `SplitTree.NewDirection` values.
+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: SplitTree.NewDirection {
+ switch self {
+ case .right: .right
+ case .left: .left
+ case .down: .down
+ case .up: .up
+ }
+ }
+}
diff --git a/macos/Sources/Features/AppleScript/ScriptWindow.swift b/macos/Sources/Features/AppleScript/ScriptWindow.swift
index 01a603843..c8e4bc8e6 100644
--- a/macos/Sources/Features/AppleScript/ScriptWindow.swift
+++ b/macos/Sources/Features/AppleScript/ScriptWindow.swift
@@ -174,6 +174,42 @@ final class ScriptWindow: NSObject {
return controllers.first
}
+ /// Handler for `activate window `.
+ @objc(handleActivateWindowCommand:)
+ func handleActivateWindow(_ command: NSScriptCommand) -> Any? {
+ guard NSApp.validateScript(command: command) else { return nil }
+
+ guard let windowContainer = preferredParentWindow else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Window is no longer available."
+ return nil
+ }
+
+ windowContainer.makeKeyAndOrderFront(nil)
+ NSApp.activate(ignoringOtherApps: true)
+ return nil
+ }
+
+ /// Handler for `close window `.
+ @objc(handleCloseWindowCommand:)
+ func handleCloseWindow(_ command: NSScriptCommand) -> Any? {
+ guard NSApp.validateScript(command: command) else { return nil }
+
+ if let managedTerminalController = preferredController as? TerminalController {
+ managedTerminalController.closeWindowImmediately()
+ return nil
+ }
+
+ guard let windowContainer = preferredParentWindow else {
+ command.scriptErrorNumber = errAEEventFailed
+ command.scriptErrorString = "Window is no longer available."
+ return nil
+ }
+
+ windowContainer.close()
+ return nil
+ }
+
/// Provides Cocoa scripting with a canonical "path" back to this object.
///
/// Without this, Cocoa can return data but cannot reliably build object