Fixes#10077
Clipboard read confirmations would immediately show a sheet which
grabbed focus. This could be used for a bunch of dumb reasons, including
DoS attacks. But, it also caused focus/sheet loops for programs that did
OSC52 on focus changes (which was seen via some Neovim configs!).
Now, if a surface is unfocused, we bell the surface and show the confirmation
request on next focus. If the surface is not focused or another request
comes in, we cancel the prior one.
This also fixes some memory management issues around clipboard requests
that were likely small leaks (didn't verify the old bug, but verified
the new code, and eyeballed the old).
windowDidLoad undoes macOS automatic window tabbing by inspecting
window.tabGroup. Accessing tabGroup on a fresh window materializes
AppKit's tab group machinery, which takes ~15-20ms and is on the
critical path of every window creation, including the first window at
app launch.
AppKit only auto-tabs a fresh window when the system tabbing
preference is "always": the tab bar "+" button goes through
newWindowForTab which we intercept and route through our own tab
logic, so it never auto-tabs. Guard the check on
NSWindow.userTabbingPreference == .always so everyone else skips the
tab group materialization entirely.
Measured on macOS (Apple Silicon) during app launch via the startup
timeline instrumentation:
windowDidLoad tab group check: 17.8ms -> ~0ms
main() -> window visible: median ~173ms -> ~165ms (n=7)
Measured on macOS (Apple Silicon) during app launch, via a startup
timeline instrumented across the Swift app and libghostty:
config apply, errors step: 35.5ms -> 0.1ms
main() -> first frame rendered: ~126ms -> ~93ms
main() -> window visible: ~193ms -> ~173ms
For new windows to get their appearance synced, we need to call
`syncAppearance` after `super.showWindow(sender)`. All previous calls to
`syncAppearance` on `TerminalWindow` will be ignored because the window
needs to have `isVisible` set to `true`.
This regression was introduced by:
5368adcd29
It added `.dropFirst()` to the `focusedSurface` appearance publishers in
`TerminalController.swift` which removes the initial call of the
subscription.
Fixes https://github.com/ghostty-org/ghostty/issues/13324
(landed on the same fix as @rasitakyol found here:
https://github.com/ghostty-org/ghostty/pull/13341)
Matches are sorted in the following order:
leadingColor > title > subtitle > description.
Ranking is lexicographic on (colorScore, textScore)
<img height="300" alt="image"
src="https://github.com/user-attachments/assets/1ec99e67-537e-4fc6-b595-d7eec8cbf31d"
/>
### AI Disclosure
Claude reviewed and added unit tests, also did some refactoring of my
original implementation.
Fixes #/13074
Overlapping clipboard confirmations now defer denial until the next
main queue turn rather than completing inside the confirmation callback.
This prevents the native request state from being invalidated while its
callback is still active, avoiding the OSC 52 crash reported in #13074.
The deferred closure retains the originating surface view and completes
the ignored request with empty data, preserving the existing deny
behavior.
Fixes#13444
A close while AppKit temporarily cleared/changed a surface's window
would leak the surface in the controller's pslit tree. This retained
surface kept a bunch of resources around, particularly large IOSurfaces.
This seems to only be reproducible under scripted load: rapid terminal
creation/destruction so that destruction happens just while there is a
nil window on a surface view.
Track surface ownership in a weak controller map updated alongside the
split tree, with validated fallbacks for existing attachment state.
Resolve scripted and App Intent operations through that ownership, and
route non-confirming root closes directly through the immediate tab or
window close path so teardown always reaches the renderer.
Fixes#13444
A close while AppKit temporarily cleared/changed a surface's window would
leak the surface in the controller's pslit tree. This retained surface kept
a bunch of resources around, particularly large IOSurfaces.
This seems to only be reproducible under scripted load: rapid terminal
creation/destruction so that destruction happens just while there is a nil
window on a surface view.
Track surface ownership in a weak controller map updated alongside the split
tree, with validated fallbacks for existing attachment state. Resolve
scripted and App Intent operations through that ownership, and route
non-confirming root closes directly through the immediate tab or window close
path so teardown always reaches the renderer.
#13611
Route new-tab window presentation through an Objective-C exception catcher.
AppKit can raise an NSInternalInconsistencyException while selecting a
new tab in native fullscreen.
Catch the presentation exception, report it through the existing error
logging path, and leave Ghostty running when AppKit’s fullscreen window
stack is inconsistent.
This was pretty hard to reproduce but I was able to reproduce it about
1/3rd of the time via AppleScript automation...
Fixes#13386
Defer transparent-titlebar KVO rebinding to the next main-queue turn.
Track the observed tab group so unchanged bindings are preserved.
Previously, a tab-group callback could invalidate and recreate its own
observation before returning, leaving closed terminal windows registered
with AppKit after the undo timeout. These windows accumulated titlebar and
layer state, increasing memory use and WindowServer CPU with tab churn.
Validated with an AppDelegate change that sat and created/closed tabs
in a loop, then counted weak controllers/windows/nsapp window.
Co-authored-by: Mustafa J <mustafa.0x@gmail.com>
#11799
Creating a CGEventTap without Accessibility permission leaks a Mach
port inside CoreGraphics on every failed attempt. The global keybind
listener retried this once per second while waiting for permission, so
Ghostty eventually exhausted the process port limit.
Request Accessibility access once, poll AXIsProcessTrusted while
access is denied, and create the event tap only after access is
granted. Stop polling before creation so an unrelated tap failure
cannot restart the leaking retry loop.
"Why is my quick terminal not taking up the entire top of my docked Mac
screen after I reconnect?" Boy howdy are you in the right PR.
It turns out that the quick terminal caches its last-closed window frame
per display so it can restore the user's size when reopened. The cache
entry was considered valid whenever the current screen was the same size
*or larger* than when the frame was saved ("persist when screens grow").
This has led to a pattern that was simply maddening. To wit: that rule
breaks across display changes.
When an external display is disconnected and later reconnected at a
different resolution (common after traveling with a laptop, do not even
get me started on projectors) the same display can come back larger than
when the frame was cached. The stale frame is still treated as valid and
restored, so the quick terminal no longer fills the screen (it appears
at a partial width/height). Because the cache is persisted, restarting
Ghostty does not clear it, and the user is slowly driven mad. Welcome to
madness; we have snacks.
This PR addresses this by treating a cached frame as valid when the
screen geometry matches exactly (both backing scale factor and frame
size). On any mismatch we drop the entry and fall back to the configured
quick-terminal-size. Manual resizes are still remembered across toggles
within a stable display configuration.
Fixes the regression reported in #12348.
AI disclaimer: I used AI for this. Of course I used AI for this, my code
is terrible on a good day. Specifically, Claude Code, as well as a
custom harness that has the curious tendency to write commit messages
containing conspiracy theories about the code because I am history's
greatest monster.
Fight me!
The quick terminal caches its last-closed window frame per display so it
can restore the user's size when reopened. The cache entry was considered
valid whenever the current screen was the same size *or larger* than when
the frame was saved ("persist when screens grow"). This has led to a pattern
that was simply maddening. To wit:
That rule breaks across display changes. When an external display is
disconnected and later reconnected at a different resolution (common
after traveling with a laptop) the same display can come back larger
than when the frame was cached. The stale frame is still treated as valid
and restored, so the quick terminal no longer fills the screen (it appears
at a partial width/height). Because the cache is persisted, restarting
Ghostty does not clear it, and the user is slowly driven mad.
Only treat a cached frame as valid when the screen geometry matches
exactly (both backing scale factor and frame size). On any mismatch we
drop the entry and fall back to the configured quick-terminal-size. Manual
resizes are still remembered across toggles within a stable display
configuration.
Fixes the regression reported in #12348.
Close#12825
Skip the initial emissions from the focused surface appearance publishers after a tab focus change. The focused surface is already synced immediately, so the initial Combine values only repeat the same titlebar and background updates. Subsequent derived config and OSC background changes still resync the window appearance.
Fixes 2 bugs
1. After dragging a non-focused surface from window A to window B
**quickly without making B the key window**, the focused surface in
window A is not receiving `keyDown` events.
https://github.com/user-attachments/assets/a8861c0a-9300-470d-bf7e-0f32a9ab2cd1
2. #12343 After dragging a surface from tab A to tab B within the same
window, the dragged surface is not rendering input correctly.
> The reason the thread is stuck is because the surface's occlusion
state is set to invisible after target tab's activate while dragging,
since the dragged surface is still in previous tree before dropping, and
after dropping the occlusion state of this surface is not updated to
visible, which causing the surface is accepting input but not rendering.
https://github.com/user-attachments/assets/d67f5dba-8609-4f67-a956-921982faf796