mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
macos: implement basic read-only applescript stuff
This commit is contained in:
@@ -55,8 +55,12 @@
|
||||
</dict>
|
||||
<key>MDItemKeywords</key>
|
||||
<string>Terminal</string>
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<true/>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>OSAScriptingDefinition</key>
|
||||
<string>Ghostty.sdef</string>
|
||||
<key>NSServices</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -4,11 +4,15 @@
|
||||
<dictionary title="Ghostty Scripting Dictionary">
|
||||
<suite name="Ghostty Suite" code="Ghst" description="Ghostty scripting support.">
|
||||
<class name="application" code="capp" description="The Ghostty application.">
|
||||
<element type="terminal" access="r"/>
|
||||
<cocoa class="NSApplication"/>
|
||||
<element type="terminal" access="r">
|
||||
<cocoa key="terminals"/>
|
||||
</element>
|
||||
</class>
|
||||
|
||||
<class name="terminal" code="Gtrm" plural="terminals" description="An individual terminal surface.">
|
||||
<property name="id" code="Gtid" type="text" access="r" description="Stable ID for this terminal surface."/>
|
||||
<cocoa class="GhosttyScriptTerminal"/>
|
||||
<property name="id" code="ID " type="text" access="r" description="Stable ID for this terminal surface."/>
|
||||
<property name="title" code="Gttl" type="text" access="r" description="Current terminal title."/>
|
||||
<property name="working directory" code="Gwdr" type="text" access="r" description="Current working directory for the terminal process."/>
|
||||
</class>
|
||||
@@ -19,4 +23,21 @@
|
||||
<result type="boolean" description="True when the action was performed."/>
|
||||
</command>
|
||||
</suite>
|
||||
|
||||
<!--
|
||||
The Standard Suite definition below is copied from Apple's
|
||||
/System/Library/ScriptingDefinitions/CocoaStandard.sdef, trimmed to only
|
||||
include what we need.
|
||||
-->
|
||||
<suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">
|
||||
<command name="count" code="corecnte" description="Return the number of elements of a particular class within an object.">
|
||||
<cocoa class="NSCountCommand"/>
|
||||
<access-group identifier="*"/>
|
||||
<direct-parameter type="specifier" requires-access="r" description="The objects to be counted."/>
|
||||
<parameter name="each" code="kocl" type="type" optional="yes" description="The class of objects to be counted." hidden="yes">
|
||||
<cocoa key="ObjectClass"/>
|
||||
</parameter>
|
||||
<result type="integer" description="The count."/>
|
||||
</command>
|
||||
</suite>
|
||||
</dictionary>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
||||
819324582F24E78800A9ED8F /* DockTilePlugin.plugin in Copy DockTilePlugin */ = {isa = PBXBuildFile; fileRef = 8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
819324642F24FF2100A9ED8F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
8F3A9B4C2FA6B88000A18D13 /* Ghostty.sdef in Resources */ = {isa = PBXBuildFile; fileRef = 8F3A9B4B2FA6B88000A18D13 /* Ghostty.sdef */; };
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
||||
A51BFC272B30F1B800E92F16 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = A51BFC262B30F1B800E92F16 /* Sparkle */; };
|
||||
A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; };
|
||||
@@ -74,6 +75,7 @@
|
||||
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
|
||||
810ACC9F2E9D3301004F8F92 /* GhosttyUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GhosttyUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8193244D2F24E6C000A9ED8F /* DockTilePlugin.plugin */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DockTilePlugin.plugin; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
8F3A9B4B2FA6B88000A18D13 /* Ghostty.sdef */ = {isa = PBXFileReference; lastKnownFileType = text.sdef; path = Ghostty.sdef; sourceTree = "<group>"; };
|
||||
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
|
||||
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = "<group>"; };
|
||||
A546F1132D7B68D7003B11A0 /* locale */ = {isa = PBXFileReference; lastKnownFileType = folder; name = locale; path = "../zig-out/share/locale"; sourceTree = "<group>"; };
|
||||
@@ -134,6 +136,8 @@
|
||||
"Features/App Intents/KeybindIntent.swift",
|
||||
"Features/App Intents/NewTerminalIntent.swift",
|
||||
"Features/App Intents/QuickTerminalIntent.swift",
|
||||
"Features/AppleScript/AppDelegate+AppleScript.swift",
|
||||
Features/AppleScript/AppleScriptTerminal.swift,
|
||||
Features/ClipboardConfirmation/ClipboardConfirmation.xib,
|
||||
Features/ClipboardConfirmation/ClipboardConfirmationController.swift,
|
||||
Features/ClipboardConfirmation/ClipboardConfirmationView.swift,
|
||||
@@ -322,6 +326,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */,
|
||||
8F3A9B4B2FA6B88000A18D13 /* Ghostty.sdef */,
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */,
|
||||
A553F4122E06EB1600257779 /* Ghostty.icon */,
|
||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */,
|
||||
@@ -557,6 +562,7 @@
|
||||
A553F4142E06EB1600257779 /* Ghostty.icon in Resources */,
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */,
|
||||
A586167C2B7703CC009BDB1D /* fish in Resources */,
|
||||
8F3A9B4C2FA6B88000A18D13 /* Ghostty.sdef in Resources */,
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */,
|
||||
A546F1142D7B68D7003B11A0 /* locale in Resources */,
|
||||
A5985CE62C33060F00C57AD3 /* man in Resources */,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import AppKit
|
||||
|
||||
/// Application-level Cocoa scripting hooks for the Ghostty AppleScript dictionary.
|
||||
///
|
||||
/// Cocoa scripting looks for specifically named Objective-C selectors derived
|
||||
/// from the `sdef` file. This extension implements those required entry points
|
||||
/// on `NSApplication`, which is the object behind the `application` class in
|
||||
/// `Ghostty.sdef`.
|
||||
@MainActor
|
||||
extension NSApplication {
|
||||
/// Backing collection for `application.terminals`.
|
||||
///
|
||||
/// Required selector name: `terminals`.
|
||||
@objc(terminals)
|
||||
var terminals: [ScriptTerminal] {
|
||||
allSurfaceViews.map(ScriptTerminal.init)
|
||||
}
|
||||
|
||||
/// Enables AppleScript unique-ID lookup for terminal references.
|
||||
///
|
||||
/// Required selector name pattern for element `terminals`:
|
||||
/// `valueInTerminalsWithUniqueID:`.
|
||||
///
|
||||
/// This is what lets scripts do stable references like
|
||||
/// `terminal id "..."` even as windows/tabs change.
|
||||
@objc(valueInTerminalsWithUniqueID:)
|
||||
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
|
||||
allSurfaceViews
|
||||
.first(where: { $0.id.uuidString == uniqueID })
|
||||
.map(ScriptTerminal.init)
|
||||
}
|
||||
|
||||
/// Handler for the `perform action` AppleScript command.
|
||||
///
|
||||
/// Required selector name from the command in `sdef`:
|
||||
/// `handlePerformActionScriptCommand:`.
|
||||
///
|
||||
/// Cocoa scripting parses script syntax and provides:
|
||||
/// - `directParameter`: the command string (`perform action "..."`).
|
||||
/// - `evaluatedArguments["on"]`: the target terminal (`... on terminal ...`).
|
||||
///
|
||||
/// We return a Bool to match the command's declared result type.
|
||||
@objc(handlePerformActionScriptCommand:)
|
||||
func handlePerformActionScriptCommand(_ command: NSScriptCommand) -> Any? {
|
||||
guard let action = command.directParameter as? String else {
|
||||
command.scriptErrorNumber = errAEParamMissed
|
||||
command.scriptErrorString = "Missing action string."
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let terminal = command.evaluatedArguments?["on"] as? ScriptTerminal else {
|
||||
command.scriptErrorNumber = errAEParamMissed
|
||||
command.scriptErrorString = "Missing terminal target."
|
||||
return nil
|
||||
}
|
||||
|
||||
return terminal.perform(action: action)
|
||||
}
|
||||
|
||||
/// Discovers all currently alive terminal surfaces across normal and quick
|
||||
/// terminal windows. This powers both terminal enumeration and ID lookup.
|
||||
private var allSurfaceViews: [Ghostty.SurfaceView] {
|
||||
NSApp.windows
|
||||
.compactMap { $0.windowController as? BaseTerminalController }
|
||||
.flatMap { $0.surfaceTree.root?.leaves() ?? [] }
|
||||
}
|
||||
}
|
||||
73
macos/Sources/Features/AppleScript/AppleScriptTerminal.swift
Normal file
73
macos/Sources/Features/AppleScript/AppleScriptTerminal.swift
Normal file
@@ -0,0 +1,73 @@
|
||||
import AppKit
|
||||
|
||||
/// AppleScript-facing wrapper around a live Ghostty terminal surface.
|
||||
///
|
||||
/// This class is intentionally ObjC-visible because Cocoa scripting resolves
|
||||
/// AppleScript objects through Objective-C runtime names/selectors, not Swift
|
||||
/// protocol conformance.
|
||||
///
|
||||
/// Mapping from `Ghostty.sdef`:
|
||||
/// - `class terminal` -> this class (`@objc(GhosttyAppleScriptTerminal)`).
|
||||
/// - `property id` -> `@objc(id)` getter below.
|
||||
/// - `property title` -> `@objc(title)` getter below.
|
||||
/// - `property working directory` -> `@objc(workingDirectory)` getter below.
|
||||
///
|
||||
/// We keep only a weak reference to the underlying `SurfaceView` so this
|
||||
/// wrapper never extends the terminal's lifetime.
|
||||
@MainActor
|
||||
@objc(GhosttyScriptTerminal)
|
||||
final class ScriptTerminal: NSObject {
|
||||
private weak var surfaceView: Ghostty.SurfaceView?
|
||||
|
||||
init(surfaceView: Ghostty.SurfaceView) {
|
||||
self.surfaceView = surfaceView
|
||||
}
|
||||
|
||||
/// Exposed as the AppleScript `id` property.
|
||||
///
|
||||
/// This is a stable UUID string for the life of a surface and is also used
|
||||
/// by `NSUniqueIDSpecifier` to re-identify a terminal object in scripts.
|
||||
@objc(id)
|
||||
var stableID: String {
|
||||
surfaceView?.id.uuidString ?? ""
|
||||
}
|
||||
|
||||
/// Exposed as the AppleScript `title` property.
|
||||
@objc(title)
|
||||
var title: String {
|
||||
surfaceView?.title ?? ""
|
||||
}
|
||||
|
||||
/// Exposed as the AppleScript `working directory` property.
|
||||
///
|
||||
/// The `sdef` uses a spaced name, but Cocoa scripting maps that to the
|
||||
/// camel-cased selector name `workingDirectory`.
|
||||
@objc(workingDirectory)
|
||||
var workingDirectory: String {
|
||||
surfaceView?.pwd ?? ""
|
||||
}
|
||||
|
||||
/// Used by command handling (`perform action ... on <terminal>`).
|
||||
func perform(action: String) -> Bool {
|
||||
guard let surfaceModel = surfaceView?.surfaceModel else { return false }
|
||||
return surfaceModel.perform(action: action)
|
||||
}
|
||||
|
||||
/// Provides Cocoa scripting with a canonical "path" back to this object.
|
||||
///
|
||||
/// Without an object specifier, returned terminal objects can't be reliably
|
||||
/// referenced in follow-up script statements because AppleScript cannot
|
||||
/// express where the object came from (`application.terminals[id]`).
|
||||
override var objectSpecifier: NSScriptObjectSpecifier? {
|
||||
guard let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NSUniqueIDSpecifier(
|
||||
containerClassDescription: appClassDescription,
|
||||
containerSpecifier: nil,
|
||||
key: "terminals",
|
||||
uniqueID: stableID
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user