From 291fbf55cb9c6946d7c080c90d8163d0b720dfe0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Mar 2026 14:41:27 -0800 Subject: [PATCH 01/26] macos: AppleScript starting --- macos/Ghostty.sdef | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 macos/Ghostty.sdef diff --git a/macos/Ghostty.sdef b/macos/Ghostty.sdef new file mode 100644 index 000000000..8a837dce8 --- /dev/null +++ b/macos/Ghostty.sdef @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + From c90a782e592aa90e3a1479b80d5b9a3acdc63dff Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Mar 2026 14:55:47 -0800 Subject: [PATCH 02/26] macos: implement basic read-only applescript stuff --- macos/Ghostty-Info.plist | 4 + macos/Ghostty.sdef | 25 ++++++- macos/Ghostty.xcodeproj/project.pbxproj | 6 ++ .../AppleScript/AppDelegate+AppleScript.swift | 67 +++++++++++++++++ .../AppleScript/AppleScriptTerminal.swift | 73 +++++++++++++++++++ 5 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift create mode 100644 macos/Sources/Features/AppleScript/AppleScriptTerminal.swift diff --git a/macos/Ghostty-Info.plist b/macos/Ghostty-Info.plist index 4896681b9..01ccd7b11 100644 --- a/macos/Ghostty-Info.plist +++ b/macos/Ghostty-Info.plist @@ -55,8 +55,12 @@ MDItemKeywords Terminal + NSAppleScriptEnabled + NSHighResolutionCapable + OSAScriptingDefinition + Ghostty.sdef NSServices diff --git a/macos/Ghostty.sdef b/macos/Ghostty.sdef index 8a837dce8..3182f6283 100644 --- a/macos/Ghostty.sdef +++ b/macos/Ghostty.sdef @@ -4,11 +4,15 @@ - + + + + - + + @@ -19,4 +23,21 @@ + + + + + + + + + + + diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 5a3e7a52e..867c52436 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyDebug.entitlements; sourceTree = ""; }; A546F1132D7B68D7003B11A0 /* locale */ = {isa = PBXFileReference; lastKnownFileType = folder; name = locale; path = "../zig-out/share/locale"; sourceTree = ""; }; @@ -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 */, diff --git a/macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift b/macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift new file mode 100644 index 000000000..7bd0513c3 --- /dev/null +++ b/macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift @@ -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() ?? [] } + } +} diff --git a/macos/Sources/Features/AppleScript/AppleScriptTerminal.swift b/macos/Sources/Features/AppleScript/AppleScriptTerminal.swift new file mode 100644 index 000000000..3f6603d0e --- /dev/null +++ b/macos/Sources/Features/AppleScript/AppleScriptTerminal.swift @@ -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 `). + 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 + ) + } +} From 52c0709d88c20f05edc1450d5e7105377f03206d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Mar 2026 20:15:21 -0800 Subject: [PATCH 03/26] macos: add ability for agents to run debug app --- macos/AGENTS.md | 14 ++++++++++++++ macos/Ghostty.sdef | 1 + .../AppleScript/AppDelegate+AppleScript.swift | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/macos/AGENTS.md b/macos/AGENTS.md index 929b37498..8fe34a0df 100644 --- a/macos/AGENTS.md +++ b/macos/AGENTS.md @@ -9,3 +9,17 @@ - Build: `build.nu [--scheme Ghostty] [--configuration Debug] [--action build]` - Output: `build//Ghostty.app` (e.g. `build/Debug/Ghostty.app`) - Run unit tests directly with `build.nu --action test` +## AppleScript + +- The AppleScript scripting definition is in `Ghostty.sdef`. +- Test AppleScript support: + (1) Build with `build.nu` + (2) Launch and activate the app via osascript using the absolute path + to the built app bundle: + `osascript -e 'tell application "" 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 "" to quit'` diff --git a/macos/Ghostty.sdef b/macos/Ghostty.sdef index 3182f6283..647fac3db 100644 --- a/macos/Ghostty.sdef +++ b/macos/Ghostty.sdef @@ -5,6 +5,7 @@ + diff --git a/macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift b/macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift index 7bd0513c3..267863712 100644 --- a/macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift +++ b/macos/Sources/Features/AppleScript/AppDelegate+AppleScript.swift @@ -54,7 +54,7 @@ extension NSApplication { return nil } - return terminal.perform(action: action) + return NSNumber(value: terminal.perform(action: action)) } /// Discovers all currently alive terminal surfaces across normal and quick From 40c74811f16236df9afeb522dcbd9a98ebcb4a3f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Mar 2026 20:32:49 -0800 Subject: [PATCH 04/26] macos: fix perform action --- macos/Ghostty.sdef | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/macos/Ghostty.sdef b/macos/Ghostty.sdef index 647fac3db..9ed37f764 100644 --- a/macos/Ghostty.sdef +++ b/macos/Ghostty.sdef @@ -5,7 +5,9 @@ - + + + From ef669eeae7574335631d6897c07f60ce4015727c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Mar 2026 20:46:52 -0800 Subject: [PATCH 05/26] macos: add AppleScript `split` command Add a new `split` command to the AppleScript scripting dictionary that splits a terminal in a given direction (right, left, down, up) and returns the newly created terminal. The command is exposed as: split terminal direction Also adds a `fourCharCode` String extension for converting four-character ASCII strings to their FourCharCode (UInt32) representation. --- macos/Ghostty.sdef | 19 +++++ macos/Ghostty.xcodeproj/project.pbxproj | 3 +- .../AppleScript/ScriptSplitCommand.swift | 74 +++++++++++++++++++ ...iptTerminal.swift => ScriptTerminal.swift} | 5 +- .../Helpers/Extensions/String+Extension.swift | 9 +++ 5 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 macos/Sources/Features/AppleScript/ScriptSplitCommand.swift rename macos/Sources/Features/AppleScript/{AppleScriptTerminal.swift => ScriptTerminal.swift} (90%) diff --git a/macos/Ghostty.sdef b/macos/Ghostty.sdef index 9ed37f764..962e1329a 100644 --- a/macos/Ghostty.sdef +++ b/macos/Ghostty.sdef @@ -8,6 +8,7 @@ + @@ -25,6 +26,24 @@ + + + + + + + + + + + + + + + + + +