macos: add new tab command

This commit is contained in:
Mitchell Hashimoto
2026-03-06 12:37:51 -08:00
parent 4d5de702f2
commit d271c8ccaa
4 changed files with 96 additions and 0 deletions

View File

@@ -16,6 +16,9 @@
<responds-to command="new window">
<cocoa method="handleNewWindowScriptCommand:"/>
</responds-to>
<responds-to command="new tab">
<cocoa method="handleNewTabScriptCommand:"/>
</responds-to>
<responds-to command="new surface configuration">
<cocoa method="handleNewSurfaceConfigurationScriptCommand:"/>
</responds-to>
@@ -141,6 +144,16 @@
<result type="window" description="The newly created window."/>
</command>
<command name="new tab" code="GhstNTab" description="Create a new Ghostty tab.">
<parameter name="in" code="GNtW" type="window" optional="yes" description="Target window for the new tab.">
<cocoa key="window"/>
</parameter>
<parameter name="with configuration" code="GNtS" type="surface configuration" optional="yes" description="Base surface configuration for the initial terminal.">
<cocoa key="configuration"/>
</parameter>
<result type="tab" description="The newly created tab."/>
</command>
<command name="split" code="GhstSplt" description="Split a terminal in the given direction.">
<cocoa class="GhosttyScriptSplitCommand"/>
<parameter name="terminal" code="GSpT" type="terminal" description="The terminal to split.">

View File

@@ -182,6 +182,80 @@ extension NSApplication {
// has not refreshed yet in the current run loop.
return ScriptWindow(primaryController: controller)
}
/// Handler for the `new tab` AppleScript command.
///
/// Required selector name from the command in `sdef`:
/// `handleNewTabScriptCommand:`.
///
/// Accepts an optional target window and optional surface configuration.
/// If no window is provided, this mirrors App Intents and uses the
/// preferred parent window.
///
/// Returns the newly created scripting tab object.
@objc(handleNewTabScriptCommand:)
func handleNewTabScriptCommand(_ command: NSScriptCommand) -> Any? {
guard let appDelegate = delegate as? AppDelegate else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Ghostty app delegate is unavailable."
return nil
}
let baseConfig: Ghostty.SurfaceConfiguration
do {
if let scriptRecord = command.evaluatedArguments?["configuration"] as? NSDictionary {
baseConfig = try Ghostty.SurfaceConfiguration(scriptRecord: scriptRecord)
} else {
baseConfig = Ghostty.SurfaceConfiguration()
}
} catch {
command.scriptErrorNumber = errAECoercionFail
command.scriptErrorString = error.localizedDescription
return nil
}
let targetWindow = command.evaluatedArguments?["window"] as? ScriptWindow
let parentWindow: NSWindow?
if let targetWindow {
guard let resolvedWindow = targetWindow.preferredParentWindow else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Target window is no longer available."
return nil
}
parentWindow = resolvedWindow
} else {
parentWindow = TerminalController.preferredParent?.window
}
guard let createdController = TerminalController.newTab(
appDelegate.ghostty,
from: parentWindow,
withBaseConfig: baseConfig
) else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Failed to create tab."
return nil
}
let createdTabID = ScriptTab.stableID(controller: createdController)
if let targetWindow,
let scriptTab = targetWindow.valueInTabs(uniqueID: createdTabID) {
return scriptTab
}
for scriptWindow in scriptWindows {
if let scriptTab = scriptWindow.valueInTabs(uniqueID: createdTabID) {
return scriptTab
}
}
// Fall back to wrapping the created controller if AppKit tab-group
// bookkeeping has not fully refreshed in the current run loop.
let fallbackWindow = ScriptWindow(primaryController: createdController)
return ScriptTab(window: fallbackWindow, controller: createdController)
}
}
// MARK: - Private Helpers

View File

@@ -9,6 +9,10 @@ extension Ghostty.SurfaceConfiguration: ScriptRecord {
init(scriptRecord source: NSDictionary?) throws {
self.init()
guard let source else {
return
}
guard let raw = source as? [String: Any] else {
throw RecordParseError.invalidType(parameter: "configuration", expected: "a surface configuration record")
}

View File

@@ -111,6 +111,11 @@ final class ScriptWindow: NSObject {
selectedController === controller
}
/// Best-effort native window to use as a tab parent for AppleScript commands.
var preferredParentWindow: NSWindow? {
selectedController?.window ?? controllers.first?.window
}
/// 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 })