AppleScript (#11208)

This adds AppleScript support to the macOS app.

AppleScript is still one of the best ways to script macOS apps. It is
more CLI friendly and share-able than Apple Shortcuts and can be used by
other CLI programs like editors (Neovim plugins), launchers
(Raycast/Alfred), etc. It has been heavily requested to introduce more
scriptability into Ghostty and this is a really good, powerful option on
macOS.

> [!NOTE]
>
> I definitely still want to do something cross-platform and more
official as a plugin/scripting API for Ghostty. But native integrations
like this are a goal of Ghostty as well and this implementation is just
some thin logic over already existing internals to expose it.

I plan on merging this ahead of 1.3. Normally I wouldn't ship a feature
so late in the game but this is fairly hermetic (doesn't impact other
systems) and I plan on documenting it as a "preview" feature since the
API and stability are in question.

## Security

Apple secures AppleScript via TCC by asking for permission when a script
is run whether an app is allowed to be controlled. Because this is
always asked, we do default AppleScript to being enabled. This is
typical of macOS native applications already.

AppleScript can be wholesale disabled via `macos-applescript = false`.

## Future

There is a big question of what else to expose to this to make it
useful. I'm going to make a call to action for the 1.3 cycle to gather
feedback on this, since we can expose mostly anything!

## Capabilities

### Objects

| Object | Key Properties | Key Elements |
| --- | --- | --- |
| `application` | `name`, `frontmost`, `version` | `windows`,
`terminals` |
| `window` | `id`, `name`, `selected tab` | `tabs`, `terminals` |
| `tab` | `id`, `name`, `index`, `selected` | `terminals` |
| `terminal` | `id`, `name`, `working directory` | None |

### Commands

| Category | Command | Purpose |
| --- | --- | --- |
| Application | `perform action` | Execute a Ghostty action string on a
terminal. |
| Configuration | `new surface configuration` | Create/copy a reusable
surface configuration record. |
| Creation | `new window` | Open a new Ghostty window (optional
configuration). |
| Creation | `new tab` | Open a new tab (optional target
window/configuration). |
| Layout | `split` | Split a terminal and return the new terminal. |
| Focus/Selection | `focus` | Focus a terminal. |
| Focus/Selection | `activate window` | Bring a window to front and
activate app. |
| Focus/Selection | `select tab` | Select and foreground a tab. |
| Lifecycle | `close` | Close a terminal. |
| Lifecycle | `close tab` | Close a tab. |
| Lifecycle | `close window` | Close a window. |
| Input | `input text` | Paste-style text input into terminal. |
| Input | `send key` | Send key press/release with optional modifiers. |
| Input | `send mouse button` | Send mouse button press/release. |
| Input | `send mouse position` | Send mouse position update. |
| Input | `send mouse scroll` | Send scroll event with
precision/momentum options. |
| Standard Suite | `count`, `exists`, `quit` | Standard Cocoa scripting
functionality. |

## Examples

### Layout

```AppleScript
-- Tmux-like layout: 4 panes in one tab (2x2), each with a job.
set projectDir to POSIX path of (path to home folder) & "src/ghostty"

tell application "Ghostty"
    activate

    -- Reusable config for all panes.
    set cfg to new surface configuration
    set initial working directory of cfg to projectDir

    -- Create the first window/tab + split into 4 panes.
    set win to new window with configuration cfg
    set paneEditor to terminal 1 of selected tab of win
    set paneBuild to split paneEditor direction right with configuration cfg
    set paneGit to split paneEditor direction down with configuration cfg
    set paneLogs to split paneBuild direction down with configuration cfg

    -- Seed each pane with a command.
    input text "nvim ." to paneEditor
    send key "enter" to paneEditor

    input text "zig build -Demit-macos-app=false" to paneBuild

    input text "git status -sb" to paneGit

    input text "tail -f /tmp/dev.log" to paneLogs
    send key "enter" to paneLogs

    -- Put focus back where you want to type.
    focus paneEditor
end tell
```

### Broadcast Commands

```AppleScript
-- Run one command across every open terminal surface.
set cmd to "echo sync && date"

tell application "Ghostty"
    set allTerms to terminals

    repeat with t in allTerms
        input text cmd to t
        send key "enter" to t
    end repeat

    display dialog ("Broadcasted to " & (count of allTerms) & " terminal(s).")
end tell
```

### Jump by Working Directory

```applescript
-- Find the first terminal whose cwd contains this text.
set needle to "ghostty"

tell application "Ghostty"
    set matches to every terminal whose working directory contains needle

    -- Fallback: try title if cwd had no match.
    if (count of matches) = 0 then
        set matches to every terminal whose name contains needle
    end if

    if (count of matches) = 0 then
        display dialog ("No terminal matched: " & needle)
    else
        set t to item 1 of matches
        focus terminal t
        input text "echo '[focused by AppleScript]'" to t
        send key "enter" to t
    end if
end tell
```
This commit is contained in:
Mitchell Hashimoto
2026-03-07 07:52:56 -08:00
committed by GitHub
21 changed files with 1912 additions and 5 deletions

View File

@@ -1,11 +1,34 @@
# macOS Ghostty Application
- Use `swiftlint` for formatting and linting Swift code.
- If code outside of this directory is modified, use
- If code outside of `macos/` directory is modified, use
`zig build -Demit-macos-app=false` before building the macOS app to update
the underlying Ghostty library.
- Use `build.nu` to build the macOS app, do not use `zig build`
- Use `macos/build.nu` to build the macOS app, do not use `zig build`
(except to build the underlying library as mentioned above).
- Build: `build.nu [--scheme Ghostty] [--configuration Debug] [--action build]`
- Output: `build/<configuration>/Ghostty.app` (e.g. `build/Debug/Ghostty.app`)
- Run unit tests directly with `build.nu --action test`
- Build: `macos/build.nu [--scheme Ghostty] [--configuration Debug] [--action build]`
- Output: `macos/build/<configuration>/Ghostty.app` (e.g. `macos/build/Debug/Ghostty.app`)
- Run unit tests directly with `macos/build.nu --action test`
## AppleScript
- The AppleScript scripting definition is in `macos/Ghostty.sdef`.
- Guard AppleScript entry points and object accessors with the
`macos-applescript` configuration (use `NSApp.isAppleScriptEnabled`
and `NSApp.validateScript(command:)` where applicable).
- In `macos/Ghostty.sdef`, keep top-level definitions in this order:
1. Classes
2. Records
3. Enums
4. Commands
- Test AppleScript support:
(1) Build with `macos/build.nu`
(2) Launch and activate the app via osascript using the absolute path
to the built app bundle:
`osascript -e 'tell application "<absolute path to build/Debug/Ghostty.app>" to activate'`
(3) Wait a few seconds for the app to fully launch and open a terminal.
(4) Run test scripts with `osascript`, always targeting the app by
its absolute path (not by name) to avoid calling the wrong
application.
(5) When done, quit via:
`osascript -e 'tell application "<absolute path to build/Debug/Ghostty.app>" to quit'`

View File

@@ -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>

312
macos/Ghostty.sdef Normal file
View File

@@ -0,0 +1,312 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="Ghostty Scripting Dictionary">
<suite name="Ghostty Suite" code="Ghst" description="Ghostty scripting support.">
<class name="application" code="capp" description="The Ghostty application.">
<cocoa class="NSApplication"/>
<property name="name" code="pnam" type="text" access="r" description="The name of the application."/>
<property name="frontmost" code="pisf" type="boolean" access="r" description="Is this the active application?">
<cocoa key="isActive"/>
</property>
<property name="version" code="vers" type="text" access="r" description="The version number of the application."/>
<responds-to command="perform action">
<cocoa method="handlePerformActionScriptCommand:"/>
</responds-to>
<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>
<responds-to command="quit">
<cocoa method="handleQuitScriptCommand:"/>
</responds-to>
<element type="window" access="r">
<cocoa key="scriptWindows"/>
</element>
<element type="terminal" access="r">
<cocoa key="terminals"/>
</element>
</class>
<class name="window" code="Gwnd" plural="windows" description="A Ghostty window containing one or more tabs.">
<cocoa class="GhosttyScriptWindow"/>
<property name="id" code="ID " type="text" access="r" description="Stable ID for this window."/>
<property name="name" code="pnam" type="text" access="r" description="The title of the window.">
<cocoa key="title"/>
</property>
<property name="selected tab" code="GWsT" type="tab" access="r" description="The selected tab in this window.">
<cocoa key="selectedTab"/>
</property>
<responds-to command="activate window">
<cocoa method="handleActivateWindowCommand:"/>
</responds-to>
<responds-to command="close window">
<cocoa method="handleCloseWindowCommand:"/>
</responds-to>
<element type="tab" access="r">
<cocoa key="tabs"/>
</element>
<element type="terminal" access="r">
<cocoa key="terminals"/>
</element>
</class>
<class name="tab" code="Gtab" plural="tabs" description="A tab within a Ghostty window.">
<cocoa class="GhosttyScriptTab"/>
<property name="id" code="ID " type="text" access="r" description="Stable ID for this tab."/>
<property name="name" code="pnam" type="text" access="r" description="The title of the tab.">
<cocoa key="title"/>
</property>
<property name="index" code="pidx" type="integer" access="r" description="1-based index of this tab in its window."/>
<property name="selected" code="GTsl" type="boolean" access="r" description="Whether this tab is selected in its window."/>
<responds-to command="select tab">
<cocoa method="handleSelectTabCommand:"/>
</responds-to>
<responds-to command="close tab">
<cocoa method="handleCloseTabCommand:"/>
</responds-to>
<element type="terminal" access="r">
<cocoa key="terminals"/>
</element>
</class>
<class name="terminal" code="Gtrm" plural="terminals" description="An individual terminal surface.">
<cocoa class="GhosttyScriptTerminal"/>
<property name="id" code="ID " type="text" access="r" description="Stable ID for this terminal surface."/>
<property name="name" code="pnam" type="text" access="r" description="Current terminal title.">
<cocoa key="title"/>
</property>
<property name="working directory" code="Gwdr" type="text" access="r" description="Current working directory for the terminal process."/>
<responds-to command="split">
<cocoa method="handleSplitCommand:"/>
</responds-to>
<responds-to command="focus">
<cocoa method="handleFocusCommand:"/>
</responds-to>
<responds-to command="close">
<cocoa method="handleCloseCommand:"/>
</responds-to>
</class>
<record-type name="surface configuration" code="GScf" description="Reusable settings applied when creating a terminal surface.">
<property name="font size" code="GScF" type="real" description="Font size in points.">
<cocoa key="fontSize"/>
</property>
<property name="initial working directory" code="GScD" type="text" description="Initial working directory for the terminal process.">
<cocoa key="workingDirectory"/>
</property>
<property name="command" code="GScC" type="text" description="Command to execute instead of the configured shell.">
<cocoa key="command"/>
</property>
<property name="initial input" code="GScI" type="text" description="Input sent to the terminal after launch.">
<cocoa key="initialInput"/>
</property>
<property name="wait after command" code="GScW" type="boolean" description="Keep the terminal open after command exit.">
<cocoa key="waitAfterCommand"/>
</property>
<property name="environment variables" code="GScE" description="Environment variables in KEY=VALUE format.">
<type type="text" list="yes"/>
<cocoa key="environmentVariables"/>
</property>
</record-type>
<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>
<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>
<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>
<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="perform action" code="GhstPfAc" description="Perform a Ghostty action string on a terminal.">
<direct-parameter type="text" description="The Ghostty action string."/>
<parameter name="on" code="GonT" type="terminal" description="Target terminal."/>
<result type="boolean" description="True when the action was performed."/>
</command>
<command name="new surface configuration" code="GhstNSCf" description="Create a reusable surface configuration object.">
<parameter name="from" code="GScS" type="surface configuration" optional="yes" description="Surface configuration to copy.">
<cocoa key="configuration"/>
</parameter>
<result type="surface configuration" description="The newly created surface configuration."/>
</command>
<command name="new window" code="GhstNWin" description="Create a new Ghostty window.">
<parameter name="with configuration" code="GNwS" type="surface configuration" optional="yes" description="Base surface configuration for the initial terminal.">
<cocoa key="configuration"/>
</parameter>
<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.">
<direct-parameter type="specifier" description="The terminal to split."/>
<parameter name="direction" code="GSpd" type="split direction" description="The direction to split.">
<cocoa key="direction"/>
</parameter>
<parameter name="with configuration" code="GSpS" type="surface configuration" optional="yes" description="Base surface configuration for the new split terminal.">
<cocoa key="configuration"/>
</parameter>
<result type="terminal" description="The newly created terminal."/>
</command>
<command name="focus" code="GhstFcus" description="Focus a terminal, bringing its window to the front.">
<direct-parameter type="specifier" description="The terminal to focus."/>
</command>
<command name="close" code="GhstClos" description="Close a terminal.">
<direct-parameter type="specifier" description="The terminal to close."/>
</command>
<command name="activate window" code="GhstAcWn" description="Activate a Ghostty window, bringing it to the front.">
<direct-parameter type="specifier" description="The window to activate."/>
</command>
<command name="select tab" code="GhstSlTb" description="Select a tab in its window.">
<direct-parameter type="specifier" description="The tab to select."/>
</command>
<command name="close tab" code="GhstClTb" description="Close a tab.">
<direct-parameter type="specifier" description="The tab to close."/>
</command>
<command name="close window" code="GhstClWn" description="Close a window.">
<direct-parameter type="specifier" description="The window to close."/>
</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>
<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. &quot;enter&quot;, &quot;a&quot;, &quot;space&quot;)."/>
<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>
<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>
<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>
<!--
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>
<command name="exists" code="coredoex" description="Verify that an object exists.">
<cocoa class="NSExistsCommand"/>
<access-group identifier="*"/>
<direct-parameter type="any" requires-access="r" description="The object(s) to check."/>
<result type="boolean" description="Did the object(s) exist?"/>
</command>
<command name="quit" code="aevtquit" description="Quit the application.">
<cocoa class="NSQuitCommand"/>
</command>
</suite>
</dictionary>

