Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261import AppKit
/// AppleScript-facing wrapper around a logical Ghostty window.////// In AppKit, each tab is often its own `NSWindow`. AppleScript users, however,/// expect a single window object containing a list of tabs.////// `ScriptWindow` is that compatibility layer:/// - It presents one object per tab group./// - It translates tab-group state into `tabs` and `selected tab`./// - It exposes stable IDs that Cocoa scripting can resolve later.@MainActor@objc(GhosttyScriptWindow)final class ScriptWindow: NSObject { /// Stable identifier used by AppleScript `window id "..."` references. /// /// We precompute this once so the object keeps a consistent ID for its whole /// lifetime, even if AppKit window bookkeeping changes after creation. let stableID: String
/// Canonical representative for this scripting window's tab group. /// /// We intentionally keep only one controller reference; full tab membership /// is derived lazily from current AppKit state whenever needed. private weak var primaryController: BaseTerminalController?
/// `scriptWindows` in `AppDelegate+AppleScript` constructs these objects. /// /// `stableID` must match the same identity scheme used by /// `valueInScriptWindowsWithUniqueID:` so Cocoa can re-resolve object /// specifiers produced earlier in a script. init(primaryController: BaseTerminalController) { self.stableID = Self.stableID(primaryController: primaryController) self.primaryController = primaryController }
/// Exposed as the AppleScript `id` property. /// /// This is what scripts read with `id of window ...`. @objc(id) var idValue: String { guard NSApp.isAppleScriptEnabled else { return "" } return stableID }
/// Exposed as the AppleScript `title` property. /// /// Returns the title of the window (from the selected/primary controller's NSWindow). @objc(title) var title: String { guard NSApp.isAppleScriptEnabled else { return "" } return selectedController?.window?.title ?? "" }
/// Exposed as the AppleScript `tabs` element. /// /// Cocoa asks for this collection when a script evaluates `tabs of window ...` /// or any tab-filter expression. We build wrappers from live controller state /// so tab additions/removals are reflected immediately. @objc(tabs) var tabs: [ScriptTab] { guard NSApp.isAppleScriptEnabled else { return [] } return controllers.map { ScriptTab(window: self, controller: $0) } }
/// Exposed as the AppleScript `selected tab` property. /// /// This powers expressions like `selected tab of window 1`. @objc(selectedTab) var selectedTab: ScriptTab? { guard NSApp.isAppleScriptEnabled else { return nil } guard let selectedController else { return nil } return ScriptTab(window: self, controller: selectedController) }
/// Enables unique-ID lookup for `tabs` references. /// /// Required selector pattern for the `tabs` element key: /// `valueInTabsWithUniqueID:`. /// /// Cocoa uses this when a script resolves `tab id "..." of window ...`. @objc(valueInTabsWithUniqueID:) func valueInTabs(uniqueID: String) -> ScriptTab? { guard NSApp.isAppleScriptEnabled else { return nil } guard let controller = controller(tabID: uniqueID) else { return nil } return ScriptTab(window: self, controller: controller) }
/// Exposed as the AppleScript `terminals` element on a window. /// /// Returns all terminal surfaces across every tab in this window. @objc(terminals) var terminals: [ScriptTerminal] { guard NSApp.isAppleScriptEnabled else { return [] } return controllers .flatMap { $0.surfaceTree.root?.leaves() ?? [] } .map(ScriptTerminal.init) }
/// Enables unique-ID lookup for `terminals` references on a window. @objc(valueInTerminalsWithUniqueID:) func valueInTerminals(uniqueID: String) -> ScriptTerminal? { guard NSApp.isAppleScriptEnabled else { return nil } return controllers .flatMap { $0.surfaceTree.root?.leaves() ?? [] } .first(where: { $0.id.uuidString == uniqueID }) .map(ScriptTerminal.init) }
/// AppleScript tab indexes are 1-based, so we add one to Swift's 0-based /// array index. func tabIndex(for controller: BaseTerminalController) -> Int? { guard NSApp.isAppleScriptEnabled else { return nil } return controllers.firstIndex(where: { $0 === controller }).map { $0 + 1 } }
/// Reports whether a given controller maps to this window's selected tab. func tabIsSelected(_ controller: BaseTerminalController) -> Bool { guard NSApp.isAppleScriptEnabled else { return false } return selectedController === controller }
/// Best-effort native window to use as a tab parent for AppleScript commands. var preferredParentWindow: NSWindow? { guard NSApp.isAppleScriptEnabled else { return nil } return selectedController?.window ?? controllers.first?.window }
/// Best-effort controller to use for window-scoped AppleScript commands. var preferredController: BaseTerminalController? { guard NSApp.isAppleScriptEnabled else { return nil } return 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 }) }
/// Live controller list for this scripting window. /// /// We recalculate on every access so AppleScript immediately sees tab-group /// changes (new tabs, closed tabs, tab moves) without rebuilding all objects. private var controllers: [BaseTerminalController] { guard NSApp.isAppleScriptEnabled else { return [] } guard let primaryController else { return [] } guard let window = primaryController.window else { return [primaryController] }
if let tabGroup = window.tabGroup { let groupControllers = tabGroup.windows.compactMap { $0.windowController as? BaseTerminalController } if !groupControllers.isEmpty { return groupControllers } }
return [primaryController] }
/// Live selected controller for this scripting window. /// /// AppKit tracks selected tab on `NSWindowTabGroup.selectedWindow`; for /// non-tabbed windows we fall back to the primary controller. private var selectedController: BaseTerminalController? { guard let primaryController else { return nil } guard let window = primaryController.window else { return primaryController }
if let tabGroup = window.tabGroup, let selectedController = tabGroup.selectedWindow?.windowController as? BaseTerminalController { return selectedController }
return controllers.first }
/// Handler for `activate window <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 <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 /// references for later script statements. This specifier encodes: /// `application -> scriptWindows[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: "scriptWindows", uniqueID: stableID ) }}
extension ScriptWindow { /// Produces the window-level stable ID from the primary controller. /// /// - Tabbed windows are keyed by tab-group identity. /// - Standalone windows are keyed by window identity. /// - Detached controllers fall back to controller identity. static func stableID(primaryController: BaseTerminalController) -> String { guard let window = primaryController.window else { return "controller-\(ObjectIdentifier(primaryController).hexString)" }
if let tabGroup = window.tabGroup { return stableID(tabGroup: tabGroup) }
return stableID(window: window) }
/// Stable ID for a standalone native window. static func stableID(window: NSWindow) -> String { "window-\(ObjectIdentifier(window).hexString)" }
/// Stable ID for a native AppKit tab group. static func stableID(tabGroup: NSWindowTabGroup) -> String { "tab-group-\(ObjectIdentifier(tabGroup).hexString)" }}