mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
Change split, focus, close, activate window, select tab, close tab, and close window commands to accept their target object as a direct parameter instead of a named parameter. This produces natural AppleScript syntax: activate window (window 1) close tab (tab 1 of window 1) split (terminal 1) direction right instead of the awkward redundant form: activate window window (window 1) close tab tab (tab 1 of window 1) split terminal (terminal 1) direction right The implementation moves command logic from NSScriptCommand subclasses into responds-to handler methods on ScriptTerminal, ScriptWindow, and ScriptTab, which is the standard Cocoa Scripting pattern for commands whose direct parameter is an application class.
174 lines
6.1 KiB
Swift
174 lines
6.1 KiB
Swift
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 {
|
|
guard NSApp.isAppleScriptEnabled else { return "" }
|
|
return stableID
|
|
}
|
|
|
|
/// Exposed as the AppleScript `title` property.
|
|
///
|
|
/// Returns the title of the tab's window.
|
|
@objc(title)
|
|
var title: String {
|
|
guard NSApp.isAppleScriptEnabled else { return "" }
|
|
return 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 NSApp.isAppleScriptEnabled else { return 0 }
|
|
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 NSApp.isAppleScriptEnabled else { return false }
|
|
guard let controller else { return false }
|
|
return window?.tabIsSelected(controller) ?? false
|
|
}
|
|
|
|
/// Best-effort native window containing this tab.
|
|
var parentWindow: NSWindow? {
|
|
guard NSApp.isAppleScriptEnabled else { return nil }
|
|
return controller?.window
|
|
}
|
|
|
|
/// Live controller backing this tab wrapper.
|
|
var parentController: BaseTerminalController? {
|
|
guard NSApp.isAppleScriptEnabled else { return nil }
|
|
return controller
|
|
}
|
|
|
|
/// Exposed as the AppleScript `terminals` element on a tab.
|
|
///
|
|
/// Returns all terminal surfaces (split panes) within this tab.
|
|
@objc(terminals)
|
|
var terminals: [ScriptTerminal] {
|
|
guard NSApp.isAppleScriptEnabled else { return [] }
|
|
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 NSApp.isAppleScriptEnabled else { return nil }
|
|
guard let controller else { return nil }
|
|
return (controller.surfaceTree.root?.leaves() ?? [])
|
|
.first(where: { $0.id.uuidString == uniqueID })
|
|
.map(ScriptTerminal.init)
|
|
}
|
|
|
|
/// Handler for `select tab <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 <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 }
|
|
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)"
|
|
}
|
|
}
|