Engine
wormhole-vt, the engine under Wormhole
A terminal engine in Rust. Bytes from a program go in; the screen, the scrollback and the events the program caused come out. Wormhole runs on it, and it is a library of its own: MIT licensed, so other projects can build on it too.
Status
Version 0.1, beta. Until 1.0 the API may change between minor versions.
The crate
MIT · Rust 1.88 or later · depends on bitflags and memchr
Why
A terminal engine you can read and embed
01
Written from the specifications
The parser follows Paul Williams' DEC state machine; the sequences follow xterm's Control Sequences, the DEC manuals and ECMA-48. Each conformance case names the line of text it rests on. Nothing is copied or ported from another terminal.
02
Fast, and steady about it
Runs of printable ASCII are found a word at a time, and stored cells are eight bytes. With the same terminal and more output, most kinds of output allocate nothing, and counting tests fail when that changes.
03
Safe against hostile output
A terminal reads whatever a program prints. The parser is total, every string and table has a cap, and anything risky (the clipboard, window control, reading the screen back) is a request to the host, off until the host allows it.
04
Made to be embedded
The host owns the PTY, the threads and the drawing. The engine tells it what changed, so a frame copies only the rows that did.
05
No unsafe code, two dependencies
The engine forbids unsafe. It depends on bitflags and memchr, and nothing else: no PTY, no window, no threads, no I/O.
06
Tested from both ends
Every conformance case runs twice, with the bytes fed whole and a byte at a time. CI runs on Linux, macOS and Windows, with fuzzing, mutation testing and three outside suites as gates.
Conformance
Tested against the text, not against another terminal
- 875
- conformance cases, 752 quoting the published text they follow, 123 marked as conventions
- 544 / 567
- esctest2 tests pass; 21 of the failures are Xcms colour forms, two want a printer port
- 15,012
- of 15,012 ucs-detect sequences sized as expected, with grapheme mode 2027 on
Against other engines
The cases are the project's own, written from the same specifications as the engine. Passing all of them means the suite and the engine agree, not that the engine is complete; another engine's failure is a difference from this reading of the text.
| Engine | Cases passed, of 758 |
|---|---|
| wormhole-vt | 758 of 758 |
| libghostty-vt (a commit of main) | 558 |
| wezterm-term (a commit of main) | 484 |
| alacritty_terminal 0.26.0 | 447 |
| vt100 0.16.2 | 313 of the 581 it can be asked |
Speed
Fast on the output terminals actually get
| Output | wormhole-vt | alacritty_terminal | libghostty-vt | wezterm-term | vt100 |
|---|---|---|---|---|---|
| ascii-flood | 2,764 | 233 | 1,800 | 67 | 257 |
| log-lines | 1,528 | 248 | 1,207 | 66 | 241 |
| sgr-churn | 379 | 232 | 165 | 49 | 253 |
| unicode-mix | 330 | 207 | 414 | 70 | 212 |
| tui-redraw | 754 | 244 | 465 | 42 | 210 |
| scroll-region | 809 | 212 | 648 | 57 | 220 |
| osc8-links | 469 | 191 | 27 | 70 | 299 |
| payloads | 569 | 422 | 1,008 | 291 | 389 |
| agent-session | 534 | 320 | 231 | 71 | 353 |
The fastest in each row is in colour; libghostty-vt leads on unicode-mix and payloads. One run on a laptop (Apple M5 Pro, 2026-10-15) with other work going on, so a guide and not a verdict: small differences prove nothing, the engines compute different things while they parse, and each runs behind an adapter the project wrote. The engine has been tuned since, and the table has not been measured again. Rerun it yourself with cargo run --release --bin vt-bench in tools/vt-compare.
Use it
A screen in a few lines
Cargo.toml
Not on crates.io yet: until it is, depend on the repository. The crate has no features for users.
[dependencies]
wormhole-vt = { git = "https://github.com/wormhole-terminal/wormhole-vt" }The quick start
Feed it bytes in any chunking, write its replies back to the PTY, and read what changed. examples/quickstart.rs in the repository is the whole program.
use wormhole_vt::{Event, Terminal};
let mut term = Terminal::new(80, 24);
// Bytes from the program, in any chunking: a sequence may be cut anywhere.
term.feed(b"\x1b[1;31mhello\x1b[0m\r\n");
term.feed(b"\x1b]2;my title\x07");
term.feed(b"\x1b[6n"); // the program asks where the cursor is
// What the program is waiting to read: write these bytes back to the PTY.
assert_eq!(term.take_replies(), b"\x1b[2;1R");
// What happened besides drawing.
let events: Vec<Event> = term.events().collect();
assert!(events.iter().any(|event| matches!(event, Event::Title)));
assert_eq!(term.title(), Some("my title"));What it does
From VT100 to the kitty protocols
- Parser
- The DEC/ANSI state machine, UTF-8, C0 and C1, CSI with sub-parameters, and OSC, DCS and APC strings streamed in pieces. Usable on its own, without a screen.
- Conformance levels
- VT100 to VT500, the device attributes and status reports, XTGETTCAP, XTVERSION and VT52 mode.
- Screen editing
- Scrolling regions and left/right margins, insert and delete, tab stops, origin mode, delayed wrap, selective erase, double-width and double-height lines, rectangular operations.
- Text style and colour
- Bold to overline, five underline styles and underline colour, 16, 256 and true colours, palette and dynamic colours with queries.
- Strings and marks
- Titles, folders (OSC 7), hyperlinks (OSC 8), shell marks (OSC 133), progress and notifications, the clipboard as a host request.
- Images
- The kitty graphics protocol with Unicode placeholders, Sixel decoded by the engine, and iTerm2 inline images. The engine places them; the host draws.
- Unicode
- Unicode 18 cell widths, extended grapheme clusters with mode 2027 on by default, emoji variation selectors.
- Scrollback and reflow
- History capped in bytes; soft-wrapped lines rewrap on resize, with the cursor and pins carried along.
- Keyboard and mouse
- The kitty keyboard protocol, modifyOtherKeys, bracketed paste and focus; mouse modes 9, 1000, 1002 and 1003 in five encodings, and the DEC locator.
- Host side
- Damage tracking and row copies into reusable buffers, selection and text extraction, pins and spans that follow text through scrolling and reflow.
- Repaint writer
- Serializes a terminal into the VT bytes that rebuild it in another, for attach and resync.
- Policy
- What a program may do beyond printing (window operations, reading the screen back) stays off until the host allows it.
The guide
Documentation in the repository
07/Contribute
Bring the bytes
A bug report with the bytes that cause it is the most useful thing there is: a hex dump or a file, what the screen shows, and what another terminal (xterm, ideally) shows instead. Security problems go through the security policy, not a public issue.