The 👻 Ghost Tab Issue
Previous failure scenario (User perspective):
1. Open a new tab
2. Instantly trigger close other tabs
(eg. through custom user keyboard shortcut)
3. Now you will see an empty Ghost Tab
(Only a window bar with empty content)
The previous failure mode is:
1. Create a tab or window now in `newTab(...)` / `newWindow(...)`.
2. Queue its initial show/focus work with `DispatchQueue.main.async`.
3. Close that tab or window with `closeTabImmediately()` /
`closeWindowImmediately()` before the queued callback runs.
4. The queued callback still runs anyway and calls `showWindow(...)` /
`makeKeyAndOrderFront(...)` on stale state.
5. The tab can be resurrected as a half-closed blank ghost tab.
The fix:
- Store deferred presentation work in a cancellable
DispatchWorkItem and cancel it from the close paths
before AppKit finishes tearing down the tab or window.
- This prevents the stale show/focus callback from
running after close.
- Fixes: https://github.com/ghostty-org/ghostty/issues/11964
Made a private enum type `VersionConfig` to reference whether the
release is a semver or tip, makes it easier for later in the view to
`switch` between cases.
I do think there could be a better place for this enum or we can get rid
of it, open to opinions. Right now version parsing is kind of duplicated
between `AboutView` and `UpdateModalView` so we can also extract to a
common helper if wanted.
Tested by manually setting `Marketing Version` in build settings to
`1.3.1`
<img width="412" height="532" alt="Screenshot 2026-03-30 at 18 31 15"
src="https://github.com/user-attachments/assets/285bb94d-138b-4169-bb66-684eb04b6ca3"
/>
`332b2aefc`
<img width="412" height="532" alt="Screenshot 2026-03-30 at 18 32 48"
src="https://github.com/user-attachments/assets/fea30d39-bea7-4885-8221-1696e148f45e"
/>
### AI Disclosure
I used Sonnet 4.6 to understand where the version strings came from and
in what format, it read release yml files to see what's going on. Then
it proposed really bad code so I manually went in and cleaned up the
view.
This adds features like:
1. Clicking outside of SearchBar works like typing `escape`
2. Typing `tab` while search bar is focused also works like typing `escape`
This is the first step (also another step forward for completing #7879)
to fix various responder issues regarding keyboard shortcuts. I tried my
best to separate changes chunk by chunk; there will follow up pr based
on this to fix them.
This pr doesn't change any existing behaviours/flaws, but following
changes will be easier to review after this.
## AI Disclosure
Claude wrote most of the test cases
Adds progress-style config to control OSC 9;4 progress bar visibility.
Defaults to true, set false to hide.
Fixes#11241
AI Disclosure: Claude Code (Opus 4.6) used for codebase exploration,
code review, and testing assistance. All code written and reviewed by
hand.
Fixes#11396
Track menu items populated from Ghostty keybind actions and only trigger
those from SurfaceView performKeyEquivalent. This avoids app-default
shortcuts such as Hide from pre-empting explicit keybinds.
Fixes#11379
For this pass, I made it a very simple "within 20%" (height-wise) of the
split handle. There is no horizontal component. I want to find the right
balance between always visible (today mostly) to only visible on direct
hover, because I think it'll be too hard to discover on that far right
side.
Tests that window position and size are correctly restored after
reopen for all four macos-titlebar-style variants.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#11316
This mirrors the `prompt` actions (hence why there is no window action
here) and enables setting titles via keybind actions which importantly
lets this work via command palettes, App Intents, AppleScript, etc.
This disables all the automatic one-time code inputs in Ghostty.
It'd be really neat to actually dynamically change this (not sure if its
possible with NSTextContext or how often thats cached) but for now we
should just fully disable it.
Test boolean, string, enum, and numeric config properties using
TemporaryConfig to verify defaults and parsed values.
Co-Authored-By: Claude <noreply@anthropic.com>
Fixes phantom mouse drag/selection when switching splits or apps.
The suppressNextLeftMouseUp flag and core mouse click_state were not
being reset on focus transitions, causing stale state that led to
unexpected drag behavior.
- Reset suppressNextLeftMouseUp in focusDidChange when losing focus
- Defensively reset the flag when processing normal clicks
- Reset core mouse.click_state and left_click_count on focus loss
## Summary
- After finishing an inline tab title edit (via keybind or
double-click), all keyboard input is lost because
`TabTitleEditor.finishEditing()` sets `makeFirstResponder(nil)`, leaving
the window itself as first responder with no path back to the terminal
surface.
- Adds a `tabTitleEditorDidFinishEditing` delegate callback to
`TabTitleEditorDelegate` that fires after every edit (commit or cancel).
- `TerminalWindow` implements it by calling
`makeFirstResponder(focusedSurface)` to restore keyboard focus to the
terminal.
Fixes https://github.com/ghostty-org/ghostty/discussions/11315
## Testing
- [x] Bind `prompt_tab_title` to a keybind (e.g. `keybind =
cmd+shift+i=prompt_tab_title`)
- [x] Trigger inline tab title edit via keybind, press Enter — verify
keyboard input works immediately
- [x] Trigger inline tab title edit via keybind, press Escape — verify
keyboard input works immediately
- [x] Double-click a tab title, press Enter — verify keyboard input
works immediately
- [x] Double-click a tab title, press Escape — verify keyboard input
works immediately
- [x] Verify Cmd+number tab switching works after all of the above
- [x] Verify split pane focus is correct after editing tab title with
splits open
AI disclosure: Codebase exploration and review via [Claude
Code](https://claude.com/claude-code)
After finishing an inline tab title edit (via keybind or double-click),
`TabTitleEditor.finishEditing()` calls `makeFirstResponder(nil)` to
clear focus from the text field, leaving the window itself as first
responder. No code path restores focus to the terminal surface, so all
keyboard input is lost until the user clicks into a pane.
Add a `tabTitleEditorDidFinishEditing` delegate callback that fires
after every edit (commit or cancel). TerminalWindow implements it by
calling `makeFirstResponder(focusedSurface)` to hand focus back to the
terminal.
Fixes https://github.com/ghostty-org/ghostty/discussions/11315
Co-Authored-By: Claude <noreply@anthropic.com>
Add initialContentSize fallback on TerminalViewContainer so
intrinsicContentSize returns the correct value immediately,
without waiting for @FocusedValue to propagate. This removes
the need for the DispatchQueue.main.asyncAfter 40ms delay.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests that validate intrinsicContentSize returns a correct value when
TerminalController.windowDidLoad() reads it. Currently fail, proving
the race condition where @FocusedValue hasn't propagated
lastFocusedSurface before the 40ms timer fires.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This adds two new propeties to make it easy to get the frontmost (main)
window and the focused terminal within a tab. We already had a property
to get the selected tab of a tab group.
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.
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.
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`.
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 `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.
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)
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.
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 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 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 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.
This is an update to address common agentic issues I run into,
but the `build.nu` script may be generally helpful to people using
the Nix env since `xcodebuild` is broken by default in Nix due to the
compiler/linker overrides Nix shell does.
## Summary
This extends the macOS bell implementation to support the `audio` bell
feature, bringing it to parity with GTK/Linux.
Previously, macOS only had the `system` feature (`NSSound.beep()`). This
PR adds:
- **`audio` bell feature on macOS**: plays the file at `bell-audio-path`
using `NSSound(contentsOfFile:)`, respecting `bell-audio-volume`
- **`cval()` on the `Path` type**: allows `Path` values (a union type)
to be returned through the C API, which is needed for Swift to read
`bell-audio-path`
- **Removes `(GTK only)` restriction** from `bell-audio-path` and
`bell-audio-volume` documentation
## How it works
In `AppDelegate.swift`, when the bell rings and the `audio` feature is
enabled, Ghostty now:
1. Reads `bell-audio-path` from config
2. Loads it as an `NSSound`
3. Applies `bell-audio-volume` and plays it
Falls back gracefully if the path is not set or the file cannot be
loaded.
## Example config
```
bell-features = audio
bell-audio-path = /System/Library/Sounds/Glass.aiff
bell-audio-volume = 0.8
```
## Testing
- Set `bell-features = audio` and `bell-audio-path` to any valid audio
file
- Trigger a bell with `echo -e '\a'`
- Audio should play at the configured volume
Extends the macOS bell implementation to support the `audio` bell
feature by playing a user-specified audio file via NSSound.
Previously, macOS only supported the `system` feature (NSSound.beep()).
This change adds support for:
- `audio` bell feature: plays the file at `bell-audio-path` using
NSSound, respecting the `bell-audio-volume` setting
- Adds `cval()` to the `Path` type so it can be returned via the C API
Also removes the "(GTK only)" restriction from `bell-audio-path` and
`bell-audio-volume` documentation, as these options now work on macOS.
Example config:
bell-features = audio
bell-audio-path = /System/Library/Sounds/Glass.aiff
bell-audio-volume = 0.8
This fixes two overlapping issues regarding window positioning and Cmd+W window closures on macOS:
1. `window-position-x` and `window-position-y` coordinates were being ignored on initial launch because `TerminalWindow.setInitialWindowPosition` depended on the `TerminalController`, which isn't fully attached during `awakeFromNib`. This logic was moved so explicit coordinates are correctly enforced.
2. When closing a window via Cmd+W (leaving the app active), reopening the window would continuously cascade down and to the right rather than restoring to the previous position. It now checks if there are other windows open before cascading.
3. `LastWindowPosition` was updated to save both the frame origin and size (width/height), ensuring that restoring a closed window correctly mimics native AppKit State Restoration size behaviors while honoring explicit configurations.
Depends on #11030
- Update constraints of `TerminalGlassView`
- Use `TerminalViewContainer.DerivedConfig` to map styling properties
- Add TerminalViewContainerTests
- Instead of using delay, now the view updates are explicitly called by
window controllers
* ensure that `ghostty.h` compiles during basic Zig tests
* ensure that non-exhaustive enums are kept synchronized between
`ghostty.h` and their respective Zig counterpart.
* adjust some enums that varied from established conventions
Fixes#11029 (probably)
If you renamed the app bundle, the prior check would infinite loop due
to the combination of two bugs: invalid termination checks and
hardcoding "Ghostty.app"
Fixes#10935
This is a more robust way to detect "is my surface focused" because that
question usually means "is my surface the last focused surface" if a
_different_ surface is not focused. We already have used this pattern
all over but we should extend it to SwiftUI too.