View File

@@ -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,18 @@
"Features/App Intents/KeybindIntent.swift",
"Features/App Intents/NewTerminalIntent.swift",
"Features/App Intents/QuickTerminalIntent.swift",
"Features/AppleScript/AppDelegate+AppleScript.swift",
"Features/AppleScript/Ghostty.Input.Mods+AppleScript.swift",
Features/AppleScript/ScriptInputTextCommand.swift,
Features/AppleScript/ScriptKeyEventCommand.swift,
Features/AppleScript/ScriptMouseButtonCommand.swift,
Features/AppleScript/ScriptMousePosCommand.swift,
Features/AppleScript/ScriptMouseScrollCommand.swift,
Features/AppleScript/ScriptRecord.swift,
Features/AppleScript/ScriptSurfaceConfiguration.swift,
Features/AppleScript/ScriptTab.swift,
Features/AppleScript/ScriptTerminal.swift,
Features/AppleScript/ScriptWindow.swift,
Features/ClipboardConfirmation/ClipboardConfirmation.xib,
Features/ClipboardConfirmation/ClipboardConfirmationController.swift,
Features/ClipboardConfirmation/ClipboardConfirmationView.swift,
@@ -322,6 +336,7 @@
isa = PBXGroup;
children = (
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */,
8F3A9B4B2FA6B88000A18D13 /* Ghostty.sdef */,
A5B30538299BEAAB0047F10C /* Assets.xcassets */,
A553F4122E06EB1600257779 /* Ghostty.icon */,
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */,
@@ -557,6 +572,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 */,

View File

@@ -0,0 +1,341 @@
import AppKit
// Application-level Cocoa scripting hooks for the Ghostty AppleScript dictionary.
//
// Cocoa scripting is mostly convention-based: we do not register handlers in
// code, we expose Objective-C selectors with names Cocoa derives from
// `Ghostty.sdef`.
//
// In practical terms:
// - An `<element>` in `sdef` maps to an ObjC collection accessor.
// - Unique-ID element lookup maps to `valueIn...WithUniqueID:`.
// - Some `<command>` declarations map to `handle...ScriptCommand:`.
//
// This file implements the selectors Cocoa expects on `NSApplication`, which is
// the runtime object behind the `application` class in `Ghostty.sdef`.
// MARK: - Windows
@MainActor
extension NSApplication {
/// Backing collection for `application.windows`.
///
/// We expose one scripting window per native tab group so scripts see the
/// expected window/tab hierarchy instead of one AppKit window per tab.
///
/// Required selector name from the `sdef` element key: `scriptWindows`.
///
/// Cocoa scripting calls this whenever AppleScript evaluates a window list,
/// such as `windows`, `window 1`, or `every window whose ...`.
@objc(scriptWindows)
var scriptWindows: [ScriptWindow] {
guard isAppleScriptEnabled else { return [] }
// AppKit exposes one NSWindow per tab. AppleScript users expect one
// top-level window object containing multiple tabs, so we dedupe tab
// siblings into a single ScriptWindow.
var seen: Set<ObjectIdentifier> = []
var result: [ScriptWindow] = []
for controller in orderedTerminalControllers {
// Collapse each controller to one canonical representative for the
// whole tab group. Standalone windows map to themselves.
guard let primary = primaryTerminalController(for: controller) else {
continue
}
let primaryControllerID = ObjectIdentifier(primary)
guard seen.insert(primaryControllerID).inserted else {
// Another tab from this group already created the scripting
// window object.
continue
}
result.append(ScriptWindow(primaryController: primary))
}
return result
}
/// Enables AppleScript unique-ID lookup for window references.
///
/// Required selector name pattern for element key `scriptWindows`:
/// `valueInScriptWindowsWithUniqueID:`.
///
/// Cocoa calls this when a script resolves `window id "..."`.
/// Returning `nil` makes the object specifier fail naturally.
@objc(valueInScriptWindowsWithUniqueID:)
func valueInScriptWindows(uniqueID: String) -> ScriptWindow? {
guard isAppleScriptEnabled else { return nil }
return scriptWindows.first(where: { $0.stableID == uniqueID })
}
}
// MARK: - Terminals
@MainActor
extension NSApplication {
/// Backing collection for `application.terminals`.
///
/// Required selector name: `terminals`.
@objc(terminals)
var terminals: [ScriptTerminal] {
guard isAppleScriptEnabled else { return [] }
return 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? {
guard isAppleScriptEnabled else { return nil }
return allSurfaceViews
.first(where: { $0.id.uuidString == uniqueID })
.map(ScriptTerminal.init)
}
}
// MARK: - Commands
@MainActor
extension NSApplication {
/// 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) -> NSNumber? {
guard validateScript(command: command) else { return nil }
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 NSNumber(value: terminal.perform(action: action))
}
/// Handler for creating a reusable AppleScript surface configuration object.
@objc(handleNewSurfaceConfigurationScriptCommand:)
func handleNewSurfaceConfigurationScriptCommand(_ command: NSScriptCommand) -> NSDictionary? {
guard validateScript(command: command) else { return nil }
do {
let configuration = try Ghostty.SurfaceConfiguration(
scriptRecord: command.evaluatedArguments?["configuration"] as? NSDictionary
)
return configuration.dictionaryRepresentation
} catch {
command.scriptErrorNumber = errAECoercionFail
command.scriptErrorString = error.localizedDescription
return nil
}
}
/// Handler for the `new window` AppleScript command.
///
/// Required selector name from the command in `sdef`:
/// `handleNewWindowScriptCommand:`.
///
/// Accepts an optional reusable surface configuration object.
///
/// Returns the newly created scripting window object.
@objc(handleNewWindowScriptCommand:)
func handleNewWindowScriptCommand(_ command: NSScriptCommand) -> ScriptWindow? {
guard validateScript(command: command) else { return nil }
guard let appDelegate = delegate as? AppDelegate else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Ghostty app delegate is unavailable."
return nil
}
let baseConfig: Ghostty.SurfaceConfiguration?
if let scriptRecord = command.evaluatedArguments?["configuration"] as? NSDictionary {
do {
baseConfig = try Ghostty.SurfaceConfiguration(scriptRecord: scriptRecord)
} catch {
command.scriptErrorNumber = errAECoercionFail
command.scriptErrorString = error.localizedDescription
return nil
}
} else {
baseConfig = nil
}
let controller = TerminalController.newWindow(
appDelegate.ghostty,
withBaseConfig: baseConfig
)
let createdWindowID = ScriptWindow.stableID(primaryController: controller)
if let scriptWindow = scriptWindows.first(where: { $0.stableID == createdWindowID }) {
return scriptWindow
}
// Fall back to wrapping the created controller if AppKit window ordering
// has not refreshed yet in the current run loop.
return ScriptWindow(primaryController: controller)
}
/// Handler for the `quit` AppleScript command.
///
/// Required selector name from the command in `sdef`:
/// `handleQuitScriptCommand:`.
@objc(handleQuitScriptCommand:)
func handleQuitScriptCommand(_ command: NSScriptCommand) {
guard validateScript(command: command) else { return }
terminate(nil)
}
/// 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) -> ScriptTab? {
guard validateScript(command: command) else { return nil }
guard let appDelegate = delegate as? AppDelegate else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Ghostty app delegate is unavailable."
return nil
}
let baseConfig: Ghostty.SurfaceConfiguration?
if let scriptRecord = command.evaluatedArguments?["configuration"] as? NSDictionary {
do {
baseConfig = try Ghostty.SurfaceConfiguration(scriptRecord: scriptRecord)
} catch {
command.scriptErrorNumber = errAECoercionFail
command.scriptErrorString = error.localizedDescription
return nil
}
} else {
baseConfig = 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
@MainActor
extension NSApplication {
/// Whether Ghostty should currently accept AppleScript interactions.
var isAppleScriptEnabled: Bool {
guard let appDelegate = delegate as? AppDelegate else { return true }
return appDelegate.ghostty.config.macosAppleScript
}
/// Applies a consistent error when scripting is disabled by configuration.
@discardableResult
func validateScript(command: NSScriptCommand) -> Bool {
guard isAppleScriptEnabled else {
command.scriptErrorNumber = errAEEventNotPermitted
command.scriptErrorString = "AppleScript is disabled by the macos-applescript configuration."
return false
}
return true
}
/// Discovers all currently alive terminal surfaces across normal and quick
/// terminal windows. This powers both terminal enumeration and ID lookup.
fileprivate var allSurfaceViews: [Ghostty.SurfaceView] {
allTerminalControllers
.flatMap { $0.surfaceTree.root?.leaves() ?? [] }
}
/// All terminal controllers in undefined order.
fileprivate var allTerminalControllers: [BaseTerminalController] {
NSApp.windows.compactMap { $0.windowController as? BaseTerminalController }
}
/// All terminal controllers in front-to-back order.
fileprivate var orderedTerminalControllers: [BaseTerminalController] {
NSApp.orderedWindows.compactMap { $0.windowController as? BaseTerminalController }
}
/// Identifies the primary tab controller for a window's tab group.
///
/// This gives us one stable representative for all tabs in the same native
/// AppKit tab group.
///
/// For standalone windows this returns the window's controller directly.
/// For tabbed windows, "primary" is currently the first controller in the
/// tab group's ordered windows list.
fileprivate func primaryTerminalController(for controller: BaseTerminalController) -> BaseTerminalController? {
guard let window = controller.window else { return nil }
guard let tabGroup = window.tabGroup else { return controller }
return tabGroup.windows
.compactMap { $0.windowController as? BaseTerminalController }
.first
}
}

View File

@@ -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
}
}
}
}

