Commit Graph

9770 Commits

Author SHA1 Message Date
Mitchell Hashimoto
760a250029 config: formatted action should be parsable into the original (#13609)
This fixes the issue where an action with string as it's parameter is
not working correctly in CommandPalette, found in #9671. For example:

```
command-palette-entry = title:"Set Ghostty Title",description:test sending text.,action:set_tab_title:👻
keybind=cmd+r=set_tab_title:👻
```

Keybind works perfectly, but the title is escaped when triggering in
CommandPalette.

> Introduced in
[#8873](https://github.com/ghostty-org/ghostty/pull/8873/changes#diff-9e7936787320bcf70e332c868125039d8c0a7f96c4a88f2af0af21d952c6830dR1216),
I tested the fixed issue as well, the following config still parses
correctly, mentioned in
https://github.com/ghostty-org/ghostty/issues/8849#issuecomment-3322018212.

```
command-palette-entry = title:Focus Split: Next,description:"Focus the next split, if any.",action:goto_split:next
```

Also `ghostty +show-config` now will also output the readable strings as
well.
<img width="1078" height="428" alt="image"
src="https://github.com/user-attachments/assets/f9dc1447-7b4e-44f4-8362-b54f4d805c7a"
/>
2026-08-04 11:19:37 -07:00
Lukas
b67f8ef51d config: don't escape Binding.Action.String 2026-08-04 19:49:37 +02:00
Lukas
8cfbaf545a config: formatted action should be parsable into the original 2026-08-04 19:49:37 +02:00
Uzair Aftab
02f34835ea datastruct: remove unused LRU implementation 2026-08-04 19:46:26 +02:00
Mitchell Hashimoto
48d85eaeb0 core: fix mouse reporting mutex lock 2026-08-04 09:10:14 -07:00
Mitchell Hashimoto
ca56412bf2 gtk: forward middle click to TUIs with mouse reporting (#13108)
Fix for Issue #12940 
I actually do not know if this has already been resolved and the issue
is just still open. Either way, here's a fix. Now we run a check to see
if the current program is accepting mouse events before discarding the
middle click.
2026-08-04 08:53:39 -07:00
Mitchell Hashimoto
363e6e6b42 i18n: translation support for command palete (#11641)
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.
2026-08-04 06:43:38 -07:00
Jon Parise
1f6e26642e config: clarify cursor-click-to-move's relation to shell-integration (#13589) 2026-08-04 08:54:06 -04:00
Lauri Tirkkonen
85083d23cd config: clarify cursor-click-to-move's relation to shell-integration
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.
2026-08-04 20:59:52 +09:00
Ēriks Remess
1125fa26df i18n: note about i18n.N_ usage and @inComptime() return msgid for i18n._ 2026-08-04 11:04:29 +03:00
Ēriks Remess
df23bef0e9 i18n: translation support for command palete and Latvian translation for it 2026-08-04 11:04:29 +03:00
Mitchell Hashimoto
b9d88292be terminal: speed up formatting anywhere from ~1.5x to ~8x (#13587)
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.
2026-08-03 20:56:46 -07:00
Mitchell Hashimoto
e69dc2bee8 renderer: reset terminal state cleanup counter (#13585)
Reset the frame counter whenever retained render state is cleared.
Otherwise, every subsequent frame will be deinitialized and rebuilt.
2026-08-03 20:55:47 -07:00
Mitchell Hashimoto
74b426458b gtk: use native blur on GTK 4.23.3+ (#13586)
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
2026-08-03 20:55:14 -07:00
Mitchell Hashimoto
2ed67cadd1 terminal: redesign pin map for formatter 2026-08-03 20:50:33 -07:00
Mitchell Hashimoto
d4391ff835 fastprint: fix compile errors 2026-08-03 20:10:19 -07:00
Mitchell Hashimoto
79aa256fa2 terminal: speed up formatter mostly by avoiding std.fmt 2026-08-03 19:59:28 -07:00
Mitchell Hashimoto
8838c37f4c terminal: fast print styles 2026-08-03 19:55:54 -07:00
Mitchell Hashimoto
85b1dd0dd9 benchmark: formatter benchmark 2026-08-03 19:50:35 -07:00
Mitchell Hashimoto
b11d60818a synthetic: styled output generator 2026-08-03 19:50:20 -07:00
Leah Amelia Chen
3263fc6c4b gtk: use native blur on GTK 4.23.3+
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!
2026-08-04 07:16:18 +08:00
Leah Amelia Chen
04f1bc0960 winproto/wayland: disable custom blur on GTK >=4.23.3
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
2026-08-04 07:10:01 +08:00
Jon Parise
9e6e2ea964 renderer: reset terminal state cleanup counter
Reset the frame counter whenever retained render state is cleared.
Otherwise, every subsequent frame will be deinitialized and rebuilt.
2026-08-03 19:07:42 -04:00
Jon Parise
ca8868a295 font/shaper: eliminate grapheme candidate allocations
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.
2026-08-03 18:22:23 -04:00
Mitchell Hashimoto
7e50356642 terminal: support pending image payloads for kitty graphics (#13582)
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.
2026-08-03 13:28:24 -07:00
Mitchell Hashimoto
5700414f14 libghostty-vt: add C API for snapshotting functions (#13580)
Expose terminal snapshot through the libghostty-vt C API and add a new C
example that runs in CI to verify this stuff works!

## Example

```c
// Enable PTY continuation tracking
size_t continuation_limit = 1024;
assert(ghostty_terminal_set(
    terminal,
    GHOSTTY_TERMINAL_OPT_CONTINUATION_MAX_BYTES,
    &continuation_limit) == GHOSTTY_SUCCESS);

// Encode a terminal with heap allocation
uint8_t *bytes = NULL;
size_t len = 0;
assert(ghostty_snapshot_encode_alloc(
    terminal, NULL, &bytes, &len) == GHOSTTY_SUCCESS);

// Full blocking decode from an owned buffer. 
GhosttySnapshotDecoder decoder = NULL;
assert(ghostty_snapshot_decoder_new_buf(
    NULL, &decoder, bytes, len) == GHOSTTY_SUCCESS);

GhosttyTerminal restored = NULL;
assert(ghostty_snapshot_decoder_decode(
    decoder, &restored) == GHOSTTY_SUCCESS);

ghostty_snapshot_decoder_free(decoder);
ghostty_free(NULL, bytes, len);
```

Streaming decode:

```c
// Streaming decoder from a custom reader IO function.
GhosttyReader reader = {
    .read = read_snapshot,
    .userdata = source,
};
GhosttySnapshotDecoder decoder = NULL;
assert(ghostty_snapshot_decoder_new(
    NULL, &decoder, reader) == GHOSTTY_SUCCESS);

// Read up to the ready state (when we can render and start processing pty bytes)
GhosttyTerminal terminal = NULL;
assert(ghostty_snapshot_decoder_ready(
    decoder, &terminal) == GHOSTTY_SUCCESS);

// Sometime later or async process remaining frames.
GhosttyResult result;
while ((result = ghostty_snapshot_decoder_next(decoder)) ==
       GHOSTTY_SUCCESS) {
  size_t rows = 0;
  assert(ghostty_snapshot_decoder_get(
      decoder,
      GHOSTTY_SNAPSHOT_DECODER_DATA_PROGRESS_ROWS,
      &rows) == GHOSTTY_SUCCESS);
  render(terminal);
}
assert(result == GHOSTTY_NO_VALUE);
```
2026-08-03 13:28:13 -07:00
Mitchell Hashimoto
6760c6482b terminal: support pending image payloads for kitty graphics
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.
2026-08-03 13:11:28 -07:00
Mitchell Hashimoto
d7bb4b8639 libghostty-vt: add C API for snapshotting functions
Expose terminal snapshot through the libghostty-vt C API and add
a new C example that runs in CI to verify this stuff works!

## Example

```c
size_t continuation_limit = 1024;
assert(ghostty_terminal_set(
    terminal,
    GHOSTTY_TERMINAL_OPT_CONTINUATION_MAX_BYTES,
    &continuation_limit) == GHOSTTY_SUCCESS);

uint8_t *bytes = NULL;
size_t len = 0;
assert(ghostty_snapshot_encode_alloc(
    terminal, NULL, &bytes, &len) == GHOSTTY_SUCCESS);

GhosttySnapshotDecoder decoder = NULL;
assert(ghostty_snapshot_decoder_new_buf(
    NULL, &decoder, bytes, len) == GHOSTTY_SUCCESS);

GhosttyTerminal restored = NULL;
assert(ghostty_snapshot_decoder_decode(
    decoder, &restored) == GHOSTTY_SUCCESS);

ghostty_snapshot_decoder_free(decoder);
ghostty_free(NULL, bytes, len);
```

Streaming decode:

```c
GhosttyReader reader = {
    .read = read_snapshot,
    .userdata = source,
};
GhosttySnapshotDecoder decoder = NULL;
assert(ghostty_snapshot_decoder_new(
    NULL, &decoder, reader) == GHOSTTY_SUCCESS);

GhosttyTerminal terminal = NULL;
assert(ghostty_snapshot_decoder_ready(
    decoder, &terminal) == GHOSTTY_SUCCESS);

GhosttyResult result;
while ((result = ghostty_snapshot_decoder_next(decoder)) ==
       GHOSTTY_SUCCESS) {
  size_t rows = 0;
  assert(ghostty_snapshot_decoder_get(
      decoder,
      GHOSTTY_SNAPSHOT_DECODER_DATA_PROGRESS_ROWS,
      &rows) == GHOSTTY_SUCCESS);
  render(terminal);
}
assert(result == GHOSTTY_NO_VALUE);
```
2026-08-03 13:09:04 -07:00
Jon Parise
2f7fbadb0b termio: free resources for discarded messages
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.
2026-08-03 15:28:01 -04:00
Mitchell Hashimoto
ac04fc2761 core: avoid copying OSC 52 clipboard responses (#13577)
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.
2026-08-03 10:40:18 -07:00
Jon Parise
c11fe5486f core: avoid copying OSC 52 clipboard responses
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.
2026-08-03 13:24:38 -04:00
Jeffrey C. Ollie
da581e0fb7 gtk: improve split sizing (#13414)
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.
2026-08-03 11:39:54 -05:00
Mitchell Hashimoto
7d748097a0 core: free allocated writes in read-only mode (#13574)
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.
2026-08-03 09:14:59 -07:00
Jon Parise
957ed21d5c core: free allocated writes in read-only mode
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.
2026-08-03 11:42:11 -04:00
Mitchell Hashimoto
863fc9531a terminal/snapshot: more misc bugs (#13573)
Again nothing critical, just some polish around the edges.
2026-08-03 08:20:43 -07:00
Mitchell Hashimoto
5c65304a27 crash: do not use global state (#13567)
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.
2026-08-03 08:13:05 -07:00
Mitchell Hashimoto
7d9aaa2970 terminal/snapshot: clarify incremental history errors
Document why incremental history decoding exposes native page finalization 
errors and intentionally bypasses the one-shot ExistingHistory guard after READY.
2026-08-03 08:11:01 -07:00
Mitchell Hashimoto
cafe7d5da4 terminal/snapshot: clamp decoded saved cursors
SCREEN decoding restored saved cursor coordinates directly from the wire 
even when they exceeded the current terminal dimensions, unlike the live 
cursor restoration path.
2026-08-03 08:10:45 -07:00
Mitchell Hashimoto
f99896bf8c terminal/snapshot: preserve style reader errors
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.
2026-08-03 08:10:31 -07:00
Mitchell Hashimoto
b5290e74c4 terminal/snapshot: release decoded hyperlink table refs
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.
2026-08-03 08:08:51 -07:00
Mitchell Hashimoto
418b5d1805 terminal/snapshot: harden grapheme suffix decode
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.
2026-08-03 06:45:09 -07:00
Mitchell Hashimoto
9a5279db68 terminal/snapshot: release decoded style table refs
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.
2026-08-03 06:45:09 -07:00
Mitchell Hashimoto
e89ff37aa8 terminal/snapshot: encode screen pages safely
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.
2026-08-03 06:45:09 -07:00
Mitchell Hashimoto
cbc9f360b1 terminal/snapshot: report invalid decoder states
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.
2026-08-03 06:45:09 -07:00
Mitchell Hashimoto
d148471838 terminal/snapshot: preserve mixed-width pending wrap
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.
2026-08-02 20:46:38 -07:00
Chris Marchesi
15c50c1db1 crash: do not use global state
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.
2026-08-02 20:24:30 -07:00
Mitchell Hashimoto
e37865bedc terminal/snapshot: add incremental decoder
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.
2026-08-02 19:54:36 -07:00
Mitchell Hashimoto
d351d9ce07 terminal/snapshot: more efficient binary form, optimize wire size + encode/decode speeds (#13566)
Reworks the terminal PAGE grid wire format and optimize both
encode/decode. Example improvements for 1MB of VT input w/ full
scrollback: ~30x smaller wire size, ~45x faster encoding and decoding.

> [!IMPORTANT]
>
> **Snapshot version 1 is still explicitly a work-in-progress format, so
this breaks wire compatibility**.

The original snapshot version I merged favored simplicity over
optimization. This was the format used a proof-of-concept in my own
projects, but I knew it wasn't what I wanted to ship. This PR looks at
the record formats and trades simplicity for performance, a fair trade
for a performance-sensitive binary format.

Overview of changes:

- **8-byte grid cells.** Cells are now one 64-bit word whose layout
deliberately coincides with the native cell. Previously, cells were 16
bytes each and in our 1MB corpus 97% of the data was `0`. Lol.
- **Blank trailing cells are not written.** Rows declare an encoded cell
count so trailing blank cells cost nothing.
- **Hardware CRC32C.** Added `src/crc32c.zig` that uses inline-asm on
aarch64/x86_64 to get hardware speeds for CRC32. Zig's stdlib is 0.56
GB/s, aarch64 hardware is 10 GB/s on my computer.
- **Variable-width cells.** Each row declares how many bytes transport
each cell word: 1, 2, 4, or 8 depending on the widest row cell.

## Format

Grid layout, per PAGE record:

```
   old                                new
   +--------------------------+      +--------------------------+
   | row 0                    |      | row 0                    |
   |   flags (1)              |      |   flags + width (1)      |
   |   cols * 16B cells with  |      |   encoded cell count (2) |
   |   inline suffixes        |      |   count * width cells    |
   +--------------------------+      +--------------------------+
   | ...                      |      | ...                      |
   +--------------------------+      +--------------------------+
   | row (rows - 1)           |      | row (rows - 1)           |
   +--------------------------+      +--------------------------+
                                     | grapheme suffix section  |
                                     +--------------------------+
```

Every row previously carried exactly `cols` cells; now it carries cells
only through its last non-default cell, and the cells past the count are
implicitly zero. The row flag byte gains the encoded cell width in its
previously reserved bits:

```
   bit 0 wrap                 bit 2-3 semantic prompt
   bit 1 wrap continuation    bit 4-5 encoded cell width (log2 bytes)
```

The cell itself, old fixed 16-byte header versus the new single word:

```
   old (16 bytes + inline suffixes)     new (one u64 word)
   +--------+---------+--------+        bit  0 +------------------+
   | kind 1 | width 1 | flags 1|               | content kind  2b |
   +--------+---------+--------+        bit  2 +------------------+
   | zero 1 | style id 2       |               | content      24b |
   +--------+------------------+        bit 26 +------------------+
   | hyperlink id 2            |               | style ID     16b |
   +---------------------------+        bit 42 +------------------+
   | value 4                   |               | width kind    2b |
   +---------------------------+        bit 44 +------------------+
   | grapheme count 4          |               | protected     1b |
   +---------------------------+        bit 45 +------------------+
   | grapheme cps 4 * count    |               | hyperlink     1b |
   +---------------------------+        bit 46 +------------------+
                                               | semantic      2b |
                                        bit 48 +------------------+
                                               | hyperlink ID 16b |
                                        bit 64 +------------------+
```

The word's bit layout intentionally matches the native cell (with the
wire hyperlink ID in the native padding), so full-width rows are a
straight copy of page memory. The row's encoded width then transports
each word truncated, and decode is the matching zero-extension:

```
   width | bytes | admitted cells
   ------+-------+------------------------------------------------
     0   |   1   | codepoint <= U+00FF, nothing else set
     1   |   2   | codepoint <= U+FFFF, nothing else set
     2   |   4   | any content kind/codepoint, style IDs 1-63,
         |       | narrow, no flags, no hyperlink
     3   |   8   | everything
```

Grapheme suffixes were inline after each cell, which forced per-cell
framing decisions; they are now one section after the rows, so a
grapheme-free page (the overwhelming case) pays 4 bytes total:

```
   old: ... | cell | cp cp | cell | ...      (inline, per cell)

   new: +----------------+----------------------------------+
        | entry count 4  | entries: row 2, col 2, count 2,  |
        |                |          count * codepoint 4     |
        +----------------+----------------------------------+
```

## Performance

Setup: `ghostty-bench +terminal-snapshot`, 80x24 terminal with unlimited
scrollback fed 1 MB of VT input.

Per-commit improvements:

| change                        | wire size | encode  | decode   |
|-------------------------------|-----------|---------|----------|
| baseline (v1 before this PR)  | 34.16 MB  | 92.8 ms | 119.8 ms |
| 8-byte cells + blank elision  | 7.66 MB   | 18.2 ms | 28.0 ms  |
| hardware CRC32C               | 7.66 MB   | 5.8 ms  | 15.5 ms  |
| gate page verification        | 7.66 MB   | 5.8 ms  | 12.2 ms  |
| staged PAGE payload decoding  | 7.66 MB   | 5.8 ms  | 8.1 ms   |
| variable-width cells          | 1.03 MB   | 2.0 ms  | 2.7 ms   |

Final result across various inputs:

| corpus | wire size | encode | decode |

|----------------------------|------------------------|----------------------|-----------------------|
| ascii lines 1-70 | 34.16 -> 1.03 MB (33x) | 92.8 -> 2.0 ms (46x) |
119.8 -> 2.7 ms (44x) |
| ascii full-width wrap | 16.01 -> 1.04 MB (15x) | 43.6 -> 1.3 ms (34x)
| 56.2 -> 1.8 ms (31x) |
| utf8 (wide/grapheme heavy) | 4.33 -> 1.89 MB (2.3x) | 12.4 -> 1.7 ms
(7x) | 19.0 -> 2.2 ms (9x) |

### Relationship with Compression

I expect that users of this will wrap everything in compression, so I
also benchmarked all my changes against a caller-owned zstd compressor
to ensure we're making the write tradeoffs. Less bytes means less time
in a compressor, even if a ton of 0s compresses really well.

My results: `zstd -1` over the `lines` snapshot drops from 12.7 ms to
0.8 ms, and the compressed artifact shrinks from 1.35 MB to 0.86 MB. So
the end state is a win-win.
2026-08-02 15:01:00 -07:00
Mitchell Hashimoto
9e3019f190 terminal/snapshot: variable-width grid cell encoding
Add a per-row encoded cell width to the PAGE grid format. Rows
previously always spent eight bytes per cell, but a plain text cell
carries only a codepoint: on line-shaped scrollback most encoded
bytes were predictable zeros that still had to pass through CRC32C,
BLAKE3, both codecs, and any transport compression the caller
applies.

Each row now declares one of four widths in previously reserved row
flag bits, chosen canonically as the smallest width admitted by the
bitwise OR of the row cell words: one or two bytes transport a bare
codepoint, four bytes transport the low word half (any content kind,
style IDs up to sixty-three, no wide or flag or hyperlink bits), and
eight bytes remain the full word. Every width is a truncation on
encode and a zero-extension on decode, so narrow rows encode and
decode as vectorizable integer loops, one and two byte rows need at
most surrogate replacement and skip cell normalization entirely, and
full-width rows keep the existing bulk copy. Decoders use the
declared width for framing and accept rows encoded wider than
necessary. Rows containing wide characters, hyperlinks, semantic
content, or large style IDs still use the full width, which leaves
CJK-heavy content unchanged.

Benchmark deltas at this commit (terminal-snapshot, M-series,
ReleaseFast, 1 MB corpora):

  ascii lines 1-70:  7.66 MB -> 1.03 MB (7.4x)
                     encode 5.8 -> 2.0 ms, decode 8.1 -> 2.7 ms
  ascii full-wrap:   8.04 MB -> 1.04 MB (7.7x)
                     encode 5.4 -> 1.3 ms, decode 7.2 -> 1.8 ms
  utf8:              unchanged (wide cells keep rows at full width)

For a caller compressing the stream, the lines snapshot end to end
with zstd -1: encode plus compress 18.5 -> 2.8 ms, decompress plus
decode 15.8 -> 3.6 ms, and the compressed size itself drops from
1.35 MB to 0.86 MB because the packed stream is denser for the
entropy coder.
2026-08-02 14:32:48 -07:00
Mitchell Hashimoto
3e5d128353 terminal/snapshot: stage PAGE payloads while decoding
PAGE payloads were decoded through a stack of stream adapters:
a CRC32C-hashing reader over a length-limited reader over the
BLAKE3-hashing snapshot reader. Every row paid several adapter
crossings and both hashes were fed row-sized chunks, which kept
BLAKE3 out of its efficient many-block path and made adapter
overhead about a quarter of decode time.

Decode now reads the remaining payload into a scratch buffer with
one bulk read, so each hash sees the payload as a single update, and
then parses the tables and grid from a flat in-memory reader. Row
headers are also read as one three-byte read instead of two calls.
Staging is capped at 8 MiB, far above any standard-capacity page
payload, so a hostile declared length cannot force a large
allocation; larger payloads fall back to the streaming path. CRC
validation and exact-exhaustion checks are unchanged, with the
staged reader checked for leftover bytes to preserve
PayloadNotExhausted semantics.

Benchmark deltas at this commit (terminal-snapshot, 1 MB corpora):

  ascii lines 1-70:  decode 12.2 -> 8.1 ms (encode unchanged)
  ascii full-wrap:   decode 11.1 -> 7.2 ms
  utf8:              decode  3.1 -> 2.1 ms

Relative to the previous wire format and codecs, the series is a
16.0x encode and 14.8x decode improvement on line-shaped scrollback
at 4.5x smaller wire size.
2026-08-02 14:32:27 -07:00