rio-vt
Rio's embeddable terminal core: the VT state machine, ANSI/escape parser, grid with scrollback, selection, search, and PTY-facing event model, extracted from Rio as a standalone, dependency-light crate.
It carries no rendering, GPU, or font-shaping dependencies. You pair
it with a renderer of your choice (Rio uses sugarloaf) or drive it purely
from the pulled grid state. It is the safe-Rust sibling of
librio, which wraps this same core in a C ABI for
embedding from Swift, C, Go (cgo), and other languages.
- Rust embedders depend on
rio-vtdirectly (this crate). - Non-Rust embedders link
librio(C ABI) + its generated header.
Add it
[]
= "0.5"
rio-vt is lean by default. The crosswords VT logic, the parser, grid,
selection and search all compile with no windowing or renderer stack.
Feature flags
| Feature | Default | Effect |
|---|---|---|
| (none) | ✓ | Lean core. No renderer, no windowing. |
rio-window |
From conversions between the core's WindowId and rio_window::window::WindowId, plus the event-loop proxy listener. |
|
renderer |
Expose the app-level RioErrorType::FontsNotFound variant (pulls a sugarloaf font type). Enabled transitively by rio-backend's renderer. |
|
x11 / wayland |
Forward the matching clipboard backend feature. |
WindowId is always the core's own WindowId(u64) (with
From<u64>/Into<u64>); rio-vt never aliases it to the windowing
layer's type. The rio-window feature only adds the boundary From
conversions.
Quick start (headless)
Create a grid, feed it bytes through the parser, and read cells back. No PTY, no renderer, no threads:
use CursorShape;
use Dimensions;
use Column;
use ;
use ;
use Processor;
// An 80x24 terminal with 10k lines of scrollback.
let size = new;
let cols = size.columns;
let mut term = new;
// Feed raw PTY bytes (SGR red "hello", reset, then " world").
let mut parser = default;
parser.advance;
// Pull the visible grid and read the first row's text.
let rows = term.visible_rows;
let line: String = .map.collect;
assert!;
The full, runnable version of this snippet lives at
examples/quickstart.rs(cargo run -p rio-vt --example quickstart).
Examples
Each is runnable with cargo run -p rio-vt --example <name>:
| Example | Shows |
|---|---|
quickstart |
Create a grid, feed bytes, read cells back. |
sixel |
Decode a Sixel image, captured via RioEvent::UpdateGraphics. |
kitty_image |
Transmit a Kitty graphics-protocol image and capture it. |
selection |
Select a range and read it back as text. |
resize |
Resize the terminal and watch the grid follow. |
The sixel and kitty_image examples double as a reference for the
event-driven graphics flow: received images are drained into a
RioEvent::UpdateGraphics event (with pending for Sixel/iTerm2 and
pending_images for Kitty), which a frontend records through its
EventListener.
Receiving events
Instead of VoidListener, implement EventListener to react to what the
terminal emits (PTY writes, bell, title changes, clipboard requests, and so
on). event() is the only required method; override the send_* hooks you
care about:
use ;
;
Driving a real PTY
performer::Machine wires a spawned PTY to the parser and the grid on a
reader thread, so bytes from the child process flow into Crosswords
automatically. See librio/src/lib.rs for a
complete, working setup (Crosswords::new + Machine::new +
teletypewriter), which is also the reference consumer of this crate.
On child exit, the reader drains available output across parsing budgets and
flushes pending synchronized updates before publishing ChildExited and
CloseTerminal. It does not wait for descendants holding the slave open:
a read that would block ends the drain, and continuous descendant output is
limited to 100 ms between parsing batches. Terminal-lock waits and parsing
can extend that interval.
Send Msg::Shutdown to close a running PTY. On Unix, shutdown sends SIGHUP,
allows a 100 ms grace period, then uses SIGKILL if necessary and waits to
reap the child. Repeated shutdown preserves its exit status and does not
signal a reaped child. The final reap can take longer for a process stuck
in the kernel; shutdown is not a strict wall-clock deadline. Embedders
should let the PTY owner manage termination instead of signaling a saved PID.
Pull-based rendering
rio-vt does not draw anything. A frontend reads terminal state on demand:
term.visible_rows()returns the current viewport asVec<Row<Square>>.Square::c()gives a cell's character; foreground/background/flags come from its style.- Damage tracking (
TerminalDamage) lets a renderer repaint only changed rows instead of the whole grid each frame.
For a complete pull implementation (dirty-row tracking, per-cell readout,
cursor and selection state), see
librio/src/render_state.rs.
C ABI
To embed from a non-Rust UI, use librio. It builds this
crate as a staticlib and exposes an engine/surface/render-state C API
(rio_engine_new, rio_surface_text, rio_render_state_cell, ...). Rio's
SwiftUI + Metal frontend drives it that way.
Architecture
rio-graphics image/graphic value types + glyph decoder (leaf, no GPU)
|
rio-vt this crate: VT core (crosswords, ansi, performer, event, ...)
| \
rio-backend librio (C ABI)
|
rioterm Rio's terminal app (adds the sugarloaf renderer + config)
rio-backend builds on rio-vt and re-exports every module at the same
path, so it stays the home of the user-facing config and the renderer glue
while this crate carries the reusable core.
Safety
The public API of rio-vt is safe Rust. All unsafe and raw-pointer
handling lives in the separate librio C-ABI shim.