rio-vt/README.md
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.
rio-vt directly (this crate).librio (C ABI) + its generated header.[dependencies]
rio-vt = "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 | 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.
Create a grid, feed it bytes through the parser, and read cells back. No PTY, no renderer, no threads:
use rio_vt::ansi::CursorShape;
use rio_vt::crosswords::grid::Dimensions;
use rio_vt::crosswords::pos::Column;
use rio_vt::crosswords::{Crosswords, CrosswordsSize};
use rio_vt::event::{VoidListener, WindowId};
use rio_vt::performer::handler::Processor;
// An 80x24 terminal with 10k lines of scrollback.
let size = CrosswordsSize::new(80, 24);
let cols = size.columns();
let mut term = Crosswords::new(
size,
CursorShape::Block,
VoidListener, // ignore events; swap for your own EventListener
WindowId::from(0),
0, // route id
10_000, // scrollback limit
);
// Feed raw PTY bytes (SGR red "hello", reset, then " world").
let mut parser = Processor::default();
parser.advance(&mut term, b"\x1b[31mhello\x1b[0m world");
// Pull the visible grid and read the first row's text.
let rows = term.visible_rows();
let line: String = (0..cols).map(|x| rows[0][Column(x)].c()).collect();
assert!(line.starts_with("hello world"));
The full, runnable version of this snippet lives at
examples/quickstart.rs(cargo run -p rio-vt --example quickstart).
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.
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 rio_vt::event::{EventListener, RioEvent, WindowId};
#[derive(Clone)]
struct MyListener;
impl EventListener for MyListener {
// Required: return a pending event (used by pull-style consumers).
fn event(&self) -> (Option<RioEvent>, bool) {
(None, false)
}
// Optional: react to events the terminal pushes out.
fn send_event(&self, event: RioEvent, _window: WindowId) {
match event {
RioEvent::PtyWrite(_route_id, text) => { /* write `text` to the PTY */ }
RioEvent::Title(title) => { /* update the window title */ }
RioEvent::Bell => { /* ring */ }
_ => {}
}
}
}
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.
rio-vt does not draw anything. A frontend reads terminal state on demand:
term.visible_rows() returns the current viewport as Vec<Row<Square>>.Square::c() gives a cell's character; foreground/background/flags come
from its style.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.
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.
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.
The public API of rio-vt is safe Rust. All unsafe and raw-pointer
handling lives in the separate librio C-ABI shim.