Related to #10651
Default Ghostty dependency builds to libghostty-vt-only mode and
avoid initializing anything that would trigger broader dependency
requirements.
The impact of this is that Zig consumers can import ghostty-vt without
requiring Xcode on macOS.
Image eviction removed associated placements from storage without
deinitializing them. Pin-backed placements therefore remained registered
with the screen after eviction, allowing graphics-heavy output to
accumulate stale tracked pins.
Pass the owning screen through image insertion and eviction, and
deinitialize each placement before removing it. Cover both the released
pin and a retained image's live pin in the eviction regression test.
Surface.handleMessage allocated a null-terminated copy for every working
directory update. OSC 7 values fit within the parser's 2 KiB fixed
buffer, so use stack-fallback storage sized for that bound and its
terminator.
The message type does not enforce the OSC bound, so an oversized future
producer still falls back to the heap. performAction already borrows the
value only for the duration of the call, preserving its existing
lifetime.
Image eviction removed associated placements from storage without
deinitializing them. Pin-backed placements therefore remained registered
with the screen after eviction, allowing graphics-heavy output to
accumulate stale tracked pins.
Pass the owning screen through image insertion and eviction, and
deinitialize each placement before removing it. Cover both the released
pin and a retained image's live pin in the eviction regression test.
Co-authored-by: Tim Culverhouse <tfc@ampcode.com>
Surface.handleMessage allocated a null-terminated copy for every working
directory update.
Use a small 256-byte stack-fallback buffer for common working directory
lengths without adding significant pressure to this deep call stack. Longer
paths retain the existing heap behavior, and performAction continues to borrow
the value only for the duration of the call.
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.
Track each image's placement count in its existing metadata. This lets
us use constant-time usage checks (rather than scans) during eviction.
Select the best candidate directly from storage on each eviction,
preserving the existing priority order: unused status, transient hint,
generation, then ID.
Since eviction no longer allocates, it can't fail, so callers no longer
need to handle out-of-memory conditions.
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#13276
Make CachedValue safe for concurrent terminal content reads and expiry.
The expiry task could previously release cached Swift String storage
while another thread retained it, aborting the process during otherwise
normal terminal use.
Protect cached values and task handles with an NSLock, and exercise
concurrent reads across repeated expiration in a regression test.
Fix d=p and d=c point deletion so only placements intersecting the
target cell are removed.
Previously, placements spanning multiple rows could be deleted from
columns outside the target because the page-order comparison flattened
row and column coordinates.
Check the rectangle's column independently and use page order only for
its row span, matching Kitty's implementation:
https://github.com/kovidgoyal/kitty/blob/master/kitty/graphics.c
NOTE: I did not look at Kitty's source prior to fixing this. I only
referenced it after the fix to verify that the behavior matches.
Spec:
https://sw.kovidgoyal.net/kitty/graphics-protocol/#deleting-images
Fixes#13266
Keep search text and its selection range synchronized as a single state
transition.
Deleting or replacing a search term could leave a String.Index range
from the old value attached to the text field. Applying that range could
crash the app.
Clear selection before publishing new text.
Debug libghostty-vt dependencies embedded in ReleaseFast or ReleaseSmall
binaries no longer panic when narrow text overwrites the tail of a wide
glyph.
Replace the root module's std.debug.runtime_safety gate with
build_options.slow_runtime_safety so mixed optimization modes use the
dependency's safety configuration consistently.
#12755
Reset previously copied the active default into the override. This is
wrong, a reset should unset the override and defer back to the default.
Reset foreground, background, and cursor colors now resolve through the
current default while explicit OSC overrides remain unchanged across
configuration updates.
Set a configured background in the OSC 11 regression, assert OSC 111
clears its override, then change the default to verify the reset color
follows it.
#12885
Ghostty already implements SGR 53 and 55, but its terminfo description
does not expose the corresponding Smol and Rmol capabilities. Add both
entries so the advertised capabilities match the renderer. Tmux uses
this to gate overline.
Fixes#13219
Screen file actions intentionally retain their temporary directory so
the generated path remains valid after dispatch. The directory and
parent handles were retained with it, while TempDir.deinit also left the
parent handle open.
Each successful action leaked two descriptors. Repeated screen,
scrollback, or selection writes could exhaust the process descriptor
limit and prevent new PTYs, tabs, and windows from opening.
Give TempDir an exhaustive close mode that either deletes or retains its
contents while always releasing both handles. Defer screen-file cleanup,
retaining only after successful dispatch, and cover both lifecycle paths
with descriptor tests.
Fixes#13269
Move VT tabstop serialization ahead of screen formatting so
cursor-moving CHA and HTS sequences run before screen state is restored.
Tabstop-enabled snapshots previously finished at the final configured
tabstop instead of the serialized cursor position. Replaying a snapshot
could resume input in the wrong column.
Keep tabstop bytes in their original pin-map accounting and extend the
round-trip test to verify tabstops, cursor position, and map length.
Fixes#13293
Treat Core Video display link creation as optional when macOS has no
active displays. The previous error path reported every creation failure
as out of memory and aborted renderer initialization.
Tabs created while the session is locked now initialize normally and
fall back to event-driven rendering without vsync.
Fixes https://github.com/ghostty-org/ghostty/discussions/13242
Clear the IOSurfaceLayer display callback when releasing the wrapper.
The host view can retain the backing layer beyond renderer teardown.
This prevents a later Core Animation display pass from invoking the
callback with a freed renderer context.
This doesn't happen the way Ghostty GUI uses our renderer, but it is
possible for folks using ghostty-internal and its straightforward and
easy for us to fix it.
Debug libghostty-vt dependencies embedded in ReleaseFast or ReleaseSmall
binaries no longer panic when narrow text overwrites the tail of a wide
glyph.
Replace the root module's std.debug.runtime_safety gate with
build_options.slow_runtime_safety so mixed optimization modes use the
dependency's safety configuration consistently.
#12755
Reset previously copied the active default into the override. This is
wrong, a reset should unset the override and defer back to the default.
Reset foreground, background, and cursor colors now resolve through the
current default while explicit OSC overrides remain unchanged across
configuration updates.
Set a configured background in the OSC 11 regression, assert OSC 111
clears its override, then change the default to verify the reset color
follows it.
#12885
Ghostty already implements SGR 53 and 55, but its terminfo description
does not expose the corresponding Smol and Rmol capabilities. Add both
entries so the advertised capabilities match the renderer.
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#13276
Make CachedValue safe for concurrent terminal content reads and expiry.
The expiry task could previously release cached Swift String storage
while another thread retained it, aborting the process during otherwise
normal terminal use.
Protect cached values and task handles with an NSLock, and exercise
concurrent reads across repeated expiration in a regression test.
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#13219
Screen file actions intentionally retain their temporary directory so
the generated path remains valid after dispatch. The directory and
parent handles were retained with it, while TempDir.deinit also left the
parent handle open.
Each successful action leaked two descriptors. Repeated screen,
scrollback, or selection writes could exhaust the process descriptor
limit and prevent new PTYs, tabs, and windows from opening.
Give TempDir an exhaustive close mode that either deletes or retains its
contents while always releasing both handles. Defer screen-file cleanup,
retaining only after successful dispatch, and cover both lifecycle paths
with descriptor tests.
Fixes#13269
Move VT tabstop serialization ahead of screen formatting so cursor-moving
CHA and HTS sequences run before screen state is restored.
Tabstop-enabled snapshots previously finished at the final configured
tabstop instead of the serialized cursor position. Replaying a snapshot
could resume input in the wrong column.
Keep tabstop bytes in their original pin-map accounting and extend the
round-trip test to verify tabstops, cursor position, and map length.
Fixes#13266
Keep search text and its selection range synchronized as a single
state transition.
Deleting or replacing a search term could leave a String.Index range
from the old value attached to the text field. Applying that range
could crash the app.
Clear selection before publishing new text.
Fixes#13293
Treat Core Video display link creation as optional when macOS has no
active displays. The previous error path reported every creation
failure as out of memory and aborted renderer initialization.
This also resyncs the display link on any display change so when
a display becomes available it re-adds itself.
Tabs created while the session is locked now initialize normally and
fall back to event-driven rendering without vsync.
Clear the IOSurfaceLayer display callback when releasing the wrapper.
The host view can retain the backing layer beyond renderer teardown.
This prevents a later Core Animation display pass from invoking the
callback with a freed renderer context.
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.
Encoded key requests are owned by the caller until they are added to a
key sequence or queued for IO. The child_exited path and failed queue
append returned without freeing the allocated request.
The existing errdefer was also too broad: after queueIo took ownership,
a later setSelection or queueRender error could free the queued request.
We now free requests in the return paths that still own them, and the
errdefer has been removed.
Also, activate a sequence only after encoding and queue append succeed
so failure preserves the previous sequence state.
#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...
#13616
Reset the soft-wrap state when CSI 2 K erases the complete cursor row.
Previously, erase-to-end reset the flag while complete-line erase left
it set.
WezTerm, kitty, Alacritty, VTE, and xterm.js clear the wrap state for
complete-line erase. xterm preserves it, but xterm copies physical rows
during resize instead of reflowing them. Diverge from xterm so reflow in
Ghostty does not treat erased rows as one logical line, and cover the
behavior with a resize regression test.
Fixes https://github.com/ghostty-org/ghostty/discussions/13441
Refill the line iterator when an ignored comment or blank line consumes
the remaining buffered data.
Configuration parsing previously stopped silently at these boundaries
and left every subsequent setting unapplied.
Request more data before continuing the loop and cover both comment and
blank line boundaries with buffered-reader regression tests.
Refill the line iterator when an ignored comment or blank line
consumes the remaining buffered data.
Configuration parsing previously stopped silently at these boundaries
and left every subsequent setting unapplied.
Request more data before continuing the loop and cover both comment
and blank line boundaries with buffered-reader regression tests.
#13616
Reset the soft-wrap state when CSI 2 K erases the complete cursor
row. Previously, erase-to-end reset the flag while complete-line erase
left it set.
WezTerm, kitty, Alacritty, VTE, and xterm.js clear the wrap state for
complete-line erase. xterm preserves it, but xterm copies physical rows
during resize instead of reflowing them. Diverge from xterm so reflow in
Ghostty does not treat erased rows as one logical line, and cover the
behavior with a resize regression test.
#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...
Encoded key requests are owned by the caller until they are added to a
key sequence or queued for IO. The child_exited path and failed queue
append returned without freeing the allocated request.
The existing errdefer was also too broad: after queueIo took ownership, a
later setSelection or queueRender error could free the queued request.
We now free requests in the return paths that still own them, and the
errdefer has been removed.
Also, activate a sequence only after encoding and queue append succeed
so failure preserves the previous sequence state.
OSC8 hyperlinks previously executed directly via the NSWorkspace opener
so a malicious application can just do whatever it wanted and trick the
user into opening something through Launch Services.
This PR notifies apprt of OSC8 hyperlinks so they can be handled
specially. In this PR, I added macOS-specific handling of OSC8 through a
variety of improvements:
- Preview text is sanitized, so invisible Unicode characters now show.
- Questionable-looking URLs require confirmation to open, but a user can
confirm to open.
- Very questionable or definitely unsafe URLs are blocked with an alert
that only allows the user to copy the link. The alert also notifies the
user why.
This PR also adds an explicit `link-osc8` config (default true) that
users can use to disable osc8 completely.
## Demos
### Custom URL Schemes (Confirm)
<img width="1432" height="1110" alt="CleanShot 2026-08-05 at 10 25
57@2x"
src="https://github.com/user-attachments/assets/f7773ca2-3389-4749-a5c9-393ae097c044"
/>
### Invisible Characters (Block)
<img width="1432" height="1110" alt="CleanShot 2026-08-05 at 10 26
44@2x"
src="https://github.com/user-attachments/assets/bd2d0f33-f128-46e8-9bdb-227afecbb942"
/>
### Executable Target (Block)
<img width="1432" height="1110" alt="CleanShot 2026-08-05 at 10 27
31@2x"
src="https://github.com/user-attachments/assets/080c0524-2c8e-4931-892f-d2643a5d0d4e"
/>
OSC8 hyperlinks previously executed directly via the NSWorkspace opener
so a malicious application can just do whatever it wanted and trick the
user into opening something through Launch Services.
This PR notifies apprt of OSC8 hyperlinks so they can be handled
specially. In this PR, I added macOS-specific handling of OSC8 through a
variety of improvements:
- Preview text is sanitized, so invisible Unicode characters now show.
- Questionable-looking URLs require confirmation to open, but a user
can confirm to open.
- Very questionable or definitely unsafe URLs are blocked with an
alert that only allows the user to copy the link. The alert also
notifies the user why.
Track each image's placement count in its existing metadata. This lets
us use constant-time usage checks (rather than scans) during eviction.
Select the best candidate directly from storage on each eviction, preserving
the existing priority order: unused status, transient hint, generation, then
ID.
Since eviction no longer allocates, it can't fail, so callers no longer
need to handle out-of-memory conditions.
Cap allocating OSC payloads at 8 MiB and retain at most 64 grapheme
suffix codepoints per cell. Our limits are generous compared to other
terminals and this prevents an easy DoS.
When the grapheme codepoint max is hit we just ignore any remainders.
This can result in real broken graphemes because Unicode spec is really
unbounded on them but for all practical use cases its reasonable.
Compared to other terminals:
| Terminal | OSC capture limit | Cell codepoints |
| --- | ---: | ---: |
| Ghostty | 8 MiB | 65 |
| kitty | ~256 KiB ordinary | 24 |
| VTE | 4,096 scalars | 11 |
| xterm | 20 or 600 KB | 3 default, 6 max |
| Alacritty | unbounded | unbounded |
| WezTerm | unbounded | no explicit limit |
Add an explicit libghostty-vt title-report option and keep CSI 21 t
disabled unless an embedder enables it.
Previously, registering the general PTY write callback also caused the
terminal to echo attacker-controlled window titles. This exposed
embedders to command injection after user interaction. Ghostty fixed
this a long time ago by making CSI 21 t an opt-in in the config. Do the
same but with our C/Zig API.
Cap allocating OSC payloads at 8 MiB and retain at most 64 grapheme
suffix codepoints per cell. Our limits are generous compared to other
terminals and this prevents an easy DoS.
When the grapheme codepoint max is hit we just ignore any remainders.
This can result in real broken graphemes because Unicode spec is really
unbounded on them but for all practical use cases its reasonable.
Compared to other terminals:
| Terminal | OSC capture limit | Cell codepoints |
| --- | ---: | ---: |
| Ghostty | 8 MiB | 65 |
| kitty | ~256 KiB ordinary | 24 |
| VTE | 4,096 scalars | 11 |
| xterm | 20 or 600 KB | 3 default, 6 max |
| Alacritty | unbounded | unbounded |
| WezTerm | unbounded | no explicit limit |