A minimal terminal-emulator core: child VT output bytes in, a live
ansi::Screen out. Feed it a shell or TUI's byte stream; read back an
in-memory grid of styled cells you can composite into a sub-rectangle and diff
onto a real terminal.
Part of the nativelite agent terminal suite. Built for atrium's tiled panes, where a pane no longer owns the whole screen, so its whole-screen escape sequences (cursor moves, clears, scroll regions, wrapping) must be interpreted into a grid and painted at an offset. Useful anywhere child output is rendered into a region: log viewers, test-runners, dashboards.
- Zero third-party dependencies. Standard library plus the org crates
ansi(screen model) anduwidth(character display width) only. Nothing third-party from crates.io. - One concern.
ansiparses and diffs;vtermis the state machine that applies parsed tokens to aScreen. No I/O: no PTY, no raw mode.
let mut t = vterm::Term::new(24, 80);
t.feed(b"\x1b[2J\x1b[3;5HX"); // clear, cursor to row 3 col 5, print 'X'
let s = t.screen();
assert_eq!(s.cell(2, 4).ch, 'X');
assert_eq!(s.cursor(), ansi::Cursor::new(2, 5)); // advanced past the 'X'The whole API is Term and its error type RestoreError:
pub struct Term { /* private */ }
impl Term {
pub fn new(rows: usize, cols: usize) -> Self;
pub fn with_history(rows: usize, cols: usize, lines: usize) -> Self; // keep scrollback
pub fn feed(&mut self, bytes: &[u8]); // drive with child output
pub fn take_dirty(&mut self) -> bool; // changed since the last call?
pub fn screen(&self) -> &ansi::Screen; // active buffer, cursor included
pub fn scrollback(&self) -> &ansi::Screen; // primary buffer + its history
pub fn in_alternate_screen(&self) -> bool;
pub fn in_sync(&self) -> bool; // inside a ?2026 synchronized update
pub fn cursor_visible(&self) -> bool; // ?25h (default) / ?25l
pub fn bracketed_paste(&self) -> bool; // ?2004h: wrap pastes in ESC[200~ ESC[201~
pub fn resize(&mut self, rows: usize, cols: usize);
// Save and rebuild the whole emulator state.
pub fn can_checkpoint(&self) -> bool; // false mid-sequence / mid-UTF-8
pub fn checkpoint(&self) -> Option<Vec<u8>>;
pub fn restore(bytes: &[u8]) -> Result<Term, RestoreError>;
}feed chunks may split at any byte boundary: mid-escape, mid-UTF-8. The
internal ansi::Parser holds partial state across calls, so the final screen
is independent of how the bytes were chunked. screen() returns the active
buffer (the alternate buffer while in alt-screen, else primary) with its
cursor field set to the emulator's position, ready for Screen::diff. During
a synchronized update it returns the last complete frame instead.
The honest MVP, enough to host a shell and the common agent TUIs correctly:
- Cursor motion:
CUP/HVP(H,f),CUU/CUD/CUF/CUB(A–D),CNL/CPL(E,F),CHA(G),VPA(d);CR,LF/VT/FF(with scroll at the bottom margin),BS,HT(tab stops every 8 columns). - Erase:
ED(0J/1J/2J, and3Jto clear the scrollback),EL(0K/1K/2K). - Scroll region:
DECSTBM(t;b r) confining scroll-on-LF;IL/DL(insert/delete line),ICH/DCH(insert/delete char). - Style:
SGR(m) viaansi::Style::apply_sgr: full color and attributes. - Autowrap (
DECAWM, default on) with the deferred pending-wrap latch at the right edge. - Cursor save/restore:
DECSC/DECRC(ESC 7/ESC 8) and CSIs/u. - Cursor show/hide:
?25h/?25l, read withcursor_visible()(checkpointed). - Bracketed paste:
?2004h/?2004l, read withbracketed_paste()(checkpointed); the host wraps what it pastes. - Alt-screen:
?1049h/l,?47h/l,?1047h/l: swap to a cleared second buffer so a full-screen app does not scribble the primary one. - Synchronized output:
?2026h/?2026l; seein_syncabove. - New-line mode:
LNM(CSI 20 h/l): LF, VT and FF also return to column 0. - Wrapped lines: a row that autowraps is marked on the screen
(
Screen::row_wrapped, andhistory_wrappedonce it scrolls off), so a host can copy a wrapped line as one; erasing to the end of the row clears the mark. Checkpointed. - Wide characters: a double-width glyph (CJK, emoji; width from
uwidth) takes two cells and advances the cursor by two. At the right edge it wraps whole, or is dropped when autowrap is off; it is never split.
Titles (OSC), charset selection (ESC ( X), and unrecognized private modes
are accepted and ignored quietly.
vterm is a correct common core, not a pixel-perfect xterm. Deliberately
out of scope; the host app's answer for these is raw passthrough (in atrium,
"zoom" the pane to full-screen and the bytes go straight to your real terminal):
- Combining marks and other zero-width characters. A cell holds one
char, so these are dropped rather than composed onto the previous glyph. - Sixel / images / mouse reporting / bracketed-paste semantics inside a tile.
- Exotic private modes beyond the set listed above.
This boundary is the point: a correct common core plus an honest escape hatch, not a leaky imitation of xterm.
python dev.py check # dependency guard + cargo fmt --check + cargo test (the pre-push gate)
python dev.py test # cargo test
python dev.py fmt # cargo fmt --check
python dev.py guard # dependency guard (org crates only, no third-party)
MIT: see LICENSE. nativelite ships everything permissively.