View File

@@ -0,0 +1,41 @@
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 NSApp.validateScript(command: self) else { return nil }
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
}
}

View File

@@ -0,0 +1,76 @@
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 NSApp.validateScript(command: self) else { return nil }
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
}
}

View File

@@ -0,0 +1,95 @@
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 NSApp.validateScript(command: self) else { return nil }
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
}
}
}

View File

@@ -0,0 +1,65 @@
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 NSApp.validateScript(command: self) else { return nil }
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
}
}

View File

@@ -0,0 +1,71 @@
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 NSApp.validateScript(command: self) else { return nil }
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
}
}

View File

@@ -0,0 +1,29 @@
import Cocoa
/// Protocol to more easily implement AppleScript records in Swift.
protocol ScriptRecord {
/// Initialize a default record.
init()
/// Initialize a record from the raw value from AppleScript.
init(scriptRecord: NSDictionary?) throws
/// Encode into the dictionary form for AppleScript.
var dictionaryRepresentation: NSDictionary { get }
}
/// An error that can be thrown by `ScriptRecord.init(scriptRecord:)`. Any localized error
/// can be thrown but this is a common one.
enum RecordParseError: LocalizedError {
case invalidType(parameter: String, expected: String)
case invalidValue(parameter: String, message: String)
var errorDescription: String? {
switch self {
case .invalidType(let parameter, let expected):
return "\(parameter) must be \(expected)."
case .invalidValue(let parameter, let message):
return "\(parameter) \(message)."
}
}
}

