macos: add AppleScript split command

Add a new `split` command to the AppleScript scripting dictionary that
splits a terminal in a given direction (right, left, down, up) and
returns the newly created terminal.

The command is exposed as:
  split terminal <terminal> direction <direction>

Also adds a `fourCharCode` String extension for converting four-character
ASCII strings to their FourCharCode (UInt32) representation.
This commit is contained in:
Mitchell Hashimoto
2026-03-05 20:46:52 -08:00
parent 40c74811f1
commit ef669eeae7
5 changed files with 108 additions and 2 deletions

View File

@@ -8,6 +8,7 @@
<responds-to command="perform action">
<cocoa method="handlePerformActionScriptCommand:"/>
</responds-to>
<element type="terminal" access="r">
<cocoa key="terminals"/>
</element>
@@ -25,6 +26,24 @@
<parameter name="on" code="GonT" type="terminal" description="Target terminal."/>
<result type="boolean" description="True when the action was performed."/>
</command>
<enumeration name="split direction" code="GSpD" description="Direction for a new split.">
<enumerator name="right" code="GSrt" description="Split to the right."/>
<enumerator name="left" code="GSlf" description="Split to the left."/>
<enumerator name="down" code="GSdn" description="Split downward."/>
<enumerator name="up" code="GSup" description="Split upward."/>
</enumeration>
<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.">
<cocoa key="terminal"/>
</parameter>
<parameter name="direction" code="GSpd" type="split direction" description="The direction to split.">
<cocoa key="direction"/>
</parameter>
<result type="terminal" description="The newly created terminal."/>
</command>
</suite>
<!--

View File

@@ -137,7 +137,8 @@
"Features/App Intents/NewTerminalIntent.swift",
"Features/App Intents/QuickTerminalIntent.swift",
"Features/AppleScript/AppDelegate+AppleScript.swift",
Features/AppleScript/AppleScriptTerminal.swift,
Features/AppleScript/ScriptSplitCommand.swift,
Features/AppleScript/ScriptTerminal.swift,
Features/ClipboardConfirmation/ClipboardConfirmation.xib,
Features/ClipboardConfirmation/ClipboardConfirmationController.swift,
Features/ClipboardConfirmation/ClipboardConfirmationView.swift,

View File

@@ -0,0 +1,74 @@
import AppKit
/// Handler for the `split` AppleScript command defined in `Ghostty.sdef`.
///
/// Cocoa scripting instantiates this class because the command's `<cocoa>` element
/// specifies `class="GhosttyScriptSplitCommand"`. The runtime calls
/// `performDefaultImplementation()` to execute the command.
@MainActor
@objc(GhosttyScriptSplitCommand)
final class ScriptSplitCommand: NSScriptCommand {
override func performDefaultImplementation() -> Any? {
guard let terminal = evaluatedArguments?["terminal"] as? ScriptTerminal else {
scriptErrorNumber = errAEParamMissed
scriptErrorString = "Missing terminal target."
return nil
}
guard let surfaceView = terminal.surfaceView else {
scriptErrorNumber = errAEEventFailed
scriptErrorString = "Terminal surface is no longer available."
return nil
}
guard let directionCode = evaluatedArguments?["direction"] as? UInt32,
let direction = ScriptSplitDirection(code: directionCode) else {
scriptErrorNumber = errAEParamMissed
scriptErrorString = "Missing or unknown split direction."
return nil
}
// Find the window controller that owns this surface.
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
scriptErrorNumber = errAEEventFailed
scriptErrorString = "Terminal is not in a splittable window."
return nil
}
guard let newView = controller.newSplit(at: surfaceView, direction: direction.splitDirection) else {
scriptErrorNumber = errAEEventFailed
scriptErrorString = "Failed to create split."
return nil
}
return ScriptTerminal(surfaceView: newView)
}
}
/// Four-character codes matching the `split direction` enumeration in `Ghostty.sdef`.
private 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
}
}
}

View File

@@ -17,7 +17,10 @@ import AppKit
@MainActor
@objc(GhosttyScriptTerminal)
final class ScriptTerminal: NSObject {
private weak var surfaceView: Ghostty.SurfaceView?
/// 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

View File

@@ -27,4 +27,13 @@ extension String {
}
#endif
/// Converts a four-character ASCII string to its `FourCharCode` (`UInt32`) value.
var fourCharCode: UInt32 {
assert(count <= 4, "FourCharCode string must be at most 4 characters")
var result: UInt32 = 0
for byte in utf8.prefix(4) {
result = (result << 8) | UInt32(byte)
}
return result
}
}