mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-18 05:20:29 +00:00
macos: add AppleScript commands for text input, key, and mouse events
Add five new AppleScript commands to Ghostty.sdef mirroring the existing App Intents for terminal input: - `input text`: send text to a terminal as if pasted - `send key`: simulate a keyboard event with optional action and modifiers - `send mouse button`: send a mouse button press/release event - `send mouse position`: send a mouse cursor position event - `send mouse scroll`: send a scroll event with precision and momentum A shared `input action` enumeration (press/release) is used by both key and mouse button commands. Modifier keys are passed as a comma-separated string parameter (shift, control, option, command).
This commit is contained in:
@@ -58,6 +58,98 @@
|
||||
<cocoa key="terminal"/>
|
||||
</parameter>
|
||||
</command>
|
||||
|
||||
<command name="input text" code="GhstInTx" description="Input text to a terminal as if it was pasted.">
|
||||
<cocoa class="GhosttyScriptInputTextCommand"/>
|
||||
<direct-parameter type="text" description="The text to input."/>
|
||||
<parameter name="to" code="GItT" type="terminal" description="The terminal to input text to.">
|
||||
<cocoa key="terminal"/>
|
||||
</parameter>
|
||||
</command>
|
||||
|
||||
<enumeration name="input action" code="GIAc" description="Whether an input is pressed or released.">
|
||||
<enumerator name="press" code="GIpr" description="Press."/>
|
||||
<enumerator name="release" code="GIrl" description="Release."/>
|
||||
</enumeration>
|
||||
|
||||
<command name="send key" code="GhstSKey" description="Send a keyboard event to a terminal.">
|
||||
<cocoa class="GhosttyScriptKeyEventCommand"/>
|
||||
<direct-parameter type="text" description="The key name (e.g. "enter", "a", "space")."/>
|
||||
<parameter name="action" code="GKeA" type="input action" optional="yes" description="Press or release (default: press).">
|
||||
<cocoa key="action"/>
|
||||
</parameter>
|
||||
<parameter name="modifiers" code="GKeM" type="text" optional="yes" description="Comma-separated modifier keys: shift, control, option, command.">
|
||||
<cocoa key="modifiers"/>
|
||||
</parameter>
|
||||
<parameter name="to" code="GKeT" type="terminal" description="The terminal to send the key event to.">
|
||||
<cocoa key="terminal"/>
|
||||
</parameter>
|
||||
</command>
|
||||
|
||||
<enumeration name="mouse button" code="GMBt" description="A mouse button.">
|
||||
<enumerator name="left button" code="GMlf" description="Left mouse button."/>
|
||||
<enumerator name="right button" code="GMrt" description="Right mouse button."/>
|
||||
<enumerator name="middle button" code="GMmd" description="Middle mouse button."/>
|
||||
</enumeration>
|
||||
|
||||
<command name="send mouse button" code="GhstSMBt" description="Send a mouse button event to a terminal.">
|
||||
<cocoa class="GhosttyScriptMouseButtonCommand"/>
|
||||
<direct-parameter type="mouse button" description="The mouse button."/>
|
||||
<parameter name="action" code="GMbA" type="input action" optional="yes" description="Press or release (default: press).">
|
||||
<cocoa key="action"/>
|
||||
</parameter>
|
||||
<parameter name="modifiers" code="GMbM" type="text" optional="yes" description="Comma-separated modifier keys: shift, control, option, command.">
|
||||
<cocoa key="modifiers"/>
|
||||
</parameter>
|
||||
<parameter name="to" code="GMbT" type="terminal" description="The terminal to send the event to.">
|
||||
<cocoa key="terminal"/>
|
||||
</parameter>
|
||||
</command>
|
||||
|
||||
<command name="send mouse position" code="GhstSMPs" description="Send a mouse position event to a terminal.">
|
||||
<cocoa class="GhosttyScriptMousePosCommand"/>
|
||||
<parameter name="x" code="GMpX" type="real" description="Horizontal position in pixels.">
|
||||
<cocoa key="x"/>
|
||||
</parameter>
|
||||
<parameter name="y" code="GMpY" type="real" description="Vertical position in pixels.">
|
||||
<cocoa key="y"/>
|
||||
</parameter>
|
||||
<parameter name="modifiers" code="GMpM" type="text" optional="yes" description="Comma-separated modifier keys: shift, control, option, command.">
|
||||
<cocoa key="modifiers"/>
|
||||
</parameter>
|
||||
<parameter name="to" code="GMpT" type="terminal" description="The terminal to send the event to.">
|
||||
<cocoa key="terminal"/>
|
||||
</parameter>
|
||||
</command>
|
||||
|
||||
<enumeration name="scroll momentum" code="GSMo" description="Momentum phase for inertial scrolling.">
|
||||
<enumerator name="none" code="SMno" description="No momentum."/>
|
||||
<enumerator name="began" code="SMbg" description="Momentum began."/>
|
||||
<enumerator name="changed" code="SMch" description="Momentum changed."/>
|
||||
<enumerator name="ended" code="SMen" description="Momentum ended."/>
|
||||
<enumerator name="cancelled" code="SMcn" description="Momentum cancelled."/>
|
||||
<enumerator name="may begin" code="SMmb" description="Momentum may begin."/>
|
||||
<enumerator name="stationary" code="SMst" description="Stationary."/>
|
||||
</enumeration>
|
||||
|
||||
<command name="send mouse scroll" code="GhstSMSc" description="Send a mouse scroll event to a terminal.">
|
||||
<cocoa class="GhosttyScriptMouseScrollCommand"/>
|
||||
<parameter name="x" code="GMsX" type="real" description="Horizontal scroll delta.">
|
||||
<cocoa key="x"/>
|
||||
</parameter>
|
||||
<parameter name="y" code="GMsY" type="real" description="Vertical scroll delta.">
|
||||
<cocoa key="y"/>
|
||||
</parameter>
|
||||
<parameter name="precision" code="GMsP" type="boolean" optional="yes" description="High-precision scroll (e.g. trackpad). Default: false.">
|
||||
<cocoa key="precision"/>
|
||||
</parameter>
|
||||
<parameter name="momentum" code="GMsM" type="scroll momentum" optional="yes" description="Momentum phase for inertial scrolling. Default: none.">
|
||||
<cocoa key="momentum"/>
|
||||
</parameter>
|
||||
<parameter name="to" code="GMsT" type="terminal" description="The terminal to send the event to.">
|
||||
<cocoa key="terminal"/>
|
||||
</parameter>
|
||||
</command>
|
||||
</suite>
|
||||
|
||||
<!--
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
extension Ghostty.Input.Mods {
|
||||
/// Parses a comma-separated modifier string into `Ghostty.Input.Mods`.
|
||||
///
|
||||
/// Recognized names: `shift`, `control`, `option`, `command`.
|
||||
/// Returns `nil` if any unrecognized modifier name is encountered.
|
||||
init?(scriptModifiers string: String) {
|
||||
self = []
|
||||
for part in string.split(separator: ",") {
|
||||
switch part.trimmingCharacters(in: .whitespaces).lowercased() {
|
||||
case "shift": insert(.shift)
|
||||
case "control": insert(.ctrl)
|
||||
case "option": insert(.alt)
|
||||
case "command": insert(.super)
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import AppKit
|
||||
|
||||
/// Handler for the `input text` AppleScript command defined in `Ghostty.sdef`.
|
||||
///
|
||||
/// Cocoa scripting instantiates this class because the command's `<cocoa>` element
|
||||
/// specifies `class="GhosttyScriptInputTextCommand"`. The runtime calls
|
||||
/// `performDefaultImplementation()` to execute the command.
|
||||
@MainActor
|
||||
@objc(GhosttyScriptInputTextCommand)
|
||||
final class ScriptInputTextCommand: NSScriptCommand {
|
||||
override func performDefaultImplementation() -> Any? {
|
||||
guard let text = directParameter as? String else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = "Missing text to input."
|
||||
return nil
|
||||
}
|
||||
|
||||
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 surface = surfaceView.surfaceModel else {
|
||||
scriptErrorNumber = errAEEventFailed
|
||||
scriptErrorString = "Terminal surface model is not available."
|
||||
return nil
|
||||
}
|
||||
|
||||
surface.sendText(text)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import AppKit
|
||||
|
||||
/// Handler for the `send key` AppleScript command defined in `Ghostty.sdef`.
|
||||
///
|
||||
/// Cocoa scripting instantiates this class because the command's `<cocoa>` element
|
||||
/// specifies `class="GhosttyScriptKeyEventCommand"`. The runtime calls
|
||||
/// `performDefaultImplementation()` to execute the command.
|
||||
@MainActor
|
||||
@objc(GhosttyScriptKeyEventCommand)
|
||||
final class ScriptKeyEventCommand: NSScriptCommand {
|
||||
override func performDefaultImplementation() -> Any? {
|
||||
guard let keyName = directParameter as? String else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = "Missing key name."
|
||||
return nil
|
||||
}
|
||||
|
||||
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 surface = surfaceView.surfaceModel else {
|
||||
scriptErrorNumber = errAEEventFailed
|
||||
scriptErrorString = "Terminal surface model is not available."
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let key = Ghostty.Input.Key(rawValue: keyName) else {
|
||||
scriptErrorNumber = errAECoercionFail
|
||||
scriptErrorString = "Unknown key name: \(keyName)"
|
||||
return nil
|
||||
}
|
||||
|
||||
let action: Ghostty.Input.Action
|
||||
if let actionCode = evaluatedArguments?["action"] as? UInt32 {
|
||||
switch actionCode {
|
||||
case "GIpr".fourCharCode: action = .press
|
||||
case "GIrl".fourCharCode: action = .release
|
||||
default: action = .press
|
||||
}
|
||||
} else {
|
||||
action = .press
|
||||
}
|
||||
|
||||
let mods: Ghostty.Input.Mods
|
||||
if let modsString = evaluatedArguments?["modifiers"] as? String {
|
||||
guard let parsed = Ghostty.Input.Mods(scriptModifiers: modsString) else {
|
||||
scriptErrorNumber = errAECoercionFail
|
||||
scriptErrorString = "Unknown modifier in: \(modsString)"
|
||||
return nil
|
||||
}
|
||||
mods = parsed
|
||||
} else {
|
||||
mods = []
|
||||
}
|
||||
|
||||
let keyEvent = Ghostty.Input.KeyEvent(
|
||||
key: key,
|
||||
action: action,
|
||||
mods: mods
|
||||
)
|
||||
surface.sendKeyEvent(keyEvent)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import AppKit
|
||||
|
||||
/// Handler for the `send mouse button` AppleScript command defined in `Ghostty.sdef`.
|
||||
///
|
||||
/// Cocoa scripting instantiates this class because the command's `<cocoa>` element
|
||||
/// specifies `class="GhosttyScriptMouseButtonCommand"`. The runtime calls
|
||||
/// `performDefaultImplementation()` to execute the command.
|
||||
@MainActor
|
||||
@objc(GhosttyScriptMouseButtonCommand)
|
||||
final class ScriptMouseButtonCommand: NSScriptCommand {
|
||||
override func performDefaultImplementation() -> Any? {
|
||||
guard let buttonCode = directParameter as? UInt32,
|
||||
let button = ScriptMouseButtonValue(code: buttonCode) else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = "Missing or unknown mouse button."
|
||||
return nil
|
||||
}
|
||||
|
||||
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 surface = surfaceView.surfaceModel else {
|
||||
scriptErrorNumber = errAEEventFailed
|
||||
scriptErrorString = "Terminal surface model is not available."
|
||||
return nil
|
||||
}
|
||||
|
||||
let action: Ghostty.Input.MouseState
|
||||
if let actionCode = evaluatedArguments?["action"] as? UInt32 {
|
||||
switch actionCode {
|
||||
case "GIpr".fourCharCode: action = .press
|
||||
case "GIrl".fourCharCode: action = .release
|
||||
default: action = .press
|
||||
}
|
||||
} else {
|
||||
action = .press
|
||||
}
|
||||
|
||||
let mods: Ghostty.Input.Mods
|
||||
if let modsString = evaluatedArguments?["modifiers"] as? String {
|
||||
guard let parsed = Ghostty.Input.Mods(scriptModifiers: modsString) else {
|
||||
scriptErrorNumber = errAECoercionFail
|
||||
scriptErrorString = "Unknown modifier in: \(modsString)"
|
||||
return nil
|
||||
}
|
||||
mods = parsed
|
||||
} else {
|
||||
mods = []
|
||||
}
|
||||
|
||||
let mouseEvent = Ghostty.Input.MouseButtonEvent(
|
||||
action: action,
|
||||
button: button.ghosttyButton,
|
||||
mods: mods
|
||||
)
|
||||
surface.sendMouseButton(mouseEvent)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Four-character codes matching the `mouse button` enumeration in `Ghostty.sdef`.
|
||||
private enum ScriptMouseButtonValue {
|
||||
case left
|
||||
case right
|
||||
case middle
|
||||
|
||||
init?(code: UInt32) {
|
||||
switch code {
|
||||
case "GMlf".fourCharCode: self = .left
|
||||
case "GMrt".fourCharCode: self = .right
|
||||
case "GMmd".fourCharCode: self = .middle
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var ghosttyButton: Ghostty.Input.MouseButton {
|
||||
switch self {
|
||||
case .left: .left
|
||||
case .right: .right
|
||||
case .middle: .middle
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import AppKit
|
||||
|
||||
/// Handler for the `send mouse position` AppleScript command defined in `Ghostty.sdef`.
|
||||
///
|
||||
/// Cocoa scripting instantiates this class because the command's `<cocoa>` element
|
||||
/// specifies `class="GhosttyScriptMousePosCommand"`. The runtime calls
|
||||
/// `performDefaultImplementation()` to execute the command.
|
||||
@MainActor
|
||||
@objc(GhosttyScriptMousePosCommand)
|
||||
final class ScriptMousePosCommand: NSScriptCommand {
|
||||
override func performDefaultImplementation() -> Any? {
|
||||
guard let x = evaluatedArguments?["x"] as? Double else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = "Missing x position."
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let y = evaluatedArguments?["y"] as? Double else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = "Missing y position."
|
||||
return nil
|
||||
}
|
||||
|
||||
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 surface = surfaceView.surfaceModel else {
|
||||
scriptErrorNumber = errAEEventFailed
|
||||
scriptErrorString = "Terminal surface model is not available."
|
||||
return nil
|
||||
}
|
||||
|
||||
let mods: Ghostty.Input.Mods
|
||||
if let modsString = evaluatedArguments?["modifiers"] as? String {
|
||||
guard let parsed = Ghostty.Input.Mods(scriptModifiers: modsString) else {
|
||||
scriptErrorNumber = errAECoercionFail
|
||||
scriptErrorString = "Unknown modifier in: \(modsString)"
|
||||
return nil
|
||||
}
|
||||
mods = parsed
|
||||
} else {
|
||||
mods = []
|
||||
}
|
||||
|
||||
let mousePosEvent = Ghostty.Input.MousePosEvent(
|
||||
x: x,
|
||||
y: y,
|
||||
mods: mods
|
||||
)
|
||||
surface.sendMousePos(mousePosEvent)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import AppKit
|
||||
|
||||
/// Handler for the `send mouse scroll` AppleScript command defined in `Ghostty.sdef`.
|
||||
///
|
||||
/// Cocoa scripting instantiates this class because the command's `<cocoa>` element
|
||||
/// specifies `class="GhosttyScriptMouseScrollCommand"`. The runtime calls
|
||||
/// `performDefaultImplementation()` to execute the command.
|
||||
@MainActor
|
||||
@objc(GhosttyScriptMouseScrollCommand)
|
||||
final class ScriptMouseScrollCommand: NSScriptCommand {
|
||||
override func performDefaultImplementation() -> Any? {
|
||||
guard let x = evaluatedArguments?["x"] as? Double else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = "Missing x scroll delta."
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let y = evaluatedArguments?["y"] as? Double else {
|
||||
scriptErrorNumber = errAEParamMissed
|
||||
scriptErrorString = "Missing y scroll delta."
|
||||
return nil
|
||||
}
|
||||
|
||||
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 surface = surfaceView.surfaceModel else {
|
||||
scriptErrorNumber = errAEEventFailed
|
||||
scriptErrorString = "Terminal surface model is not available."
|
||||
return nil
|
||||
}
|
||||
|
||||
let precision = evaluatedArguments?["precision"] as? Bool ?? false
|
||||
|
||||
let momentum: Ghostty.Input.Momentum
|
||||
if let momentumCode = evaluatedArguments?["momentum"] as? UInt32 {
|
||||
switch momentumCode {
|
||||
case "SMno".fourCharCode: momentum = .none
|
||||
case "SMbg".fourCharCode: momentum = .began
|
||||
case "SMch".fourCharCode: momentum = .changed
|
||||
case "SMen".fourCharCode: momentum = .ended
|
||||
case "SMcn".fourCharCode: momentum = .cancelled
|
||||
case "SMmb".fourCharCode: momentum = .mayBegin
|
||||
case "SMst".fourCharCode: momentum = .stationary
|
||||
default: momentum = .none
|
||||
}
|
||||
} else {
|
||||
momentum = .none
|
||||
}
|
||||
|
||||
let scrollEvent = Ghostty.Input.MouseScrollEvent(
|
||||
x: x,
|
||||
y: y,
|
||||
mods: .init(precision: precision, momentum: momentum)
|
||||
)
|
||||
surface.sendMouseScroll(scrollEvent)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user