Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223import AppKit
/// AppleScript-facing wrapper around a live Ghostty terminal surface.////// This class is intentionally ObjC-visible because Cocoa scripting resolves/// AppleScript objects through Objective-C runtime names/selectors, not Swift/// protocol conformance.////// Mapping from `Ghostty.sdef`:/// - `class terminal` -> this class (`@objc(GhosttyAppleScriptTerminal)`)./// - `property id` -> `@objc(id)` getter below./// - `property title` -> `@objc(title)` getter below./// - `property working directory` -> `@objc(workingDirectory)` getter below./// - `property pid` -> `@objc(pid)` getter below./// - `property tty` -> `@objc(tty)` getter below.////// We keep only a weak reference to the underlying `SurfaceView` so this/// wrapper never extends the terminal's lifetime.@MainActor@objc(GhosttyScriptTerminal)final class ScriptTerminal: NSObject { /// Weak reference to the underlying surface. Package-visible so that /// other AppleScript command handlers (e.g. `ScriptSplitCommand`) can /// access the live surface without exposing it to ObjC/AppleScript. weak var surfaceView: Ghostty.SurfaceView?
init(surfaceView: Ghostty.SurfaceView) { self.surfaceView = surfaceView }
/// Exposed as the AppleScript `id` property. /// /// This is a stable UUID string for the life of a surface and is also used /// by `NSUniqueIDSpecifier` to re-identify a terminal object in scripts. @objc(id) var stableID: String { guard NSApp.isAppleScriptEnabled else { return "" } return surfaceView?.id.uuidString ?? "" }
/// Exposed as the AppleScript `title` property. @objc(title) var title: String { guard NSApp.isAppleScriptEnabled else { return "" } return surfaceView?.title ?? "" }
/// Exposed as the AppleScript `working directory` property. /// /// The `sdef` uses a spaced name, but Cocoa scripting maps that to the /// camel-cased selector name `workingDirectory`. @objc(workingDirectory) var workingDirectory: String { guard NSApp.isAppleScriptEnabled else { return "" } return surfaceView?.pwd ?? "" }
/// Exposed as the AppleScript `pid` property. @objc(pid) var pid: Int { guard NSApp.isAppleScriptEnabled else { return 0 } return surfaceView?.surfaceModel?.foregroundPID ?? 0 }
/// Exposed as the AppleScript `tty` property. @objc(tty) var tty: String { guard NSApp.isAppleScriptEnabled else { return "" } return surfaceView?.surfaceModel?.ttyName ?? "" }
/// Used by command handling (`perform action ... on <terminal>`). func perform(action: String) -> Bool { guard NSApp.isAppleScriptEnabled else { return false } guard let surfaceModel = surfaceView?.surfaceModel else { return false } return surfaceModel.perform(action: action) }
/// Handler for `split <terminal> direction <dir>`. @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 <terminal>`. @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 <terminal>`. @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 /// referenced in follow-up script statements because AppleScript cannot /// express where the object came from (`application.terminals[id]`). override var objectSpecifier: NSScriptObjectSpecifier? { guard NSApp.isAppleScriptEnabled else { return nil } guard let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else { return nil }
return NSUniqueIDSpecifier( containerClassDescription: appClassDescription, containerSpecifier: nil, key: "terminals", uniqueID: stableID ) }}
/// 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<Ghostty.SurfaceView>.NewDirection { switch self { case .right: .right case .left: .left case .down: .down case .up: .up } }}