Files
ghostty-rs/docs/rewrite/notes/ghostty-terminal-render-formatter.md
T
ddidderr 6a374b6953 feat(terminal): port render state, highlights and the formatter
- render.rs ports render.zig: RenderState, the per-frame snapshot the
  renderer consumes (rows, cells, styles, graphemes, selection,
  highlights, cursor, colors), updated incrementally with Zig's full
  redraw triggers and per-page/per-row dirty tracking. Cells are stored
  struct-of-arrays; use the accessors.
- highlight.rs ports highlight.zig (untracked, tracked, flattened).
- formatter.rs ports formatter.zig: plain, VT and HTML output for
  terminal, screen, page list and page, with selection, rectangle,
  unwrap, trim, palette, codepoint maps, hyperlinks and pin/point maps.
  Screen's test dump helpers now delegate to it.

Cargo.toml also picks up dependencies for the kitty graphics and search
modules that are being ported concurrently.

Test Plan:
- cargo test -p ghostty-terminal --lib (1243 passed; formatter 100/100,
  render 15/15 Zig tests)
2026-10-11 22:50:22 +02:00

58 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ghostty-terminal: render, highlight, formatter
## Ported
| Zig | Rust | tests |
|---|---|---|
| render.zig | `render.rs` | 15 of 15 (+2 extra) |
| highlight.zig | `highlight.rs` | 0 in Zig (+2 extra) |
| formatter.zig | `formatter.rs` (+ `formatter/tests/p1..p4.rs`) | 100 of 100 (p1 33, p2 26, p3 26, p4 15) |
`Screen::selection_string[_map]`, `dump_string[_alloc[_unwrapped]]` (and so `Terminal::plain_string[_unwrapped]`) now delegate to
`formatter::ScreenFormatter` (the minimal `plain_format` in screen.rs is gone).
## RenderState API (for the renderer / font shaper)
```rust
use ghostty_terminal::render::{RenderState, Dirty};
let mut state = RenderState::new(); // Zig `.empty`; keep it across frames (buffers are reused)
state.update(&mut terminal)?; // hold the renderer mutex; consumes terminal.flags.dirty + screen.dirty
```
`update` follows Zig exactly: full redraw (`Dirty::Full`) if the active screen key, terminal dirty flags, screen dirty flags,
size or viewport pin changed; otherwise only rows whose page/row dirty flag is set are rebuilt (`Dirty::Partial`), clearing row/page dirty.
The renderer must reset `state.dirty = Dirty::False` (and each `row.dirty = false`) after drawing; `update` only ever raises them.
Fields: `rows, cols, colors {background, foreground, cursor: Option<Rgb>, palette}` (fg/bg already swapped for reverse-colors mode),
`cursor {active: Coordinate, viewport: Option<CursorViewport{x,y,wide_tail}>, cell, style, visual_style, password_input, visible, blinking}`,
`row_data: Vec<Row>` (length == rows), `dirty`, `screen: ScreenKey`, `viewport_pin`.
`Row { pin, raw: page::Row, cells: Cells, dirty, selection: Option<[x0,x1]>, highlights: Vec<Highlight{tag, range:[x0,x1]}> }`.
`Row.pin` is a plain (untracked) `Pin`; only valid until the terminal changes.
`Cells` is struct-of-arrays (Zig `MultiArrayList(Cell)`): `raw: Vec<page::Cell>` always `cols` long; `grapheme` / `style` are EITHER empty
(row has no managed memory => no graphemes, default styles) or `cols` long. Use the accessors: `cells.get(x) -> Cell{raw, grapheme: &[u32], style}`
(same shape as the font crate's `RunCell`: `raw`, `grapheme` = extra codepoints, `style`), `cells.grapheme(x)`, `cells.style(x)`, `cells.iter()`.
As in Zig, bg-color-only cells (`ContentTag::BgColor*`) get their `style.bg_color` filled only on rows with managed memory; otherwise read the raw cell.
Other methods:
- `update_highlights_flattened(tag: u8, &[highlight::Flattened])` (infallible; appends to `row.highlights`, marks rows dirty; does not clear old ones).
- `string(&mut String, Option<&mut Vec<Coordinate>>)` – unwrapped viewport text, \0 for empty cells, `\n` after non-wrapped rows (map: newline => x = cols).
- `link_cells(&PageList, Coordinate) -> HashSet<Coordinate>` – DEVIATION: needs the page list (`&terminal.screen().pages`) because Rust pins are
node ids, not page pointers. Must be called with the terminal unchanged since `update`.
Selection rows are cached (`selection_cache`) exactly as in Zig.
## highlight.rs
`Untracked{start,end}`, `Tracked{start,end: TrackedPin}` (handles into `screen.pages`; `Tracked::init(&mut Screen, ..)`, `deinit(&mut Screen)`, `untracked(&PageList)`),
`Flattened{chunks: Vec<Chunk{node,serial,start,end}>, top_x, bot_x}` with `init(&PageList, start, end)`, `start_pin()`, `end_pin()`, `untracked()`.
Zig's `Flattened.init` has a typo (`.end_x`) in unreferenced code; the port sets `bot_x = end.x`.
`highlight::pin_page_iterator(&PageList, start, end)` is the local equivalent of `Pin.pageIterator(.right_down, limit)`.
## formatter.rs
- Writers are `&mut dyn std::io::Write`; every formatter has `format_string(self) -> io::Result<String>` as a convenience. Formatters consume `self` on `format`.
- `Options::{plain(), vt(), html()}` (+ pub fields; `palette: Option<Palette>` owned, `codepoint_map: Option<Vec<CodepointMap>>`, default `Some(vec![])`).
- Formatters: `TerminalFormatter::new(&Terminal, opts)` (extra: `TerminalExtra::{NONE,STYLES,ALL}`), `ScreenFormatter::new(&Screen, opts)` (`ScreenExtra`, `Content::{None, Selection(Option<Selection>)}`),
`PageListFormatter::new(&PageList, opts)`, `PageFormatter::new(&Page, &Options)` (+ `format_with_state` -> `TrailingState`).
- Pin maps: `pin_map: Option<&mut Vec<Pin>>`; page formatter `point_map: Option<&mut Vec<Coordinate>>`. Instead of Zig's discarding writers, extras are rendered into a scratch buffer whose length is used for the map.
- Quirk kept from Zig: blank-cell point-map entries are appended in reverse order (see PageFormatter); `</div>`/`</a>`/`ESC[0m` closers map to the previous byte's coordinate.
## Skipped / deviations
- No Zig tests skipped. Allocator-failure paths do not exist in Rust.
- Hyperlink URI bytes are escaped per byte (as Zig `writeCodepoint(byte)`), so non-ASCII URI bytes become numeric entities.