State JSON
State JSON records terminal text, styles, and cursor position for snapshot tests and debugging.
Write final state with Output <path>.json or checkpoint state with State <path>.json. See
Outputs for when each one is written.
Top-level shape
Section titled “Top-level shape”{ "size": [80, 24], "total_rows": 31, "scrollback_rows": 7, "cursor": { "x": 2, "y": 10, "visible": true }, "default_style": { "fg": "#dddddd", "bg": "#102040" }, "styles": [{ "fg": "#5a56e0" }, { "fg": "#ffcc66", "bold": true }], "viewport_text": "> echo ok\nok\n> ", "scrollback_text": "cargo build\n...", "viewport": [[["> ", 0], "echo ok"], ["ok"], [["> ", 0]]], "scrollback": [["cargo build"], ["..."]]}Important fields:
sizeis[columns, rows]for the PTY grid.total_rowsis the total rows reported bylibghostty-vt, including scrollback and viewport.scrollback_rowsis the number of rows currently available above the viewport.titleandworking_directoryare included only when non-empty.cursoris zero-based within the visible viewport.viewport_textandscrollback_textare the easiest fields to assert in tests.viewportandscrollbackcontain compact styled spans for the same trimmed rows.
Text fields
Section titled “Text fields”viewport_text contains visible viewport text after trailing blank rows are trimmed.
scrollback_text contains rows above the viewport, also trimmed. Non-empty text fields include a
trailing newline because they represent terminal rows rather than a single logical string.
For many tests, these fields are enough:
let state: serde_json::Value = serde_json::from_str(&json)?;assert!(state["viewport_text"].as_str().unwrap().contains("Saved profile"));Use styled spans only when color or attributes are part of the behavior being tested.
Styles and spans
Section titled “Styles and spans”Rows are arrays of spans:
"text"means text usingdefault_style.["text", 0]means text usingstyles[0].- Empty rows are represented as
[]. - Adjacent cells with the same style are merged into one span.
- Trailing empty rows are omitted because they are implied by
size.
default_style is fully expanded so plain strings are self-describing. Entries in styles are
deltas from default_style; fields that match the default are omitted.
Style objects may include:
| Field | Meaning |
|---|---|
fg |
Foreground color as #rrggbb |
bg |
Background color as #rrggbb |
bold |
Bold text attribute |
italic |
Italic text attribute |
faint |
Faint text attribute |
blink |
Blink text attribute |
inverse |
Inverse text after style/color resolution |
invisible |
Invisible text attribute |
strikethrough |
Strikethrough text attribute |
overline |
Overline text attribute |
underline |
single, double, curly, dotted, dashed, or none |
Boolean fields are omitted when false. underline is omitted when it is none.
Why this encoding
Section titled “Why this encoding”The chosen encoding keeps no-style text short while still preserving styles:
[["plain text"], [["styled", 0], " normal again"]]Earlier options were rejected because they were either too verbose for style-heavy output or too hard to read in diffs:
- Cell-by-cell JSON repeated default style data and made ordinary terminal output noisy.
- Fully referential spans with row/column positions were compact for sparse styles but verbose when styles changed often.
- Object spans such as
{ "text": "foo", "style": 1 }were readable but much larger than["foo", 1]. - YAML, TOML, and JSONC each add parsing tradeoffs. Betamax writes strict JSON so tests can consume snapshots with standard parsers.