View File

@@ -0,0 +1,140 @@
import Foundation
/// AppleScript record support for `Ghostty.SurfaceConfiguration`.
///
/// This keeps scripting conversion at the data-structure boundary so AppleScript
/// can pass records by value (`new surface configuration`, assign, copy, mutate)
/// without introducing an additional wrapper type.
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")
}
if let rawFontSize = raw["fontSize"] {
guard let number = rawFontSize as? NSNumber else {
throw RecordParseError.invalidType(parameter: "font size", expected: "a number")
}
let value = number.doubleValue
guard value.isFinite else {
throw RecordParseError.invalidValue(parameter: "font size", message: "must be a finite number")
}
if value < 0 {
throw RecordParseError.invalidValue(parameter: "font size", message: "must be a positive number")
}
if value > 0 {
fontSize = Float32(value)
}
}
if let rawWorkingDirectory = raw["workingDirectory"] {
guard let workingDirectory = rawWorkingDirectory as? String else {
throw RecordParseError.invalidType(parameter: "initial working directory", expected: "text")
}
if !workingDirectory.isEmpty {
self.workingDirectory = workingDirectory
}
}
if let rawCommand = raw["command"] {
guard let command = rawCommand as? String else {
throw RecordParseError.invalidType(parameter: "command", expected: "text")
}
if !command.isEmpty {
self.command = command
}
}
if let rawInitialInput = raw["initialInput"] {
guard let initialInput = rawInitialInput as? String else {
throw RecordParseError.invalidType(parameter: "initial input", expected: "text")
}
if !initialInput.isEmpty {
self.initialInput = initialInput
}
}
if let rawWaitAfterCommand = raw["waitAfterCommand"] {
if let boolValue = rawWaitAfterCommand as? Bool {
waitAfterCommand = boolValue
} else if let numericValue = rawWaitAfterCommand as? NSNumber {
waitAfterCommand = numericValue.boolValue
} else {
throw RecordParseError.invalidType(parameter: "wait after command", expected: "boolean")
}
}
if let assignments = raw["environmentVariables"] as? [String], !assignments.isEmpty {
environmentVariables = try Self.parseScriptEnvironmentAssignments(assignments)
}
}
var dictionaryRepresentation: NSDictionary {
var record: [String: Any] = [
"fontSize": 0,
"workingDirectory": "",
"command": "",
"initialInput": "",
"waitAfterCommand": false,
"environmentVariables": [String](),
]
if let fontSize {
record["fontSize"] = NSNumber(value: fontSize)
}
if let workingDirectory {
record["workingDirectory"] = workingDirectory
}
if let command {
record["command"] = command
}
if let initialInput {
record["initialInput"] = initialInput
}
if waitAfterCommand {
record["waitAfterCommand"] = true
}
if !environmentVariables.isEmpty {
record["environmentVariables"] = environmentVariables.map { "\($0.key)=\($0.value)" }
}
return record as NSDictionary
}
private static func parseScriptEnvironmentAssignments(_ assignments: [String]) throws -> [String: String] {
var result: [String: String] = [:]
for assignment in assignments {
guard let separator = assignment.firstIndex(of: "=") else {
throw RecordParseError.invalidValue(
parameter: "environment variables",
message: "expected KEY=VALUE, got \"\(assignment)\""
)
}
let key = String(assignment[..<separator])
let valueStart = assignment.index(after: separator)
let value = String(assignment[valueStart...])
result[key] = value
}
return result
}
}

