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.
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.
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.
#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.
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 |
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.
Gate the response in the shared terminal stream, append the C API
option without renumbering existing values, and cover the default,
opt-in, and reset behavior in Zig and C API tests.
Handle VS15 width changes when the wide grapheme base is directly under
the cursor.
A zero cursor distance previously underflowed while locating the spacer
tail. Debug builds panicked and ReleaseFast computed an out-of-bounds
cell pointer before updating it.
Closes#12505
This PR allows Ghostty to respond to DECRQM queries for DECECM with the
"permanently reset".
AI disclosure: I used Codex to help inspect the relevant code path and
explain the issue, but I reviewed and made the code changes myself.
Handle VS15 width changes when the wide grapheme base is directly under
the cursor. Cover both disabled wraparound and restored pending-wrap
cursor states.
A zero cursor distance previously underflowed while locating the spacer
tail. Debug builds panicked and ReleaseFast computed an out-of-bounds
cell pointer before updating it.
Find the spacer from the wide base instead of subtracting from the
cursor distance. Reposition the cursor from the base column and clamp it
to the active right margin.
Reclaim pin-backed Kitty graphics placements after their tracked screen
content is pruned. Treat garbage pins as non-renderable until the next
placement command sweeps them.
Placements that scrolled beyond retained history previously remained in
the placement map and tracked-pin set. Long-running graphics output could
accumulate stale state, and remapped garbage pins could appear at an
unrelated fallback location.
Sweep garbage placements before growing the placement map, releasing each
tracked pin while preserving virtual placements. Return no geometry or
visible render position for garbage pins and cover both storage and C API
behavior with regression tests.
Release a Kitty graphics placement's tracked pin before replacement.
Repeated updates to an external placement previously leaked tracked pins.
Pass the owning screen to storage and deinitialize the old placement.
Limit individual allocator requests made by PNG decoders to the Kitty
graphics protocol's 400 MiB image ceiling. Add a reusable allocator
wrapper for callers that need per-request bounds.
PNG decoding previously used Wuffs' 4 GiB package limit and checked
the result only after allocation. A tiny PNG with oversized dimensions
could cause a multi-gigabyte RSS spike before being rejected.
Wrap decoder allocators with LimitedAllocator and translate limit
rejections to invalid image data while preserving genuine out-of-memory
errors. Add allocator boundary tests and regression coverage for a
crafted PNG below Wuffs' limit.
Validate Kitty shared memory byte ranges before mapping and copying
image data. Interpret S as a byte count from O and preserve default
raw-image sizing.
Shared memory transmissions previously multiplied untrusted u32
dimensions before the limit check and sliced mappings with an unchecked
offset. Malformed commands could panic in safe builds or request a
wrapped allocation in fast builds.
Reject oversized dimensions before widening size arithmetic, derive
bounded ranges from the stat size, and enforce max_size before
constructing a slice. Add regression tests for explicit and implicit
offsets, out-of-bounds offsets, and maximum dimensions.
Validate Kitty file transmissions against a canonical path derived from
the open file handle. Keep temporary file policy and cleanup keyed to
that handle path.
Path validation previously ran before opening, so a local cooperating
process could replace a symlink or directory entry and make Ghostty
read a blocklisted file.
Open the submitted path once, derive its canonical path from the handle,
and use the same handle for stat and reads. Add a regression test that
replaces a blocked symlink after open and verifies the pinned target is
still rejected.
Treat Kitty placement dimensions and offsets as untrusted values when
calculating pixel, grid, and rectangle geometry. Saturate results that
do not fit and return no rectangle when missing pixel metrics produces
an empty grid.
Unchecked u32 arithmetic previously panicked in safe builds and wrapped
in fast builds. A zero row count could underflow into a maximum-size
page traversal, while maximum dimensions could spin cursor movement or
overflow render visibility calculations.
Use checked integer scaling instead of floating-point casts, saturating
arithmetic for extents and cursor columns, and bound off-screen cursor
work to the terminal row count. Compute C API visibility in i64 and
cover maximum protocol values in storage, execution, and render-info
tests.
Require temporary image file paths to match complete directory
components when checking /tmp, /dev/shm, the configured temporary
directory, and its resolved path.
The previous byte-prefix checks accepted similarly named sibling
directories such as /tmpX. A temporary-file transmission could read
and unlink a file outside the permitted temporary directories.
Add a component-boundary helper and regression coverage for built-in
and configured directory prefixes. An integration test also verifies
that a rejected file remains on disk.
Use inclusive image ID bounds for the Kitty graphics protocol range
delete operation.
Range deletion previously joined the lower and upper bound checks with or,
which matched every placement for any valid range. A targeted delete could
therefore remove every graphics placement.
Join the bounds with and and update the lowercase and uppercase range tests
to keep placements below and above the selected interval.
While doing some work on my tmux fork I noticed multiple parts of
libghostty-vt was slower than tmux equivalents(isolated). Turns out they
do some smart stuff there.
printRepeat called print() once per repeat, so something like \x1b[2000b
ran grapheme checks, width lookups, wrap handling, etc etc 2000 times.
printSlice is already documented as semantically identical to
calling print per codepoint, so this just feeds the repeated
codepoint through it in 4096-entry stack chunks. Simple runs take
the batched fast path, and anything that needs care falls back to the
previous behaviour.
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.
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.
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 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.
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.
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.
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.
PAGE decoding verified the complete native integrity of every decoded
page unconditionally, building per-cell reference maps that accounted
for roughly a fifth of decode time. The decoder normalizes every
semantic value while decoding, so a completed decode upholds page
invariants by construction and the verification only defends against
decoder bugs. Follow the native page policy instead: assertIntegrity
and friends run full verification only when slow runtime safety is
enabled, which keeps the check in debug and test builds where those
bugs are caught.
Benchmark deltas at this commit (terminal-snapshot, 1 MB corpora):
ascii lines 1-70: decode 15.5 -> 12.2 ms (encode unchanged)
Rework the PAGE grid encoding for codec speed and size. This is a
breaking change to the work-in-progress version 1 wire format.
Cells were previously a fixed 16-byte header plus inline grapheme
suffixes: one byte each for content kind, width, and flags, a
reserved byte, 16-bit style and hyperlink IDs, a 32-bit value, and an
always-present 32-bit suffix count that was almost always zero. Cells
are now one 64-bit little-endian word with a documented bit registry
that carries the hyperlink ID in its high bits. The layout
deliberately coincides with the native cell so clean rows encode as a
straight copy of page memory and decode as one bulk read plus an
in-place normalization pass; a comptime check falls back to a
portable field-by-field codec if the native layout ever diverges.
Each row header also gains an encoded cell count so trailing default
cells are elided instead of spending 16 bytes apiece encoding
nothing: on typical shell output most of every row is blank, and
measurement showed 97% of encoded snapshot bytes were zero. Grapheme
suffixes move out of the cell stream into a per-grid section of
(row, column, codepoints) entries, which keeps row decoding
fixed-stride and bulk-copyable.
Decode ID remapping switches from hash maps to direct-indexed tables
sized by the 16-bit encoded ID space, removing per-styled-cell hash
lookups.
Benchmark deltas at this commit (terminal-snapshot, M-series,
ReleaseFast, 1 MB corpora):
ascii lines 1-70: 34.16 MB -> 7.66 MB (4.5x)
encode 92.8 -> 18.2 ms, decode 119.8 -> 28.0 ms
ascii full-wrap: 16.01 MB -> 8.04 MB (2.0x)
encode 43.6 -> 18.4 ms, decode 56.2 -> 25.6 ms
utf8: 4.33 MB -> 1.90 MB (2.3x)
encode 12.4 -> 4.9 ms, decode 19.0 -> 9.6 ms
OSC 52 clipboard writes decoded their base64 payload with the scalar
std implementation while Kitty graphics payloads already used the SIMD
decoder in src/simd.
Move clipboard path to use the same SIMD decoder. The encode side
of the read reply stays scalar since the codebase has no SIMD
encoder. 3.3x faster on a 4KB-payload decode micro-benchmark.
In the html formatter every page is formatted in a div. When the div
closes it causes a newline in the html rendering. In order to fix this a
newline is now removed whenever the div is closed (if there are any
newlines waiting to be rendered - as far as I could see in my testing
there was always one).
I thought of trying to add a test but could not think of a way to do so
without adding a massive blob of html into the file.
Before (orange was added by me to show where the div ends):
<img width="1278" height="586" alt="image"
src="https://github.com/user-attachments/assets/473ba28d-bec0-481f-9a89-a6a72c9a3657"
/>
After:
<img width="959" height="439" alt="image"
src="https://github.com/user-attachments/assets/27bae0d3-cc82-4dc8-aa72-c4a8b0f7d424"
/>
No AI was used in this pr.
Builds on #13544
This adds a new CONTINUATION record type that is sent before READY.
CONTINUATION contains the bytes (if any) that will bring a ground-state
VT state machine up to the same state.
This allows snapshotting a terminal instance that is, for example,
blocked waiting for a caller to complet an in-flight Kitty graphics
protocol send. In practice, I think this will be rare. But in theory, it
avoids a DoS-type attack.
The continuation state must be the MINIMAL set of bytes that will move
the virtual terminal state from a ground to non-ground state. The reason
it must be minimal is because any extra bytes can duplicate work into the
terminal that might already exist.
This adds opt-in continuation tracking to `terminal.Stream` that allows
any caller to call `writeContinuation` in order to get the minimum bytes
necessary from a grounded parser state to the identical state.
This enables reliable stream restart across serialization states, which
could be used for local restart, networked terminals, etc. For me, this
is used for multiplexers. :)
## Implementation
The implementation of this was really carefully done to avoid any
negative performance impact particularly when continuation tracking is
_off_.
The way this work is simple:
1. ESC is the only char that leaves the ground state and most
ESC sequences are short. So if we're in a non-ground state, we
do a backwards vectorized search to find the last `ESC` in the
input slice. If one doesn't exist, we assume we found it previously
and store the whole slice (rare, since ESC sequences are usually
short like I said).
2. If we're in the ground state that means we only have a potential
incomplete UTF-8 codepoint, so we find the lead UTF-8 byte.
3. When writing, we normalize the suffix to drop things like BEL
commands that would've already been handled to avoid
double-calling.
## Performance
Via `ghostty-bench +terminal-stream`
Corpus main tracking off tracking on
plain ASCII (256 MiB) 175.4ms 175.8ms 175.5ms
UTF-8 (32 MiB) 268.4ms 270.0ms 269.9ms
5% invalid UTF-8 (32 MiB) 316.4ms 317.8ms 320.2ms
CSI-heavy (32 MiB) 145.0ms 146.3ms 145.9ms
OSC (32 MiB) 1621.0ms 1627.5ms 1638.3ms
Kitty APC (128 MiB) 95.5ms 96.9ms 96.3ms
mixed traffic (32 MiB) 172.4ms 172.0ms 172.3ms
giant APC (128 MiB) 38.3ms 38.3ms 40.8ms