Commit Graph

16827 Commits

Author SHA1 Message Date
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
Mitchell Hashimoto
bab076c1a2 build: lower iOS deployment target version (#13539)
Reopening #13535.
2026-08-02 07:03:52 -07:00
Mitchell Hashimoto
915496c221 libghostty(formatter): fix superfluous newline in html formatting (#13543)
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.
2026-08-02 07:03:40 -07:00
Mitchell Hashimoto
f6830420ce termio: fix doc comment grammar and PTY casing in message.zig (#13549)
### What
Comment-only cleanup in `src/termio/message.zig`:

- Fixes a subject-verb agreement error in the `Message` union's doc
  comment: "the number of messages ... are also very few" -> "is also
  very small"
- Capitalizes "pty" to "PTY" in several doc comments 

### Why
Caught while reading through `message.zig`. No functional changes,
doc comments only.
2026-08-02 07:03:19 -07:00
Mitchell Hashimoto
322636bfb0 config: update scrollbar doc per current implementation (#13553)
We should also update the doc after #9865, which is discussed in
https://github.com/ghostty-org/ghostty/discussions/9610
2026-08-02 07:02:19 -07:00
Mitchell Hashimoto
e85bf9fb2d terminal/snapshot: pty continuation record (#13556)
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.
2026-08-02 07:01:56 -07:00
Mitchell Hashimoto
70e41e96d3 terminal/snapshot: pty continuation
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.
2026-08-02 06:41:42 -07:00
Lukas
a7cfa6fc23 config: update scrollbar doc per current implementation 2026-08-02 14:45:55 +02:00
ghostty-vouch[bot]
6837d7027f Update VOUCHED list (#13550)
Triggered by
[comment](https://github.com/ghostty-org/ghostty/issues/13549#issuecomment-5157211534)
from @trag1c.

Vouch: @12ya

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-02 10:32:48 +00:00
Iliya Lyan
f5911d6964 comment: fix grammar and acronym casing in message.zig doc comments
- "the number of messages we send to the IO thread are also very few"
  had a subject-verb agreement issue; reworded to "is also very small"
- Capitalized "pty" -> "PTY"
2026-08-02 19:27:31 +09:00
Roni Jacobson
75302feda4 Add test for superfluous newline in html formatting 2026-08-02 02:21:12 +03:00
Mitchell Hashimoto
46edeee407 macOS: fix update error pill is not showing properly (#13540)
`acknowledgement` will call `dismissUpdateInstallation` so the error
state will never happen.
2026-08-01 14:28:06 -07:00
Mitchell Hashimoto
f5880782fe terminal: add stream continuation tracking for replay (#13544)
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. :)

**LLM usage:** I wrote the continuation tracker myself, used a mix of
5.6+Fable to review it for me, applied their feedback directly. Only
place with predominantly AI code are tests, which I reviewed. Commit and
PR message written myself.

## 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

No real impact.

Via `ghostty-bench +terminal-stream`.

Corpus | Main | PR w/ Tracking Off | PR w/ Tracking On
-- | -- | -- | --
Plain ASCII (256 MiB) | 175.4 ms | 175.8 ms | 175.5 ms
UTF-8 (32 MiB) | 268.4 ms | 270.0 ms | 269.9 ms
5% invalid UTF-8 (32 MiB) | 316.4 ms | 317.8 ms | 320.2 ms
CSI-heavy (32 MiB) | 145.0 ms | 146.3 ms | 145.9 ms
OSC (32 MiB) | 1621.0 ms | 1627.5 ms | 1638.3 ms
Kitty APC (128 MiB) | 95.5 ms | 96.9 ms | 96.3 ms
Mixed traffic (32 MiB) | 172.4 ms | 172.0 ms | 172.3 ms
Giant APC (128 MiB) | 38.3 ms | 38.3 ms | 40.8 ms
2026-08-01 14:16:20 -07:00
Mitchell Hashimoto
68beeeb3f6 terminal: add stream continuation tracking for replay
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
2026-08-01 13:39:21 -07:00
Roni Jacobson
f024d21fc4 Fix superfluous newline in html formatting
Every page is formatted in a div, and when the div closes it creates 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).
2026-08-01 21:36:58 +03:00
Jeffrey C. Ollie
2ee42adc76 datastruct/circ_buf: fix metadata after shrinking (#13515)
## Summary

- normalize circular-buffer metadata after every resize
- retain the oldest values when shrinking below the current length
- cover partial shrink, exact-length shrink, and empty-to-zero
boundaries

## Root cause

`resize` rotated live values to index zero before reallocating, but only
repaired `head` and `full` when capacity grew. Shrinking a partially
filled buffer could therefore leave `head` beyond the new allocation and
report a length greater than capacity. A later append could index
outside the resized storage.

## Validation

- `zig fmt --check src/datastruct/circ_buf.zig`
- `zig test src/circ_buf_test.zig --test-filter 'CircBuf resize'` using
a temporary import harness: 8 tests passed
2026-08-01 12:19:05 -05:00
trag1c
b2d4462590 gtk: fix capitalization of banner title (#10642) 2026-08-01 18:58:27 +02:00
Jeffrey C. Ollie
e9e7864b43 remove fuzze entries in *.po files 2026-08-01 11:28:10 -05:00
ghostty-vouch[bot]
74ad15c104 Update VOUCHED list (#13542)
Triggered by
[comment](https://github.com/ghostty-org/ghostty/issues/13527#issuecomment-5152299257)
from @jcollie.

Vouch: @gadgetman6

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-01 16:23:13 +00:00
Jeffrey C. Ollie
7a512c3125 gtk: fix capitalization of banner title 2026-08-01 11:03:10 -05:00
Jeffrey C. Ollie
631c71e41c inspector: add copy and export for terminal IO events (#13519)
Adds "Copy" and "Export to file" buttons to the Terminal IO inspector so
recorded VT events can be saved outside the app for sharing or analysis.

Found myself needing/wishing for this while I was debugging my tmux fork
with libghostty-vt.


Disclaimer: I haven't considered performance at all, so please lmk if
there are anything here you would like me to optimize.
2026-08-01 10:54:10 -05:00
Leah Amelia Chen
57ad6c77a5 Rewrite the localization teams docs to improve legibility, and explicitly permit members who know each other (#12720)
~~Maybe I have too many exclamation marks, let me know if I should
metaphorically calm down.~~ Fixed now.

My wording is intentionally biased toward languages spoken less
(**edit**: not nearly as much anymore), but I specifically do not
disallow members who know each other regardless of language popularity.
Quoting myself from Discord[^convo]:

> people who know each other are more likely to have more similar tastes
or quirks in their language use by virtue of (perhaps subconsciously)
stealing off each other, and there's also the whole “eh it's good enough
i trust that you thought it through” thing that's more likely if you
know the other translators already

I don't believe it's *necessarily worse*, and if you have more than two
members then the issue greatly diminishes too, but I don't want people
to see this and go “oh no I need to get my translations in before
Ghostty 1.4 that releases while I'm sleeping tomorrow so I should ask my
bestie to help”, and to instead be willing to be more patient, at least
for a reasonable amount of time (which I consider to be ≤ 2 months).

[^convo]: @trag1c and I chatted about this prior to this PR in
`#maintainers` on the Ghostty Discord server. If you have access to that
channel, check out these links:
[1](https://discord.com/channels/1005603569187160125/1337443701403815999/1504241367357063188),
[2](https://discord.com/channels/1005603569187160125/1337443701403815999/1504465642361979021),
[3](https://discord.com/channels/1005603569187160125/1337443701403815999/1505255908676993135).
2026-08-01 23:26:56 +08:00
Lukas
cc1d262105 macOS: fix update error pill is not showing properly 2026-08-01 15:35:44 +02:00
Mitchell Hashimoto
aa21caeaa3 terminal: improve resize with reflow performance (~6x faster) (#13537)
Improves the time to resize mixed content w/ wrapping 120x80, 10k lines
of scrollback by about ~6x.

I'm working on deferred resize in another branch, but it still follows
roughly the same logic so I instead decided to shift course and look at
the existing full-pagelist resize+reflow and found many places to
improve while keeping understanding.

The optimizations here match the general patterns of other recent
optimizations: cache some stuff, reuse some pages, bring in our
`page.Mask` helper and add vectorized ops. Nothing exotic we haven't
been doing recently.

Also note the Neovim project brought this up as a noticeable issue and I
believe this will help mitigate their issues until we get proper
deferred reflow in.

**LLM notes:** The optimizations were produced with Fable 5 using a
profile-driven approach (macOS `sample` plus disassembly-level
attribution of the hot loops at each step). I then requested each be
split into its own measurable commit, reviewed each in isolation, and
modified most of the commit messages. This PR message is hand-written.
2026-08-01 06:10:41 -07:00
Elias Andualem
aa74971282 build: lower iOS deployment target version 2026-08-01 13:42:34 +03:00
Mitchell Hashimoto
ec5b369611 terminal: vectorize reflow run scan
The masked-compare scan that finds bulk-copyable cell runs still
processed one cell per iteration and remained the largest single
cost in a column reflow.

Scan whole groups of cells at a time using the group variants of the
Mask helper: a group that fully matches the run pattern (and, for
text runs, contains no Kitty virtual placeholder, via eqlAny)
extends the run by the whole group, and any mismatch falls through
to the scalar loop which finds the exact end of the run within it.
The group length comes from the shared simd.lanes helper where the
target has SIMD support and falls back to a plain unrolled group
elsewhere.

1.19x faster on ghostty-bench +terminal-resize --mode=cols (120x80
terminal, 10k-line scrollback, shrink/grow column reflow cycles).
Combined with the preceding reflow optimizations, resize with reflow
is 5.8x faster than before the series.
2026-07-31 21:26:53 -07:00
Mitchell Hashimoto
d4e446c480 terminal: reduce reflow run scan to masked compares
Finding the length of a bulk-copyable cell run evaluated the
field-wise bulkCopyable predicate plus a style compare per cell,
which compiles to a chain of extracts and branches and had become
the hottest loop in a column reflow.

Once the first cell passes the full predicate, a cell continues the
run iff it matches the first cell in content tag, style id, wide
property, and hyperlink flag, so the continuation test is now a
masked compare of the raw cell bits via the Mask helper, plus a
masked equality test against the Kitty virtual placeholder codepoint
for text runs (placeholders must set a row flag so they take the
slow path). This is slightly stricter than the predicate (a bg-color
cell no longer extends an unstyled text run), which only splits a
copy into multiple runs and remains correct.

1.21x faster on ghostty-bench +terminal-resize --mode=cols (120x80
terminal, 10k-line scrollback, shrink/grow column reflow cycles).
2026-07-31 21:26:53 -07:00
Mitchell Hashimoto
0fb3565c76 terminal: support nested field paths and eqlAny in Mask
Two small extensions to the Mask helper, both motivated by the
reflow bulk run scan in the next commits.

fieldMask now accepts dot-separated field paths so a mask can cover
a nested field of a packed struct or packed union member, e.g.
"content.codepoint.data" covers exactly the codepoint bits of a cell
without its padding. Packed union members all share bit offset zero.

Mask gains eqlAny, the "any" counterpart to eql: it returns whether
any value in a group has masked fields equal to the expected
pattern. This supports run scans that must stop when a sentinel
value appears anywhere in a group, such as the Kitty virtual
placeholder codepoint which requires slow-path handling.
2026-07-31 21:26:53 -07:00
Mitchell Hashimoto
46276d046c terminal: recycle pages within a column reflow
In `resizeCols`, stash the most recently finished source node
instead of destroying it, so we can recycle it without a bunch of
syscalls.

1.30x faster on ghostty-bench +terminal-resize --mode=cols (120x80
terminal, 10k-line scrollback, shrink/grow column reflow cycles),
with system time dropping from 34ms to 8ms per run.
2026-07-31 21:14:00 -07:00
Mitchell Hashimoto
179161c081 terminal: memoize reflow new-page capacity adjustment
reflowRow computed the capacity for prospective destination pages on
every source row via Capacity.adjust, which performs a full page
layout calculation to find the available grid space. 

The result only depends on the source page, and reflow visits source pages
sequentially and never revisits one, so memoize the adjustment per
source page so we only do this once.

1.06x faster on ghostty-bench +terminal-resize --mode=cols (120x80
terminal, 10k-line scrollback, shrink/grow column reflow cycles).
2026-07-31 21:10:33 -07:00
Mitchell Hashimoto
c249b9de34 terminal: bulk-copy runs of simple cells during reflow
Reflow copied every cell through a per-cell state machine
(writeCell) that dispatches on content tag, wide property, grapheme,
hyperlink, and style handling, and advances the destination cursor
one cell at a time. The vast majority of cells in practice are
narrow text or bg-color cells with no managed memory that share a
single style across long runs.

reflowRow now scans ahead for the run of such cells bounded by the
remaining space in the destination row, copies the run with a single
memcpy, and adjusts the style ref count once for the whole run via
useMultiple. Wide characters, spacers, graphemes, hyperlinks, Kitty
placeholders, and rows containing tracked pins all take the original
per-cell path, and a style set failure falls back to writeCell which
handles growing page capacity.

2.19x faster on ghostty-bench +terminal-resize --mode=cols (120x80
terminal, 10k-line scrollback, shrink/grow column reflow cycles).
2026-07-31 21:06:16 -07:00
Mitchell Hashimoto
c5ca2db1b6 terminal: memoize style id mapping during reflow
Memoize the most recent style mapping and when there is a reuse
bump the ref with `use()`. This avoids a lookup (`addWithId`) on
every single styled cell.

1.25x faster on ghostty-bench +terminal-resize --mode=cols (120x80
terminal, 10k-line scrollback, shrink/grow column reflow cycles).
2026-07-31 21:06:16 -07:00
Mitchell Hashimoto
4a88cc5948 terminal: skip reflow pin scans for rows without pins
Reflow scanned the full tracked pin list for every source cell it
copied, twice per cell in the wide-character case, even though pins
are rare and at most a handful exist. Each check also went through
node.page(), which can restore a compressed page just to compare
pointers.

reflowRow now determines once per row whether any tracked pin is on
the source row and skips the per-cell pin scans entirely when there
is none, which is the overwhelmingly common case. The comparisons
use node identity instead of pages: a node owns exactly one page, so
they are equivalent, and this avoids the restore hazard.

1.09x faster on ghostty-bench +terminal-resize --mode=cols (120x80
terminal, 10k-line scrollback, shrink/grow column reflow cycles).
2026-07-31 21:03:10 -07:00
Mitchell Hashimoto
dc52c248e7 benchmark: terminal-resize 2026-07-31 20:00:34 -07:00
Mitchell Hashimoto
08f039fbb3 cli: report ssh terminfo cache failures (#13533)
A state directory with the wrong permissions left the terminfo cache
failing with errors that named no path, so there was nothing to act on:

    $ ghostty +ssh-cache --add=user@host
    Error: Unable to add 'user@host' to cache. Error: error.AccessDenied

Every +ssh-cache failure now names its cache file, and +ssh no longer
swallows cache-related errors.

Error messages in these actions are also lowercased after the "Error: "
prefix and append the error with ": {t}" rather than a second "Error: ".

Ref:
https://github.com/ghostty-org/ghostty/issues/9393#issuecomment-5145799368
2026-07-31 19:59:13 -07:00
Mitchell Hashimoto
154ddc2a2f terminal/snapshot: binary snapshot format (#13534)
This adds the first version of a binary snapshot format for terminal
state.

Use cases: replay software (like asciinema), multiplexers (like zmx),
scrollback-saving on disk, etc.

The intention of the binary snapshot format is to be able to fully
encode and decode terminal state across mediums such as network and
disk. You can also encode partial terminal state (e.g. only one screen
or even one page of contents). Long term, the intention is to also
support streaming state while a live terminal is running, but this
initial PR focuses on the full snapshot first (with some design choices
to get to the streaming state in the future).

The format is documented in the Zig code, but I also did a
[Kaitai](https://kaitai.io/) descriptor and both the Zig and Kaitai spec
verify they can parse committed fixtures. This helps identify drift in
the format or encoder/decoders in any way since this must ultimately be
a fixed format.

> [!NOTE]
>
> **On reviewability:** this is a massive PR that I don't expect anyone
to reasonably review. I'm going through it line-by-line (again) but I
purposely extracted any changes that affect other parts of Ghostty out
to other already-merged PRs. This one is isolated purely to a package
that isn't called by any client software. **So the plan is if this rough
shape looks good I'll merge it and we'll iterate from there.**

> [!WARNING]
>
> **Experimental.** The format can and will change. And we may also
decide that binary snapshotting in this way isn't the right direction
altogether (although, I'm pretty confident it is). It'd be impossible to
get a single large perfect PR because it'd be even larger than this by
multiples. So instead, we'll iterate on main so long as this work is not
touching any production code, which it isn't!

## Example

Encode:

```zig
const terminal = @import("terminal/main.zig");

var file_buffer: [16 * 1024]u8 = undefined;
var file_writer = file.writer(io, &file_buffer);
try terminal.snapshot.encode(alloc, &file_writer.interface, &t);
try file_writer.interface.flush();
```

Decode a full terminal:

```zig
var t = try terminal.snapshot.decode(&reader, io, alloc);
defer t.deinit(alloc);
```

## Future

This PR purposely only supports a synchronous encode/decode. I wanted to
get the large groundwork in before iterating further. Some iterations in
the future:

* Kitty graphics
* Live terminal snapshotting
* PTY stream continuation records (so VT state machines can stay in
sync)
* Performance work (encoding and decoding, maybe size)
* Configurable limits to prevent DoS
* C API
* etc...

## Wire format

The "robustness principle" is a guiding principle: "be conservative in
what you do, be liberal in what you accept from others." Our encoders
have a lot of extra validation, our decoders massage invalid data into
reasonable defaults (e.g. invalid styles become unstyled text).

> [!IMPORTANT]
>
> **Version 1 has no compatibility promise.** We use version 1 in the
envelope header. We will absolutely break this format as needed as we
iterate and improve on it...

### Envelope

Every snapshot starts with a fixed ten-byte envelope:

| Offset | Size | Field |
| ---: | ---: | :--- |
| 0 | 8 | Magic: `GHOSTSNP` |
| 8 | 2 | Snapshot version: `1` |

### Record framing

After the envelope, records are concatenated back-to-back. Every record
has a fixed header:

| Offset | Size | Field |
| ---: | ---: | :--- |
| 0 | 2 | Record tag |
| 2 | 4 | Payload length |
| 6 | 4 | CRC32C |
| 10 | variable | Payload |

CRC32C covers the encoded tag, payload length, and payload. The
payload-length boundary prevents a malformed record decoder from
consuming bytes belonging to the next record.

The registered record tags are:

| Value | Tag | Purpose |
| ---: | :--- | :--- |
| 1 | `TERMINAL` | Terminal-wide state and declared screens |
| 2 | `SCREEN` | One screen's live state and active page manifest |
| 3 | `PAGE` | One self-contained set of rows and cells |
| 4 | `HISTORY` | One screen's historical page manifest |
| 5 | `READY` | Digest of the renderable prefix |
| 6 | `FINISH` | Digest of the complete snapshot |

To view the format of each record, read its corresponding
`terminal/snapshot/<type>.zig` file.

### Complete record sequence

```text
+----------------------------------------+
| Envelope                               |
+----------------------------------------+
| TERMINAL                               |
+----------------------------------------+
| SCREEN * terminal.screen_count         |
| PAGE   * each screen.page_count        |
+----------------------------------------+
| READY                                  |
+----------------------------------------+
| HISTORY * terminal.screen_count        |
| PAGE    * each history.page_count       |
+----------------------------------------+
| FINISH                                 |
+----------------------------------------+
| Optional containing-transport bytes    |
+----------------------------------------+
```

`SCREEN` and `HISTORY` groups are routed by their encoded screen key and
may arrive in either key order.

## Checkpoints and validation

Each record has an independent CRC32C, but per-record checksums cannot
detect a valid record being reordered, omitted, or duplicated. `READY`
and `FINISH` therefore contain BLAKE3-256 digests over exact snapshot
prefixes:

- `READY` covers the envelope, `TERMINAL`, and all live `SCREEN`/`PAGE`
sequences. It does not include itself.
- `FINISH` covers that same prefix, the complete `READY` record, and all
`HISTORY`/`PAGE` sequences. It does not include itself.

This gives the format two useful integrity boundaries:

```text
envelope ... active pages | READY | history pages | FINISH
<------ renderable ------->
<------------- complete snapshot --------------->
```

## Performance

### Size, Compression Recommended

We intentionally use a simple grid over something like RLE (run-length
encoding). So every row contains exactly `columns` cells and each is
16-bytes! This is large! A 80x24, 10,000 line scrollback terminal
uncompressed would be ~13MB. However, with zstd level 1 compression that
goes down to 260K.

### Speed

We haven't benchmarked encoding or decoding speed yet. This PR focused
on getting a format in place. This will be heavily optimized later. I
suspect its probably pretty darn slow, actually.

## Kaitai Struct

I added a `snapshot.ksy` Kaita Struct spec that independently describes
the complete format. This is used by us for format validation but it can
also be used to programmatically generate parsers. For example, our test
fixture in the Kaita Struct web IDE decodes to:

<img width="523" height="779" alt="image"
src="https://github.com/user-attachments/assets/cd199c73-b6d6-4b35-8957-0cfe3d1a18f2"
/>

**AI Usage:** This work was done in concert with various models and
agents. Writing full encoders/decoders is tedious so it took a lot of
that way. A lot of review was done by AI (trying to find holes, issues,
inconsistencies). The actual binary protocol design and iteration was
done by me. This PR message was written by me.
2026-07-31 19:26:59 -07:00
Jon Parise
ca3dc9eeac cli: classify ssh cache errors in DiskCache 2026-07-31 16:48:26 -04:00
Mitchell Hashimoto
6b09bb3fca terminal/snapshot: ignore hex files in typos
Exclude annotated snapshot fixture hex files from typo checking. Their arbitrary binary byte sequences can otherwise be misidentified as misspelled words.
2026-07-31 13:21:53 -07:00
Mitchell Hashimoto
05d4934848 terminal/snapshot: better root export
Expose complete encode and decode entry points directly from terminal.snapshot instead of requiring terminal.snapshot.snapshot. Reorder the decode APIs to accept the allocator and I/O context before the reader.
2026-07-31 13:16:42 -07:00
Jon Parise
8fca64957b cli: report ssh terminfo cache failures
A state directory with the wrong permissions left the terminfo cache
failing with errors that named no path, so there was nothing to act on:

    $ ghostty +ssh-cache --add=user@host
    Error: Unable to add 'user@host' to cache. Error: error.AccessDenied

Every +ssh-cache failure now names its cache file, and +ssh no longer
swallows cache-related errors.

Error messages in these actions are also lowercased after the "Error: "
prefix and append the error with ": {t}" rather than a second "Error: ".

Ref: https://github.com/ghostty-org/ghostty/issues/9393#issuecomment-5145799368
2026-07-31 16:11:30 -04:00
Mitchell Hashimoto
e2e74fecbe terminal/snapshot: move history size hints to screen record
Publish each screen's logical history extent before READY so clients can size scrollbars while older pages are still arriving. Keep the value advisory and continue deriving native PageList totals from decoded pages.

Reduce HISTORY to its structural screen key and page count, and update the format documentation, Kaitai schema, verifier, and versioned fixtures.
2026-07-31 11:57:58 -07:00
Mitchell Hashimoto
d37e1fe184 terminal/snapshot: format doesn't require EOF
Treat FINISH as the self-delimiting snapshot boundary instead of peeking for end-of-file. Normal decoding now leaves continuation bytes unread, allowing snapshots and live protocol data to share a stream without waiting for closure.

Add decodeExact for bounded files that still require strict exhaustion, and update the Kaitai schema, documentation, and tests for continuation and sequential snapshot decoding.
2026-07-31 11:46:07 -07:00
Mitchell Hashimoto
f0fe788fcc terminal/snapshot: less buffering, better stream writing
Stream complete snapshot records to any std.Io.Writer while retaining one reusable payload buffer for length and CRC calculation. Update BLAKE3 incrementally so checkpoints no longer require rehashing an allocating destination.

Wrap decode hashing in StreamReader to enforce exact checkpoint boundaries. Preserve v1 bytes while allowing snapshots to begin at the current writer position and retaining only valid prefixes on failures.
2026-07-31 11:27:11 -07:00
Mitchell Hashimoto
58e92098a2 terminal/snapshot: snapshot robustness
Route HISTORY sequences by their encoded screen key so both keyed sequence groups can arrive in either order. Keep undeclared and duplicate routing strict, separate HISTORY manifest parsing from page restoration, and clear decoder-only generation state before returning the terminal.
2026-07-31 10:57:28 -07:00
Mitchell Hashimoto
9d1c6a9217 terminal/snapshot: terminal robustness
Normalize unknown terminal-wide semantic fields during restore while keeping dimensions and screen count structural. Preserve canonical encoding, ignore reserved mode and tab-stop bits, reset invalid color and scrolling state, and clamp finite scrollback policies to the native range.
2026-07-31 10:53:23 -07:00
Mitchell Hashimoto
7c64181b69 terminal/snapshot: history robustness
Treat HISTORY row counts as canonical metadata rather than a reason to reject otherwise usable history. Restore topology from the declared PAGE sequence while keeping record framing, routing keys, and sequence boundaries strict.
2026-07-31 10:47:05 -07:00
Mitchell Hashimoto
a44eb83358 terminal/snapshot: style/hyperlink robustness in page and screen
Keep the standalone style and hyperlink codecs strict while allowing PAGE and SCREEN decoders to discard invalid optional data at boundaries they own. Normalize invalid styles to defaults, ignore unrepresentable hyperlinks, reuse duplicate entries, and validate hyperlink values before encoding.
2026-07-31 09:59:42 -07:00
Mitchell Hashimoto
465488d6b4 terminal/snapshot: screen robustness
Keep SCREEN encoding strict while allowing decoding to recover from unknown or noncanonical semantic state. Cursor positions now clamp to the restored active area, and invalid enum values, reserved bits, and optional state degrade to native defaults.
2026-07-31 09:41:04 -07:00
Uzair Aftab
2c0d2588a7 inspector: make FileChooser cast type-safe 2026-07-31 18:40:49 +02:00