Fixes#13386, based on
https://github.com/mustafa0x/ghostty/commit/a8c090
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.
Most obvious next step in translating Ghostty is the command palette.
Added support for i18n.N_ (https://docs.gtk.org/glib/i18n.html#macros).
Made a Latvian translation for the command palette to test. Codex did
bulk of the translations but I verified them.
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>
the original wording is a bit confusing; I thought cursor-click-to-move
required shell-integration to be enabled, and was confused when the
mouse was still moving my cursor in fish even with
shell-integration=none.
#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.
Tested this with various settings and global keys working fine.
This PR speeds up our formatting (plain text, html, and VT) by anywhere
from ~1.5x to ~8x.
The formatter is the hot path behind multiple features in Ghostty GUI:
clipboard copy (plain/VT/HTML), `write_screen_file`, `selectionString`,
and terminal search sliding window. It's also the hot path for
libghostty users, namely people like
[zmx](https://github.com/neurosnap/zmx) which utilize the VT formatter
to restore a terminal.
This PR also adds the benchmarking infrastructure for the formatter.
## How
- **Fast cell-run optimization.** For simple cells (single codepoint, no
style/hyperlink) we encode them as a single run rather than one at a
time.
- **Make some arguments comptime.** Generates more code but benchmarks
show it improves things, specifically for per-format switches that we do
a LOT.
- **Interned style id fast path.** Styles are interned per page, so id
equality implies style equality. We track the id of the active style and
skip the per-cell `Style` copy + `eql` when it matches.
- **Fast printing.** Avoid `std.fmt` where possible and assemble
integers, RGB colors, codepoints in fixed-width buffers with a single
memcpy. This was extracted partially to `fastprint.zig` so we can reuse
it.
- **Avoid double-formatting for tracked pins.** Previously we formatted
twice (once through a `Discarding` writer to count bytes) for pin maps.
Now I'm smarter about it and do a single pass.
## Performance
All on my machine, 80x24 terminal, 10K lines of scrollback.
| workload | main | this PR | speedup | throughput |
| --------------------- | -------- | -------- | ------- | ---------- |
| plain / plain | 5.74 ms | 1.67 ms | 3.4x | 364 MB/s |
| plain / vt | 6.46 ms | 1.04 ms | 6.2x | 596 MB/s |
| plain / html | 7.39 ms | 2.32 ms | 3.2x | 308 MB/s |
| unicode / plain | 9.74 ms | 5.42 ms | 1.8x | 276 MB/s |
| unicode / vt | 10.42 ms | 5.53 ms | 1.9x | 275 MB/s |
| unicode / html | 12.53 ms | 7.35 ms | 1.7x | 509 MB/s |
| styled / plain | 5.65 ms | 1.69 ms | 3.4x | 360 MB/s |
| styled / vt | 9.07 ms | 4.20 ms | 2.2x | 409 MB/s |
| styled / html | 10.78 ms | 6.64 ms | 1.6x | 740 MB/s |
| mixed / plain | 8.59 ms | 4.81 ms | 1.8x | 226 MB/s |
| mixed / vt | 11.25 ms | 6.65 ms | 1.7x | 250 MB/s |
| mixed / html | 14.47 ms | 10.52 ms | 1.4x | 414 MB/s |
| wrapped / plain | 7.30 ms | 1.11 ms | 6.6x | 733 MB/s |
| wrapped / vt | 8.14 ms | 1.04 ms | 7.8x | 789 MB/s |
| wrapped / html | 9.00 ms | 2.12 ms | 4.2x | 465 MB/s |
| pin-map / plain | 12.51 ms | 3.80 ms | 3.3x | |
| pin-map / vt | 13.12 ms | 2.99 ms | 4.4x | |
| active screen / plain | 12.5 µs | 2.7 µs | 4.6x | |
| active screen / vt | 18.4 µs | 6.8 µs | 2.7x | |
Workloads:
- `plain` is ASCII lines
- `unicode` is 2/3/4-byte codepoints with 10% grapheme clusters
- `styled` is heavy SGR churn
- `mixed` is styles + Unicode + hyperlinks
- `wrapped` is a continuous soft-wrapped stream
- `pin-map`/`active screen` are the selectionString/search-style and
visible-screen-only cases respectively.
Finally, what was previously thought impossible, is now possible.
The blur region itself is far more accurate than what we can conjure up
on our own, and in a much more finetuned and detailed way too.
Thank you, GTK devs!
Closes#13581
#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.
Finally, what was previously thought impossible, is now possible.
The blur region itself is far more accurate than what we can conjure up
on our own, and in a much more finetuned and detailed way too.
Thank you, GTK devs!
GTK 4.23.3 added its own (much smarter) implementation of background blur,
which means our implementation is not only redundant, it also crashes the
program because a surface cannot have multiple associated blur objects.
Ergo, don't do custom blur on newer GTK versions.
See #13578
RunIterator allocated a list of font candidates for every
multi-codepoint grapheme, then scanned it for the first font covering
the entire cluster.
Instead, check the primary and additional font candidates as they're
discovered. This preserves their order while removing the temporary
array and avoids additional lookups when the primary font supports the
full grapheme.
RunIterator allocated a list of font candidates for every multi-codepoint
grapheme, then scanned it for the first font covering the entire cluster.
Instead, check the primary and additional font candidates as they're
discovered. This preserves their order while removing the temporary
array and avoids additional lookups when the primary font supports the
full grapheme.
Represent Kitty image data as a complete/pending tagged union. Kitty
images can now be completed _later_ if we have all their other metadata
up front.
This will be used by the snapshot API to transmit lightweight
information up front so that renderers of the snapshot can show
placeholders and accept mutating pty data, while the real image data
streams in later.
No user-visible behavior changes today.
Messages can own allocated data or a derived config. Some paths (writer
thread draining, mailbox shutdown with unread messages, and queue push
failures) discarded messages without releasing those resources.
This change adds Message.deinit and uses it whenever a message is
discarded.
Represent Kitty image data as a complete/pending tagged union.
Kitty images can now be completed _later_ if we have all their other
metadata up front.
This will be used by the snapshot API to transmit lightweight
information up front so that renderers of the snapshot can show
placeholders and accept mutating pty data, while the real image data
streams in later.
Messages can own allocated data or a derived config. Some paths (writer
thread draining, mailbox shutdown with unread messages, and queue push
failures) discarded messages without releasing those resources.
This change adds Message.deinit and uses it whenever a message is
discarded.
OSC 52 clipboard reads built their response in an allocated buffer and
then passed it through Message.writeReq, which allocated a second copy
for large responses.
Instead, transfer the allocated response directly using .write_alloc.
Small responses now retain their initial allocation until the IO thread
consumes them instead of being copied inline and freed immediately.
Their allocation count is unchanged, while large responses improve from
two allocations to one. Both cases avoid the additional copy.
OSC 52 clipboard reads built their response in an allocated buffer and
then passed it through Message.writeReq, which allocated a second copy
for large responses.
Instead, transfer the allocated response directly using .write_alloc.
Small responses now retain their initial allocation until the IO thread
consumes them instead of being copied inline and freed immediately.
Their allocation count is unchanged, while large responses improve from
two allocations to one. Both cases avoid the additional copy.
This PR improves the way splits/surfaces are sized in the GTK app, which
eliminates flickering and slightly improves performance.
Fixes#13328, #12709, #11187.
Related #8208 (closed) but some later comments mention flickering issues
persisting.
Builds on top of the changes in #12698.
Previously an idle callback was used to sync the split ratio between the
GTK widget tree and the split tree data structure that represents the
split layout. The widget tree contains a `SplitTreeSplit` widget, which
wraps a `GtkPaned` widget, for every split. During size allocation a
`GtkPaned` widget first computes the initial position of the divider and
thereby the size for its two children. We get notified of that position
(and the max possible position) via the `propPosition/propMaxPosition`
callbacks in `SplitTreeSplit` and set up an idle callback (the `onIdle`
function) to update the position if it does not match the desired split
ratio. Since the initial position is often not correct, especially in
nested layouts or if the ratio is not 0.5, a surface will first be shown
with the wrong size for a few frames until the idle callback runs and
corrects the sizing. In nested layouts it might take multiple rounds of
size allocation and idle callbacks until every surface gets the correct
size. This causes flickering as widgets eventually snap to another size,
which is especially noticeable if the layout changes quickly e.g. when
resizing a split using keybinds.
To fix this, the divider position will now be corrected directly from
the `propMaxPosition` callback, which runs during GTK size allocation,
right after a `GtkPaned` computes the initial position and right before
it uses the position to allocate sizes for its two children. With this
change every surface will be sized correctly during the first round of
size allocation.
The idle callback is still used to update the ratio in the split tree
when a split is resized by manually dragging the divider in the UI. The
logic to sync the split ratio was moved to the new `syncSplitRatio`
function which is called from both `propMaxPosition` and `onIdle`.
This is kind of hacky, but I reviewed the GTK source code in detail to
verify that this is safe (see the various code comments for more
details). I also tested extensively on both Hyprland and KDE Plasma:
creating deeply nested layouts, resizing with both keybinds and dragging
dividers by hand, with multiple tabs, resizing the entire window,
resizing entire subtrees to 0 and back. Everything seems to work fine.
For performance testing I used sysprof which can also collect GTK stats.
When creating/deleting/resizing splits I can measure a slight but
consistent increase in GTK FPS (+5 to 10) on my system. Other than that
CPU usage and FPS seem to be the same before and after. I guess this
makes sense, while we added a bit of work to the GTK loop during size
allocation, we avoid surfaces being resized.
For the flickering, here's a side-by-side comparison. Left is before the
changes, right is after.
https://github.com/user-attachments/assets/2a4f0b4b-e113-49b5-b0d7-d9e507a5a4ff
AI Disclosure: no AI was used.
Read-only filtering happens in Surface.queueIo after callers construct
the message. This early return leaked write_alloc payloads because the
IO thread never receives them and therefore does not perform its normal
cleanup.
Read-only filtering happens in Surface.queueIo after callers construct
the message. This early return leaked write_alloc payloads because the
IO thread never receives them and therefore does not perform its normal
cleanup.
This removes use of global state from the crash reporting functionality
(everything in `src/crash`).
This particularly ensures that there are no races on the system
environment during the execution of the initialization thread that would
possibly cause crashes, particularly in any (albeit unsupported) 3rd
party integrations of libghostty-internal.
Ultimately, this pushes any coupling of I/O and environment to places
that would more correctly interface with global state, such as the
same-thread `global.init`, and the crash report CLI.
Note that similar de-coupling actions have been taken on XDG and home
directory functionality, pushing their coupling points up the stack in a
similar way.
Document why incremental history decoding exposes native page finalization
errors and intentionally bypasses the one-shot ExistingHistory guard after READY.
SCREEN decoding restored saved cursor coordinates directly from the wire
even when they exceeded the current terminal dimensions, unlike the live
cursor restoration path.
Lenient style decoding previously caught every error, so PAGE and SCREEN
could treat truncation or an I/O failure as an invalid semantic style and
continue from a corrupted stream position.
Add a nullable decoder that discards only invalid style contents while
propagating reader failures. Update snapshot callers and cover both semantic
fallback and structural failure behavior.
Decoded hyperlink table entries retained their insertion reference after
the grid added its per-cell references. Overwriting all linked cells could
therefore leave unused entries alive indefinitely.
Release each accepted wire table entry after grid decoding, including
duplicate values that map to one native ID. Regression coverage verifies
exact cell ownership and reaping after overwrite.
Grapheme suffix decoding accepted U+0000 even though zero is the native
empty-cell sentinel. It also appended codepoints one at a time and, when
page capacity failed after a prefix had been stored, left that truncated
prefix attached to the cell. Hostile snapshots could therefore introduce
invalid cluster data or render a partial cluster depending on allocator
capacity.
Ignore NUL alongside invalid scalar values. If any append runs out of
native capacity, remove the prefix already attached and consume the rest
of the declared suffix without applying it, making delivery atomic at the
cluster level. Cover NUL input and a failure after 128 accepted suffix
codepoints.
PAGE decoding inserted every valid style table entry into the native
ref-counted set before decoding cells. That insertion contributed one
reference in addition to every cell reference, unlike organically built
pages where the initial add belongs to the first cell. An unused encoded
style therefore remained live with refcount one and was emitted again on
every re-encode; used styles were also permanently over-counted.
After the grid has installed all cell references, release the temporary
table-owned reference once per distinct live style. Unused styles become
dead immediately and used styles retain exactly their cell count. Cover
used reference counts, unordered sparse IDs, and canonical first
re-encoding of an unused entry.
SCREEN encoding assumed every page from the active boundary onward was
resident. A debug assertion guarded that PageList policy invariant, but
release builds immediately used pageAssumeResident. If compression policy
ever allowed a SCREEN suffix page to remain compressed, the encoder would
read an inactive union field, causing undefined behavior and potentially
a crash or corrupt snapshot.
Use pagePreservingState for every SCREEN suffix page, as HISTORY already
does, and include allocation failure in EncodeError. Resident pages remain
a zero-allocation borrow while compressed pages decode into temporary
read-only storage without changing the source representation. Exercise the
path with an explicitly compressed active suffix page.
Decoder.next treated calls before READY and calls after any prior decode
error as unreachable. Network or mux glue that retried after a truncated
history record, or invoked next before setup completed, could therefore
turn a recoverable protocol misuse into a process panic.
Add DecoderNotReady and DecoderFailed to NextError and return them for
the start and failed states. Keep finished calls idempotent, and cover
both an early call and a retry after FINISH truncation.
SCREEN decode clamped the cursor x coordinate to the physical page
width, but validated pending wrap against the terminal-wide column
count. A lazily reflowed page narrower than the current terminal could
therefore lose a valid pending-wrap state at its last physical column.
The next write would continue on the same row instead of wrapping.
Validate pending wrap against the cursor page width, matching the clamp
and the page-local cursor pin. Add a mixed-width decode regression that
places the cursor at the narrow page boundary.
This removes use of global state from the crash reporting functionality
(everything in src/crash).
This particularly ensures that there are no races on the system
environment during the execution of the initialization thread that would
possibly cause crashes, particularly in any (albeit unsupported) 3rd
party integrations of libghostty-internal.
Ultimately, this pushes any coupling of I/O and environment to places
that would more correctly interface with global state, such as the
same-thread global.init, and the crash report CLI.
Note that similar de-coupling actions have been taken on XDG and home
directory functionality, pushing their coupling points up the stack in a
similar way.
This adds a new `terminal.snapshot.Decoder` that allows for incremental
decoding of a snapshot stream. There are two methods: `ready` builds up
the entire terminal up to READY. Then `next` acts like a Zig iterator
and applies incremental history as it becomes available. In between
calls to `ready` and `next` the caller can do whatever.
The use case for this: with a 1MB ascii stream, the time to decode to
READY is ~40us on my machine, versus 1.5ms for the entire history. This
means that a terminal could be rendered and visible after 40us rather
than waiting for the full terminal. This isn't a large terminal, but
that READY time should be pretty standard since screens don't get that
big, but history is unbounded.