View File

@@ -0,0 +1,173 @@
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)"
}
}

View File

@@ -0,0 +1,206 @@
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 {
/// 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
}
/// 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 {
guard NSApp.isAppleScriptEnabled else { return "" }
return surfaceView?.id.uuidString ?? ""
}
/// Exposed as the AppleScript `title` property.
@objc(title)
var title: String {
guard NSApp.isAppleScriptEnabled else { return "" }
return 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 {
guard NSApp.isAppleScriptEnabled else { return "" }
return surfaceView?.pwd ?? ""
}
/// Used by command handling (`perform action ... on <terminal>`).
func perform(action: String) -> Bool {
guard NSApp.isAppleScriptEnabled else { return false }
guard let surfaceModel = surfaceView?.surfaceModel else { return false }
return surfaceModel.perform(action: action)
}
/// Handler for `split <terminal> direction <dir>`.
@objc(handleSplitCommand:)
func handleSplit(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard let surfaceView else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Terminal surface is no longer available."
return nil
}
guard let directionCode = command.evaluatedArguments?["direction"] as? UInt32 else {
command.scriptErrorNumber = errAEParamMissed
command.scriptErrorString = "Missing or unknown split direction."
return nil
}
guard let direction = ScriptSplitDirection(code: directionCode)?.splitDirection else {
command.scriptErrorNumber = errAEParamMissed
command.scriptErrorString = "Missing or unknown split direction."
return nil
}
let baseConfig: Ghostty.SurfaceConfiguration?
if let scriptRecord = command.evaluatedArguments?["configuration"] as? NSDictionary {
do {
baseConfig = try Ghostty.SurfaceConfiguration(scriptRecord: scriptRecord)
} catch {
command.scriptErrorNumber = errAECoercionFail
command.scriptErrorString = error.localizedDescription
return nil
}
} else {
baseConfig = nil
}
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Terminal is not in a splittable window."
return nil
}
guard let newView = controller.newSplit(
at: surfaceView,
direction: direction,
baseConfig: baseConfig
) else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Failed to create split."
return nil
}
return ScriptTerminal(surfaceView: newView)
}
/// Handler for `focus <terminal>`.
@objc(handleFocusCommand:)
func handleFocus(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard let surfaceView else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Terminal surface is no longer available."
return nil
}
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Terminal is not in a window."
return nil
}
controller.focusSurface(surfaceView)
return nil
}
/// Handler for `close <terminal>`.
@objc(handleCloseCommand:)
func handleClose(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard let surfaceView else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Terminal surface is no longer available."
return nil
}
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Terminal is not in a window."
return nil
}
controller.closeSurface(surfaceView, withConfirmation: false)
return nil
}
/// 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 NSApp.isAppleScriptEnabled else { return nil }
guard let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else {
return nil
}
return NSUniqueIDSpecifier(
containerClassDescription: appClassDescription,
containerSpecifier: nil,
key: "terminals",
uniqueID: stableID
)
}
}
/// Converts four-character codes from the `split direction` enumeration in `Ghostty.sdef`
/// to `SplitTree.NewDirection` values.
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

