Skip to content

AppleScript#11208

Merged
mitchellh merged 26 commits intomainfrom
applescript
Mar 7, 2026
Merged

AppleScript#11208
mitchellh merged 26 commits intomainfrom
applescript

Conversation

@mitchellh
Copy link
Copy Markdown
Contributor

@mitchellh mitchellh commented Mar 6, 2026

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

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

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

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

mitchellh added 21 commits March 5, 2026 20:03
Add a new `split` command to the AppleScript scripting dictionary that
splits a terminal in a given direction (right, left, down, up) and
returns the newly created terminal.

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

Also adds a `fourCharCode` String extension for converting four-character
ASCII strings to their FourCharCode (UInt32) representation.
Add two new AppleScript commands to the scripting dictionary:

- `focus terminal <terminal>` — focuses the given terminal and brings
  its window to the front.
- `close terminal <terminal>` — closes the given terminal without a
  confirmation prompt.

Each command is implemented as an NSScriptCommand subclass following
the same pattern as the existing split command.
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).
Add standard Cocoa scripting definitions to the AppleScript dictionary:

- Application properties: name, frontmost, version
- Standard Suite commands: exists, quit

These are backed by built-in Cocoa scripting classes (NSExistsCommand,
NSQuitCommand) and standard NSApplication KVC keys, so no Swift code
changes are needed.
Add ScriptWindow and ScriptTab classes to expose window/tab hierarchy
to AppleScript, along with the corresponding sdef definitions.
The application class in Ghostty.sdef was missing a responds-to
declaration for the quit command. Apple's Cocoa Scripting requires
the application class to explicitly declare it responds to quit via
handleQuitScriptCommand: for the aevtquit event to be dispatched.
Expose terminal surfaces as elements on both ScriptWindow and ScriptTab,
allowing AppleScript to enumerate terminals scoped to a specific window
or tab (e.g. `terminals of window 1`, `terminals of tab 1 of window 1`).

Changes:
- Add `<element type="terminal">` to window and tab classes in Ghostty.sdef
- Add `terminals` computed property and `valueInTerminalsWithUniqueID:`
  lookup to ScriptWindow (returns all surfaces across all tabs)
- Add `terminals` computed property and `valueInTerminalsWithUniqueID:`
  lookup to ScriptTab (returns surfaces within that tab)
Add a `name` property (code `pnam`, cocoa key `title`) to the window, tab,
and terminal classes in the scripting definition. This follows the standard
Cocoa scripting convention where `name`/`pnam` maps to the `title` KVC key,
matching what Apple does in CocoaStandard.sdef for NSWindow.

Also fixes the pre-existing terminal `title` property which used a custom
four-char code (`Gttl`) that AppleScript could not resolve directly — only
via `properties of terminal`. All three classes now use the standard `pnam`
code so `name of window 1`, `name of tab 1 of window 1`, and
`name of terminal 1` all work correctly.
Add a `new window` command to the scripting dictionary and wire it to
`NSApplication` so AppleScript can create Ghostty windows.

The command returns a scripting `window` object for the created window,
with a fallback to a direct wrapper when AppKit window ordering has not
yet refreshed in the current run loop.
Add a `surface configuration` record type to the scripting dictionary,
implement `new surface configuration` (with optional copy-from), and allow
`new window` to accept `with configuration`.
Document the preferred Ghostty.sdef top-level order in AGENTS.md and reorder 
Ghostty Suite definitions to classes, records, enums, then commands.
Add scripting dictionary commands for activating windows, selecting tabs,
closing tabs, and closing windows.

Implement the corresponding Cocoa AppleScript command handlers and expose
minimal ScriptWindow/ScriptTab helpers needed to resolve live targets.

Verified by building Ghostty and running osascript commands against the
absolute Debug app path to exercise all four new commands.
@mitchellh mitchellh added this to the 1.3.0 milestone Mar 6, 2026
@mitchellh mitchellh requested a review from a team as a code owner March 6, 2026 23:04
@mitchellh mitchellh mentioned this pull request Mar 6, 2026
Copy link
Copy Markdown
Contributor

@nmggithub nmggithub left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice. Only a few things I would personally change:

  1. define explicit return value types for the script handler methods (if possible),
  2. define better terminology for non-terminal (window / tab) handling commands, and
  3. use errAEEventNotPermitted inside validateScript instead of errAEEventFailed.

Comment thread macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift Outdated
Comment thread macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift
Comment thread macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift Outdated
Comment thread macos/Ghostty.sdef
Comment thread macos/Ghostty.sdef
Change split, focus, close, activate window, select tab, close tab, and
close window commands to accept their target object as a direct parameter
instead of a named parameter. This produces natural AppleScript syntax:

  activate window (window 1)
  close tab (tab 1 of window 1)
  split (terminal 1) direction right

instead of the awkward redundant form:

  activate window window (window 1)
  close tab tab (tab 1 of window 1)
  split terminal (terminal 1) direction right

The implementation moves command logic from NSScriptCommand subclasses
into responds-to handler methods on ScriptTerminal, ScriptWindow, and
ScriptTab, which is the standard Cocoa Scripting pattern for commands
whose direct parameter is an application class.
@mitchellh mitchellh merged commit fd3a62b into main Mar 7, 2026
130 checks passed
@mitchellh mitchellh deleted the applescript branch March 7, 2026 15:52
@gurgeous
Copy link
Copy Markdown

Thank you for adding. I am using this now to create new titled tabs with a particular title from the cli. This is embedded in a larger bash script:

on run argv
  set title to item 1 of argv
  tell application "Ghostty"
    set term to terminal 1 of (new tab in front window)
    -- note space to avoid history
    input text " printf \"\\e]0;%s\\a\" \"" & title & "\"; clear" to term
    send key "enter" to term
  end tell
end run

Looking forward to trying out #11316 as well. Minor feedback from my initial explorations:

  • some discussion about window vs. pane vs. tab vs. terminal in the applescript docs
  • some richer examples (that create windows/panes/tabs/terminals) would be nice
  • easier syntax for sending enter

I could also imagine combining with printf '\e]21;background=#200;\a' for deeper customization. Wait until alfred, btt and hammerspoon users start messing with this stuff...

@mitchellh
Copy link
Copy Markdown
Contributor Author

I'm usually annoyed when people comment on merged PRs but your comment is actually good. So, I'm mildly annoyed but thank you lol.

@gurgeous
Copy link
Copy Markdown

This will be the last time I comment on a merged PR... Apologies. There is a lot of action in the repo and it isn't always obvious where to chime in with this stuff. I will stick to discussions. Time to bring in OpenClaw to gently spank well meaning users

@cmaciasjimenez
Copy link
Copy Markdown

cmaciasjimenez commented Apr 10, 2026

Hi! you mention in your Twitter that with more code we could do different layouts for the terminal windows/tabs/splits. Is this possible with the current version of this feature?

I'm trying to find a way to run a script to set up new tabs but the best I've gotten to work is to have a new tab/window with a vertical split but I would like to have that vertical split be like 20%/80% instead of 50/50. I've hacked it bu running a resize action X number of times but I would like to know if there's some cleaner way to do this with some kind of layout or resize feature.

Thank you

Edit: I will create a discussion asking about this. Maybe someone can help me there or it is actually not a current feature and maybe we could open a GH issue then. Thanks

@00-kat
Copy link
Copy Markdown
Contributor

00-kat commented Apr 10, 2026

Edit: I will create a discussion asking about this.

xref #12218.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants