Commit Graph

2625 Commits

Author SHA1 Message Date
Fredrik Fornwall
74efadb446 lib-vt: answer XTGETTCAP queries
Ghostty's full termio path answers XTGETTCAP from the static terminfo
map, but terminal/stream_terminal.zig, which backs libghostty-vt,
parses the same DCS request and then discards it. There is no XTGETTCAP
effect either, so an embedder cannot restore the replies through the
C API.

Programs query these over SSH instead of assuming the remote host has
the client's terminfo entry. This matters more for an embedder than for
the desktop app, which can install its entry on the remote through
shell integration.

Answer the queries in stream_terminal the same way termio does: look
up each requested key in the static terminfo map and write the reply
to the pty, skipping the lookups entirely when no write_pty effect is
set. The map now stores null-terminated responses so they can be
handed straight to write_pty without copying. terminal/dcs.zig and the
termio path are unchanged.

"TN" is handled separately. It names the terminfo entry the terminal
runs as, so it has to agree with TERM -- which is set in
termio/Exec.zig, a layer libghostty-vt does not contain. The library
never sees TERM and cannot answer on the embedder's behalf, and
answering with Ghostty's own entry from the static map would misreport
every embedder, so "TN" is intercepted before the map lookup. The name
is instead configured through a new option,
GHOSTTY_TERMINAL_OPT_TERMINFO_NAME: the string is copied into the
terminal, names longer than 128 bytes are rejected, and while unset
the query goes unanswered.

This is the first dependency from src/terminal on src/terminfo, so
libghostty-vt now carries Ghostty's terminfo table: +16,023 bytes
(+1.9%) on a wasm32-freestanding ReleaseSmall build.

Signed-off-by: Fredrik Fornwall <fredrik@fornwall.net>
2026-08-08 20:15:40 -07:00
Mitchell Hashimoto
6b990de5be terminal: C API for unknown sequences 2026-08-08 16:49:45 -07:00
Mitchell Hashimoto
b537282411 terminal/apc: support reporting unknown APC sequences 2026-08-08 08:54:32 -07:00
Mitchell Hashimoto
afb351f838 terminal/stream: fast-path APC termination
APC payload bytes are bulk consumed, but the terminating byte still passed 
through the generic parser action loop. Handle ESC and C1 ST directly after 
bulk consumption while leaving other transitions on the scalar path.
2026-08-08 08:54:16 -07:00
Mitchell Hashimoto
219173ab37 terminal/snapshot: remove BLAKE3 digests
Remove BLAKE3 prefix digests. Keep READY/FINISH as empty records since
they're semantically important markers.

Our existing format (CRC32 per-record, declared counts, strict tag ordering
requirements, etc.) already detect: accidental corruption, truncation,
data omission, and duplication. 

BLAKE3 only protects against valid records being swapped or removed entirely. 
It is heavy for just that, and callers can solve that anyways via their
own transport (like, just use TCP). For more adversarial protection,
callers can also add layers like TLS or their own alternate signing
methods depending on their own threat models.

Removing the hash improves encode times by ~1.4x, decode times by ~1.3x.
Time-to-READY decoding is effectively unchanged because it was such a 
small package to begin with.
2026-08-06 14:12:08 -07:00
Mitchell Hashimoto
cfc19e8053 libghostty: add configurable mode defaults, remove mode_set/get
ABI BREAKING: This removes `ghostty_terminal_mode_get` and `_mode_set`.
We can now represent these operations completely with standard 
`ghostty_terminal_get` and `ghostty_terminal_set`, which makes it much
more flexible to preserve ABI in the future.

This is all centered around a new `GhosttyTerminalModeConfig` structure
that is an in or out parameter depending on use case.

This also adds a new `GHOSTTY_TERMINAL_OPT_MODE_DEFAULT` option that
can be used to set the _default_ value of mode that happens when a RIS
event (full reset) is sent.
2026-08-05 22:07:25 -07:00
Amp
b5e86a4284 terminal/kitty: release evicted placement pins
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>
2026-08-05 23:10:17 +00:00
Mitchell Hashimoto
9cb2147641 terminal/kitty: evict without scratch allocation (#13627)
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.
2026-08-05 15:28:41 -07:00
Mitchell Hashimoto
090d161b28 terminal: report overline in DECRQSS SGR response (#13653)
#11638

Report SGR 53 when the active cursor style has overline enabled.
2026-08-05 15:20:19 -07:00
Mitchell Hashimoto
f973bd53ba terminal: report overline in DECRQSS SGR response
#11638

Report SGR 53 when the active cursor style has overline enabled.
2026-08-05 15:09:01 -07:00
Mitchell Hashimoto
8524cb593c terminal/kitty: fix point deletion calculations (d=p, d=c)
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
2026-08-05 15:00:53 -07:00
Mitchell Hashimoto
9ed61428da libghostty-vt: spacer-tail handling needs to respect slow runtime safety (#13651)
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.
2026-08-05 14:49:10 -07:00
Mitchell Hashimoto
1aeca6705e terminal: color reset should set override to null, not default (#13650)
#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.
2026-08-05 14:48:40 -07:00
Mitchell Hashimoto
e20564791e libghostty-vt: spacer-tail handling needs to respect slow runtime safety
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.
2026-08-05 14:34:55 -07:00
Mitchell Hashimoto
7cd2f65f5c terminal: color reset should set override to null, not default
#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.
2026-08-05 14:30:21 -07:00
Mitchell Hashimoto
7a9c369cf5 terminal: preserve cursor when formatting tabstops
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.
2026-08-05 14:06:11 -07:00
Mitchell Hashimoto
bfd40c84bd terminal: reset wrap state for CSI 2 K
#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.
2026-08-05 11:26:48 -07:00
Jon Parise
4371871bc2 terminal/kitty: evict without scratch allocation
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.
2026-08-05 13:16:27 -04:00
Mitchell Hashimoto
46767b5213 terminal: bound OSC and grapheme allocations (#13633)
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 |
2026-08-05 09:52:11 -07:00
Mitchell Hashimoto
ad27c989a4 libghostty-vt: require opt-in for title reports (#13632)
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.
2026-08-05 09:32:45 -07:00
Mitchell Hashimoto
727b8a02f8 terminal: bound OSC and grapheme allocations
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 |
2026-08-05 09:30:24 -07:00
Mitchell Hashimoto
38e891e6c0 terminal: require opt-in for title reports
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.
2026-08-05 09:17:03 -07:00
Mitchell Hashimoto
bd21ff153e terminal: avoid VS15 cursor underflow (#13631)
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.
2026-08-05 09:10:46 -07:00
Mitchell Hashimoto
fe98aef21c terminal: report DECECM as permanently reset (#12660)
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.
2026-08-05 09:03:15 -07:00
Mitchell Hashimoto
33d34cf5ce terminal: avoid VS15 cursor underflow
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.
2026-08-05 08:55:21 -07:00
Mitchell Hashimoto
402b9227de terminal/kitty: reclaim pruned placements
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.
2026-08-05 08:37:45 -07:00
Mitchell Hashimoto
d0c516f8f3 terminal/kitty: release replaced placement pins
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.
2026-08-05 08:28:13 -07:00
Mitchell Hashimoto
590d669c4a terminal/kitty: limit png decoder allocations
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.
2026-08-05 08:24:42 -07:00
Mitchell Hashimoto
f766f303a7 terminal/kitty: validate shared memory ranges
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.
2026-08-05 08:20:41 -07:00
Mitchell Hashimoto
ec04900ab9 terminal/kitty: validate opened image file paths
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.
2026-08-05 08:14:47 -07:00
Mitchell Hashimoto
e5840bb9ba terminal/kitty: harden placement geometry
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.
2026-08-05 08:07:25 -07:00
Mitchell Hashimoto
af2faa311a terminal/kitty: restrict temporary image file paths
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.
2026-08-05 08:07:25 -07:00
Mitchell Hashimoto
d866fa4553 terminal/kitty: fix graphics range deletion
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.
2026-08-05 08:05:27 -07:00
Uzair Aftab
5b70f208bc terminal: print repeated characters through printSlice
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.
2026-08-05 15:43:25 +02:00
Mitchell Hashimoto
2ed67cadd1 terminal: redesign pin map for formatter 2026-08-03 20:50:33 -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
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
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
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
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