@@ -0,0 +1,260 @@
import AppKit
/// AppleScript-facing wrapper around a logical Ghostty window.
///
/// In AppKit, each tab is often its own `NSWindow`. AppleScript users, however,
/// expect a single window object containing a list of tabs.
///
/// `ScriptWindow` is that compatibility layer:
/// - It presents one object per tab group.
/// - It translates tab-group state into `tabs` and `selected tab`.
/// - It exposes stable IDs that Cocoa scripting can resolve later.
@MainActor
@objc(GhosttyScriptWindow)
final class ScriptWindow: NSObject {
/// Stable identifier used by AppleScript `window id "..."` references.
///
/// We precompute this once so the object keeps a consistent ID for its whole
/// lifetime, even if AppKit window bookkeeping changes after creation.
let stableID: String
/// Canonical representative for this scripting window's tab group.
///
/// We intentionally keep only one controller reference; full tab membership
/// is derived lazily from current AppKit state whenever needed.
private weak var primaryController: BaseTerminalController?
/// `scriptWindows` in `AppDelegate+AppleScript` constructs these objects.
///
/// `stableID` must match the same identity scheme used by
/// `valueInScriptWindowsWithUniqueID:` so Cocoa can re-resolve object
/// specifiers produced earlier in a script.
init(primaryController: BaseTerminalController) {
self.stableID = Self.stableID(primaryController: primaryController)
self.primaryController = primaryController
}
/// Exposed as the AppleScript `id` property.
///
/// This is what scripts read with `id of window ...`.
@objc(id)
var idValue: String {
guard NSApp.isAppleScriptEnabled else { return "" }
return stableID
}
/// Exposed as the AppleScript `title` property.
///
/// Returns the title of the window (from the selected/primary controller's NSWindow).
@objc(title)
var title: String {
guard NSApp.isAppleScriptEnabled else { return "" }
return selectedController?.window?.title ?? ""
}
/// Exposed as the AppleScript `tabs` element.
///
/// Cocoa asks for this collection when a script evaluates `tabs of window ...`
/// or any tab-filter expression. We build wrappers from live controller state
/// so tab additions/removals are reflected immediately.
@objc(tabs)
var tabs: [ScriptTab] {
guard NSApp.isAppleScriptEnabled else { return [] }
return controllers.map { ScriptTab(window: self, controller: $0) }
}
/// Exposed as the AppleScript `selected tab` property.
///
/// This powers expressions like `selected tab of window 1`.
@objc(selectedTab)
var selectedTab: ScriptTab? {
guard NSApp.isAppleScriptEnabled else { return nil }
guard let selectedController else { return nil }
return ScriptTab(window: self, controller: selectedController)
}
/// Enables unique-ID lookup for `tabs` references.
///
/// Required selector pattern for the `tabs` element key:
/// `valueInTabsWithUniqueID:`.
///
/// Cocoa uses this when a script resolves `tab id "..." of window ...`.
@objc(valueInTabsWithUniqueID:)
func valueInTabs(uniqueID: String) -> ScriptTab? {
guard NSApp.isAppleScriptEnabled else { return nil }
guard let controller = controller(tabID: uniqueID) else { return nil }
return ScriptTab(window: self, controller: controller)
}
/// Exposed as the AppleScript `terminals` element on a window.
///
/// Returns all terminal surfaces across every tab in this window.
@objc(terminals)
var terminals: [ScriptTerminal] {
guard NSApp.isAppleScriptEnabled else { return [] }
return controllers
.flatMap { $0.surfaceTree.root?.leaves() ?? [] }
.map(ScriptTerminal.init)
}
/// Enables unique-ID lookup for `terminals` references on a window.
@objc(valueInTerminalsWithUniqueID:)
func valueInTerminals(uniqueID: String) -> ScriptTerminal? {
guard NSApp.isAppleScriptEnabled else { return nil }
return controllers
.flatMap { $0.surfaceTree.root?.leaves() ?? [] }
.first(where: { $0.id.uuidString == uniqueID })
.map(ScriptTerminal.init)
}
/// AppleScript tab indexes are 1-based, so we add one to Swift's 0-based
/// array index.
func tabIndex(for controller: BaseTerminalController) -> Int? {
guard NSApp.isAppleScriptEnabled else { return nil }
return controllers.firstIndex(where: { $0 === controller }).map { $0 + 1 }
}
/// Reports whether a given controller maps to this window's selected tab.
func tabIsSelected(_ controller: BaseTerminalController) -> Bool {
guard NSApp.isAppleScriptEnabled else { return false }
return selectedController === controller
}
/// Best-effort native window to use as a tab parent for AppleScript commands.
var preferredParentWindow: NSWindow? {
guard NSApp.isAppleScriptEnabled else { return nil }
return selectedController?.window ?? controllers.first?.window
}
/// Best-effort controller to use for window-scoped AppleScript commands.
var preferredController: BaseTerminalController? {
guard NSApp.isAppleScriptEnabled else { return nil }
return selectedController ?? controllers.first
}
/// 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 })
}
/// Live controller list for this scripting window.
///
/// We recalculate on every access so AppleScript immediately sees tab-group
/// changes (new tabs, closed tabs, tab moves) without rebuilding all objects.
private var controllers: [BaseTerminalController] {
guard NSApp.isAppleScriptEnabled else { return [] }
guard let primaryController else { return [] }
guard let window = primaryController.window else { return [primaryController] }
if let tabGroup = window.tabGroup {
let groupControllers = tabGroup.windows.compactMap {
$0.windowController as? BaseTerminalController
}
if !groupControllers.isEmpty {
return groupControllers
}
}
return [primaryController]
}
/// Live selected controller for this scripting window.
///
/// AppKit tracks selected tab on `NSWindowTabGroup.selectedWindow`; for
/// non-tabbed windows we fall back to the primary controller.
private var selectedController: BaseTerminalController? {
guard let primaryController else { return nil }
guard let window = primaryController.window else { return primaryController }
if let tabGroup = window.tabGroup,
let selectedController = tabGroup.selectedWindow?.windowController as? BaseTerminalController {
return selectedController
}
return controllers.first
}
/// Handler for `activate window <window>`.
@objc(handleActivateWindowCommand:)
func handleActivateWindow(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
guard let windowContainer = preferredParentWindow else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Window is no longer available."
return nil
}
windowContainer.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
return nil
}
/// Handler for `close window <window>`.
@objc(handleCloseWindowCommand:)
func handleCloseWindow(_ command: NSScriptCommand) -> Any? {
guard NSApp.validateScript(command: command) else { return nil }
if let managedTerminalController = preferredController as? TerminalController {
managedTerminalController.closeWindowImmediately()
return nil
}
guard let windowContainer = preferredParentWindow else {
command.scriptErrorNumber = errAEEventFailed
command.scriptErrorString = "Window is no longer available."
return nil
}
windowContainer.close()
return nil
}
/// Provides Cocoa scripting with a canonical "path" back to this object.
///
/// Without this, Cocoa can return data but cannot reliably build object
/// references for later script statements. This specifier encodes:
/// `application -> scriptWindows[id]`.
override var objectSpecifier: NSScriptObjectSpecifier? {
guard NSApp.isAppleScriptEnabled else { return nil }
guard let appClassDescription = NSApplication.shared.classDescription as? NSScriptClassDescription else {
return nil
}
return NSUniqueIDSpecifier(
containerClassDescription: appClassDescription,
containerSpecifier: nil,
key: "scriptWindows",
uniqueID: stableID
)
}
}
extension ScriptWindow {
/// Produces the window-level stable ID from the primary controller.
///
/// - Tabbed windows are keyed by tab-group identity.
/// - Standalone windows are keyed by window identity.
/// - Detached controllers fall back to controller identity.
static func stableID(primaryController: BaseTerminalController) -> String {
guard let window = primaryController.window else {
return "controller-\(ObjectIdentifier(primaryController).hexString)"
}
if let tabGroup = window.tabGroup {
return stableID(tabGroup: tabGroup)
}
return stableID(window: window)
}
/// Stable ID for a standalone native window.
static func stableID(window: NSWindow) -> String {
"window-\(ObjectIdentifier(window).hexString)"
}
/// Stable ID for a native AppKit tab group.
static func stableID(tabGroup: NSWindowTabGroup) -> String {
"tab-group-\(ObjectIdentifier(tabGroup).hexString)"
}
}

