Rio 0.5 splits its terminal core into embeddable layers
Rio 0.5 decouples its terminal engine from the renderer and app, shipping as two layers: rio-vt (a safe Rust crate) and librio (a C ABI). The goal: let any project reuse the hard-won parts—VT state machine, grid, scrollback, selection, search, and image protocols—without inheriting the whole terminal app.
rio-vt: the Rust core
rio-vt is a safe Rust crate that includes the escape parser, grid with scrollback, selection, search, PTY driver, and sixel / Kitty / iTerm2 image protocols. It has no rendering, GPU, or font shaping dependencies by default. You feed it bytes, then pull the grid state out.
Here's the entire loop, 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;
let size = CrosswordsSize::new(80, 24);
let cols = size.columns();
let mut term = Crosswords::new(
size, CursorShape::Block, VoidListener, WindowId::from(0), 0, 10_000,
);
let mut parser = Processor::default();
parser.advance(&mut term, b"\x1b[31mhello\x1b[0m world");
let rows = term.visible_rows();
let line: String = (0..cols).map(|x| rows[0][Column(x)].c()).collect();
assert!(line.starts_with("hello world"));
Selection and search come built in:
use rio_vt::crosswords::pos::{Column, Line, Pos, Side};
use rio_vt::selection::{Selection, SelectionType};
let mut selection = Selection::new(
SelectionType::Simple, Pos::new(Line(0), Column(0)), Side::Left,
);
selection.update(Pos::new(Line(0), Column(4)), Side::Right);
term.selection = Some(selection);
assert_eq!(term.selection_to_string().as_deref(), Some("hello"));
Image protocols are decoded by the core and surfaced via an event listener. Here's a Kitty graphics transmission captured headless:
impl EventListener for ImageSink {
fn event(&self) -> (Option, bool) { (None, false) }
fn send_event(&self, event: RioEvent, _: WindowId) {
if let RioEvent::UpdateGraphics { queues, .. } = event {
for (id, g) in &queues.pending_images {
// id, g.width, g.height, g.pixels (RGBA) ...
}
}
}
}
parser.advance(&mut term, b"\x1b_Gf=32,s=2,v=2,a=T;/wAA//8AAP//AAD//wAA/w==\x1b\\");
librio: the same core via C ABI
librio wraps rio-vt in a C ABI, in the spirit of libghostty. It ships as a prebuilt static library (librio.a + librio.h), so you can use it from Swift, C, Go, Python, or any language that calls C. No Rust toolchain required.
Example C usage:
#include "librio.h"
rio_engine_t *engine = rio_engine_new(&config);
rio_surface_t *surface = rio_surface_new(engine, &desc);
rio_surface_text(surface, "ls -la\n", 7);
rio_render_state_t *state = rio_render_state_new(surface);
rio_render_state_update(state);
for (uint16_t line = 0; line < rio_render_state_lines(state); line++) {
if (!rio_render_state_row_dirty(state, line)) continue;
for (uint16_t col = 0; col < rio_render_state_columns(state); col++) {
rio_cell_s cell = rio_render_state_cell(state, line, col);
// hand `cell` to your own renderer...
}
}
rio_render_state_reset_dirty(state);
The dirty-row render state enables efficient CPU rendering: only repaint rows that changed. The C ABI is cross-platform (macOS, Linux, Windows), using ConPTY on Windows.
Already in production
Rio's core is not a science project. rio-vt already powers real terminal workloads at companies like Lovable. Extracting the core into its own crate was a deliberate move to let other products build on the same engine Rio ships.
Performance benchmarks
The benchmark suite (rio-vt-benchmark) compares rio-vt against vt100 and alacritty_terminal on parsing, serialization, and resize. Criterion medians on Apple Silicon, 80x24 terminal:
| Workload | rio-vt | vt100 | alacritty | winner |
|---|---|---|---|---|
| mixed | 302 MiB/s | 221 MiB/s | 254 MiB/s | rio-vt |
| ascii_plain | 835 MiB/s | 196 MiB/s | 279 MiB/s | rio-vt 3.0× |
| sgr_churn | 235 MiB/s | 349 MiB/s | 332 MiB/s | vt100 |
| scroll_storm | 274 MiB/s | 101 MiB/s | 266 MiB/s | rio-vt |
| alt_screen_redraw | 588 MiB/s | 231 MiB/s | 282 MiB/s | rio-vt 2.1× |
| unicode_wide | 248 MiB/s | 203 MiB/s | 337 MiB/s | alacritty |
Screen serialization (lower is better):
| Operation | rio-vt | vt100 | winner |
|---|---|---|---|
| contents_formatted (ANSI) | 4.3 µs | 18.6 µs | rio-vt 4.3× |
| contents_plain (text) | 3.8 µs | 13.9 µs | rio-vt 3.7× |
Resize 80x24 to 100x40 and back:
| Operation | rio-vt | vt100 | alacritty | winner |
|---|---|---|---|---|
| resize | 5.0 µs | 7.5 µs | 227 µs | rio-vt |
rio-vt parses faster on most shapes, especially plain glyphs and full-screen repaints. It serializes screens several times faster and resizes far quicker, while still reflowing wrapped lines (vt100 skips reflow, alacritty reflows but is two orders of magnitude slower).
Not a clean sweep: vt100 wins sgr_churn due to rio-vt's per-cell style interning, and alacritty wins unicode_wide. The benchmark is public for that reason.
Where this is going
rio-vt is on crates.io, versioned alongside Rio (0.5). librio ships as a RioKit.xcframework for Swift and librio.a + librio.h for C on macOS; on Linux and Windows, build from source with cargo build -p librio. WebAssembly support is planned.
If you've ever wanted to embed a real terminal in a Rust app, a native macOS app, or something else behind the C ABI, this is for you. Try it, and report back to the Rio team.




