import AppKit /// AppleScript-facing wrapper around a single tab in a scripting window. /// /// `ScriptWindow.tabs` vends these objects so AppleScript can traverse /// `window -> tab` without knowing anything about AppKit controllers. @MainActor @objc(GhosttyScriptTab) final class ScriptTab: NSObject { /// Stable identifier used by AppleScript `tab id "..."` references. private let stableID: String /// Weak back-reference to the scripting window that owns this tab wrapper. /// /// We only need this for dynamic properties (`index`, `selected`) and for /// building an object specifier path. private weak var window: ScriptWindow? /// Live terminal controller for this tab. /// /// This can become `nil` if the tab closes while a script is running. private weak var controller: BaseTerminalController? /// Called by `ScriptWindow.tabs` / `ScriptWindow.selectedTab`. /// /// The ID is computed once so object specifiers built from this instance keep /// a consistent tab identity. init(window: ScriptWindow, controller: BaseTerminalController) { self.stableID = Self.stableID(controller: controller) self.window = window self.controller = controller } /// Exposed as the AppleScript `id` property. @objc(id) var idValue: String { stableID } /// Exposed as the AppleScript `title` property. /// /// Returns the title of the tab's window. @objc(title) var title: String { controller?.window?.title ?? "" } /// Exposed as the AppleScript `index` property. /// /// Cocoa scripting expects this to be 1-based for user-facing collections. @objc(index) var index: Int { guard let controller else { return 0 } return window?.tabIndex(for: controller) ?? 0 } /// Exposed as the AppleScript `selected` property. /// /// Powers script conditions such as `if selected of tab 1 then ...`. @objc(selected) var selected: Bool { guard let controller else { return false } return window?.tabIsSelected(controller) ?? false } /// Exposed as the AppleScript `terminals` element on a tab. /// /// Returns all terminal surfaces (split panes) within this tab. @objc(terminals) var terminals: [ScriptTerminal] { guard let controller else { return [] } return (controller.surfaceTree.root?.leaves() ?? []) .map(ScriptTerminal.init) } /// Enables unique-ID lookup for `terminals` references on a tab. @objc(valueInTerminalsWithUniqueID:) func valueInTerminals(uniqueID: String) -> ScriptTerminal? { guard let controller else { return nil } return (controller.surfaceTree.root?.leaves() ?? []) .first(where: { $0.id.uuidString == uniqueID }) .map(ScriptTerminal.init) } /// Provides Cocoa scripting with a canonical "path" back to this object. override var objectSpecifier: NSScriptObjectSpecifier? { guard let window else { return nil } guard let windowClassDescription = window.classDescription as? NSScriptClassDescription else { return nil } guard let windowSpecifier = window.objectSpecifier else { return nil } // This tells Cocoa how to re-find this tab later: // application -> scriptWindows[id] -> tabs[id]. return NSUniqueIDSpecifier( containerClassDescription: windowClassDescription, containerSpecifier: windowSpecifier, key: "tabs", uniqueID: stableID ) } } extension ScriptTab { /// Stable ID for one tab controller. /// /// Tab identity belongs to `ScriptTab`, so both tab creation and tab ID /// lookups in `ScriptWindow` call this helper. static func stableID(controller: BaseTerminalController) -> String { "tab-\(ObjectIdentifier(controller).hexString)" } }