View File

@@ -678,6 +678,14 @@ extension Ghostty {
return v
}
var macosAppleScript: Bool {
guard let config = self.config else { return true }
var v = false
let key = "macos-applescript"
_ = ghostty_config_get(config, &v, key, UInt(key.lengthOfBytes(using: .utf8)))
return v
}
var maximize: Bool {
guard let config = self.config else { return true }
var v = false

View File

@@ -0,0 +1,7 @@
import Foundation
extension ObjectIdentifier {
var hexString: String {
String(UInt(bitPattern: self), radix: 16)
}
}

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
}
}

View File

@@ -3347,6 +3347,16 @@ keybind: Keybinds = .{},
/// you may want to disable it.
@"macos-secure-input-indication": bool = true,
/// If true, Ghostty exposes and handles the built-in AppleScript dictionary
/// on macOS.
///
/// If false, all AppleScript interactions are disabled. This includes
/// AppleScript commands and AppleScript object lookup for windows, tabs,
/// and terminals.
///
/// The default is true.
@"macos-applescript": bool = true,
/// Customize the macOS app icon.
///
/// This only affects the icon that appears in the dock, application

View File

@@ -46,6 +46,9 @@ extend-ignore-re = [
"\"hel\\\\x",
# Ignore long hex-like IDs such as 815E26BA2EF1E00F005C67B1
"[0-9A-F]{12,}",
# Ignore Apple four char codes
'code="[A-Za-z]{4,8}"',
'"[A-Za-z]{4}"\.fourCharCode',
]
[default.extend-words]