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