feat(vt): port Ghostty's VT parsing layer

Ports the parsing half of ZIG/src/terminal into the ghostty-vt crate:
- Parser state machine with a compile-time generated parse table and
  the UTF-8 decoder
- OSC parser incl. all sub-parsers (titles, colors, clipboard, hyperlinks,
  semantic prompts/OSC 133, OSC 9 / ConEmu, iTerm2, kitty clipboard,
  color and text-sizing protocols, context signals)
- CSI, SGR, modes table with save/restore, charsets, ansi, device status,
  mouse shapes, APC, DCS (incl. XTGETTCAP), tmux control-mode parser
- color palette / X11 color names, kitty color and graphics command parser

Deviations from Zig (documented in docs/rewrite/notes/ghostty-vt-W*.md):
parser actions own their payloads instead of borrowing parser buffers;
OSC strings are Vec<u8>; non-exhaustive Zig enums gain an Other(raw)
variant; tmux regexes replaced by equivalent hand-written matchers.

Known follow-up: semantic_prompt.rs carries local copies of the printf-q
and percent decoders that belong in ghostty-os.

Test Plan:
- cargo test -p ghostty-vt: 395 passed, 1 ignored (skipped in Zig too)
- cargo clippy -p ghostty-vt --all-targets -- -D warnings: clean
This commit is contained in:
ddidderr committed 2026-10-11 21:52:41 +02:00
1 parent 987f2957ad
commit bf84326c2f
44 files changed
+15040 -6

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
# ghostty-vt W1 notes
## Ported (Zig -> Rust, under crates/ghostty-vt/src)
- Parser.zig -> parser.rs (`Parser`, `State`, `TransitionAction`, `Action`, `Csi`/`Esc`/`Dcs`, `MAX_PARAMS`, `SepList`)
- parse_table.zig -> parse_table.rs (`TABLE`, built by `const fn`; identical rule order)
- UTF8Decoder.zig -> utf8_decoder.rs (`Utf8Decoder::next -> (Option<u32>, bool)`)
- osc.zig -> osc/mod.rs; osc/encoding.zig -> osc/encoding.rs; osc/parsers/*.zig -> osc/parsers/*.rs (all 15)
- color.zig -> color.rs; x11_color.zig -> x11_color.rs (res/rgb.txt copied, `include_str!`, lazy map); kitty/color.zig -> kitty/color.rs
## Deviations / API notes
- `Action` owns its data (fixed inline arrays) rather than borrowing from the parser: use `csi.params()`, `csi.intermediates()`, `csi.final_`, `csi.params_sep`. `Print(u32)`. `Parser::next` returns `[Option<Action>; 3]`.
- `Parser::new()`; `parser.osc_parser.alloc` is a `bool` ("allocator available", Zig `?Allocator`). Stream must set `osc_parser.alloc = true` as stream.zig does, otherwise OSC 4/5/10-19/104/110-119/133/21 are rejected like in Zig.
- OSC `Command` is a Rust enum (`osc::Command`, `osc::Key` via `Command::key()`); `[:0]const u8` fields are `Vec<u8>` (no NUL, not necessarily UTF-8). `Parser::end` returns an owned `Option<Command>`.
- Fixed buffer (MAX_BUF=2048, one byte reserved for NUL where Zig writes one) vs allocating writer = one `Vec<u8>` with an optional cap. `Parser::data()` (reserves NUL) / `buffered()` (does not) replace `writer.buffered()`. Remaining edge: osc9/iterm2/text_sizing use `data()` even where Zig writes no NUL for some sub-commands (only differs at exactly 2048 payload bytes).
- semantic_prompt: `Option` enum is `OptionKey`; `printf_q_decode`/`url_percent_decode` local copies (TODO(integration): move to/consume from ghostty-os string_encoding). `color::Rgb` (alias `RGB`); `DynamicRgb.override` is `override_`.
- Clippy: test modules `#![allow(clippy::unwrap_used)]`.
## Skipped tests
- `ghostty.h Command.ProgressReport.State` (C ABI), `refAllDecls`/`test { _ = ... }` comptime-reference tests, comptime `@sizeOf(Command)` assert.
- One iterm2 test is `SkipZigTest` in Zig; kept as `#[ignore]`.
- Extra tests added: osc_112 in color parser, printf-q decode in semantic_prompt.
+32
View File
@@ -0,0 +1,32 @@
# ghostty-vt W2 notes
## Ported (Zig -> Rust, all in crates/ghostty-vt/src)
- csi.zig -> csi.rs (EraseDisplay, EraseLine/TabClear with `Other(u8)` for the non-exhaustive Zig enums, SizeReportStyle, TitlePushPop)
- sgr.zig -> sgr.rs (`Attribute<'a>`, `Parser<'a>` implementing `Iterator`; uses `crate::color::{Name, Rgb}` and `crate::parser::{SepList, MAX_PARAMS}`)
- modes.zig -> modes.rs (`Mode` enum, `ENTRIES` table, `ModePacked(u64)`, `ModeState`, `mode_from_int`)
- charsets.zig -> charsets.rs (const-built 256-entry tables)
- ansi.zig -> ansi.rs; device_status.zig -> device_status.rs; mouse_shape.zig -> mouse_shape.rs
- kitty/key.zig: three remaining tests added to existing kitty/key.rs
- kitty/graphics_command.zig -> kitty/graphics_command.rs (parser + Response + all command structs; `pub mod graphics_command` in kitty/mod.rs)
- apc.zig -> apc.rs; dcs.zig -> dcs.rs; tmux/control.zig -> tmux/control.rs (+ tmux/mod.rs, only control)
- Dependencies added: `thiserror`, `base64`.
## Deviations
- Non-exhaustive Zig enums (EraseLine, TabClear, RenditionAspect, StatusLineType) are Rust enums with `Other(raw)`; C0 uses `from_int -> Option`.
- tmux Notification owns its data (`Vec<u8>`) instead of borrowing the parser buffer. Oniguruma regexes replaced by hand-written matchers with the same greedy semantics. IDs overflowing usize are treated as non-match (Zig: `unreachable`).
- DCS/APC handlers drop allocator params; `tmux` state holds `Box<tmux::control::Parser>`.
- graphics_command base64: `simd.base64` replaced by `base64` crate (standard alphabet, padding optional, ASCII whitespace ignored like simdutf). zlib is not decoded in the parser (Zig does not either), so no flate2 dependency.
- `Parser::complete` moves the decoded payload out (Zig `decodeData` also consumes the buffer).
- modes: Zig tests `ModePacked` size == 8; Rust has an equivalent `packed_size` test (u64). Mode bit index = enum discriminant = position in ENTRIES.
- sgr uses a bounds-tolerant `sep_is_set` (Zig's StaticBitSet would assert past MAX_PARAMS).
- `Attribute` variant names: `Fg8/Bg8/Fg8Bright/Bg8Bright/Fg256/Bg256/UnderlineColor256` for Zig `@"8_fg"` etc.
## Skipped (Zig-only)
- sgr "Attribute C compat", `Attribute.C`/`cval`/`lib.Enum`/`lib.TaggedUnion` (C ABI).
- mouse_shape "ghostty.h MouseShape" (C header check) and GObject type registration (belongs to apprt-gtk; `MouseShape` is `repr(i32)` in declaration order).
- modes `test { _ = Mode; _ = ModePacked }` and charsets/tests reflection-only bits (replaced by explicit tests).
- Allocator-failure aspects (`deinit`, arenas) have no Rust equivalent.
## Known gaps / integration
- dcs.rs uses `crate::parser::Dcs` (W1). `sgr`/`dcs` depend on W1 modules compiling.
- tmux layout/output/viewer are NOT here (ghostty-terminal).