Files
ghostty-rs/docs/rewrite/notes/ghostty-vt-stream.md
T
ddidderr 800fe5be5e feat(vt): port the stream layer between parser and handlers
Ports ZIG/src/terminal/stream.zig: the Stream feeds bytes through the
UTF-8 decoder and parser and dispatches every CSI/ESC/OSC/DCS/APC and
C0/C1 control to a Handler as a high-level Action (all 95 Zig
variants). Zig's comptime handler interface becomes a Handler trait with
an associated error type, a required vt() and an optional vt_raw() hook.

Performance: next_slice() processes 4 KiB chunks, finds ESC with memchr
and decodes printable runs in bulk (scalar port of ZIG/src/simd's
index_of / utf8_decode_until_control_seq), plus the CSI parameter fast
path.

Terminals must construct the stream with Stream::new_alloc so the OSC
parser accepts allocating commands (colors, OSC 133), as in Zig.

Minor differences: after a stray UTF-8 byte the rest of the chunk
returns to the fast path, and a spurious Zig warning after CSI 2/5 W is
dropped.

Test Plan:
- cargo test -p ghostty-vt (448 passed; 37/38 stream tests ported,
  the skipped one only exercises the C ABI)
2026-10-11 21:59:15 +02:00

48 lines
4.7 KiB
Markdown

# ghostty-vt stream notes
## Ported
- terminal/stream.zig -> `crates/ghostty-vt/src/stream.rs` (Action, Stream, CSI/ESC/OSC/C0/C1 dispatch, 37 of 38 Zig tests as `#[test]`, plus 8 extra).
- simd/index_of.zig, simd/vt.zig -> `src/simd.rs` (scalar; `memchr` dependency). 1 + 6 Zig tests ported.
Not ported: simd/codepoint_width (belongs to ghostty-unicode), simd/base64 (graphics_command already uses the `base64` crate).
- Small edits to W1 files: `parser.rs` (`Esc::new`, parser param fields `pub(crate)` for the CSI fast path), `utf8_decoder.rs` (`in_sequence()`), `lib.rs`.
- Skipped test: `test Action` (C ABI reification). `Action.C`/`cval`/`lib.TaggedUnion` not ported (C ABI).
## API overview (for handler implementors)
```rust
use ghostty_vt::stream::{Action, ActionKey, Handler, Stream};
impl Handler for MyHandler {
type Error = MyError; // Zig `!void` error set (Debug)
fn vt(&mut self, action: Action<'_>) -> Result<(), Self::Error> {
match action {
Action::Print(p) => ..., // p.cp: u32
Action::CursorUp(m) => ..., // m.value: u16
Action::SetMode(m) => ..., // m.mode: modes::Mode
_ => {}
}
Ok(())
}
// optional: fn vt_raw(&mut self, &parser::Action) -> Result<bool, Error> (Zig vtRaw; true = skip)
}
let mut s = Stream::new_alloc(handler); // sets osc_parser.alloc = true (Zig initAlloc)
s.next_slice(bytes)?; // fast path; s.next(byte)? is scalar
s.handler / s.parser / s.utf8decoder // public fields
```
- `Stream::new` = Zig `init` (alloc flag false: OSC 4/5/10-19/104/110-119/133/21/52-style commands needing alloc are rejected). Real terminals should use `new_alloc`.
- Variant names are CamelCase of the Zig tag (`.cursor_up` -> `Action::CursorUp`, `.erase_display_below` -> `EraseDisplayBelow(bool protected)`, `.kitty_keyboard_set_or` -> `KittyKeyboardSetOr`). `Action::key() -> ActionKey` is Zig `Action.Key`.
- Payload struct names/fields match Zig (`CursorMovement{value}`, `CursorPos{row,col}`, `Margin{top_left,bottom_right}`, `Mode{mode}`, `RawMode{mode,ansi}`, `InvokeCharset{bank,charset,locking}`, `ConfigureCharset{slot,charset}`, ...).
Count-style payloads that are `usize` in Zig (`PrintRepeat`, `InsertBlanks`, `DeleteChars`, `EraseChars`, `InsertLines`, `DeleteLines`, `ScrollUp/Down`) are `usize`; `HorizontalTab(u16)`, `HorizontalTabBack(u16)`, `TitlePush/Pop(u16)`, `KittyKeyboardPop(u16)`.
- Payloads borrow from the decoded OSC command (`Action<'a>`): `WindowTitle{title:&str}` (validated UTF-8), `ReportPwd{url:&[u8]}`, `ShowDesktopNotification{title,body}`, `StartHyperlink{uri, id:Option<&[u8]>}`, `ClipboardContents{kind,data}`, `ColorOperation{op, requests:&List, terminator}`, `KittyColorReport(&kitty::color::OSC)`, `SemanticPrompt(&osc::SemanticPrompt)`, `SetAttribute(sgr::Attribute<'a>)`. Copy what you need to keep. `DcsHook(parser::Dcs)` is owned/Copy; `DcsPut/ApcPut` get single bytes.
- `ProgressReport(osc::ProgressReport)` and `MouseShape(MouseShape)` by value.
- Errors returned from `vt` abort the current `next`/`next_slice` call and propagate (Zig `try`). Errors from `vt_raw` are logged and the action skipped (as in Zig).
- `impl Handler for &mut H` exists, so `Stream::new(&mut handler)` works (Zig pointer handlers). Zig `deinit` = `Drop`.
- Mirror `terminal/stream_readonly.zig` and `termio/stream_handler.zig` with a single `match action { ... }`; unhandled variants fall into `_ => {}`.
## Behavior notes / deviations
- Logging uses the `log` crate with the same messages/levels as Zig.
- `next_slice` processes input in 4096-byte chunks like Zig; ESC is located with `memchr`; ground-state text is decoded in bulk (`simd::utf8_decode_until_control_seq`); a stray/partial byte is handed to the scalar DFA decoder and decoding of the rest of the chunk then resumes on the fast path (Zig fed the entire remainder through the scalar path, which would also run `next_utf8` outside the ground state if an ESC followed; same output otherwise).
- Codepoints 0x00-0x0F are executed, all other codepoints (including 0x10-0x1F, 0x7F, C1 from UTF-8) are printed, exactly like Zig's SIMD/scalar paths. Raw C1 bytes reach `execute` only via the parser and are mapped to `ESC c-0x40`.
- CSI W: Zig logs a spurious "invalid cursor tabulation control" after successfully handling `CSI 2 W` / `CSI 5 W`; the Rust port does not log in that case (log-only difference).
- CSI K accepts params 0..2 only, so `EraseLineRightUnlessPendingWrap` (4) is unreachable from CSI, as in Zig. DECRQM only handles `CSI ? Ps $ p` (ANSI `CSI Ps $ p` is "unimplemented", as in Zig).
- Test handlers use a small closure-based `F<T, C>` helper instead of per-test handler structs. Zig's `Stream(H).init(.{})` tests use `Stream::new`.