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:

Workloadrio-vtvt100alacrittywinner
mixed302 MiB/s221 MiB/s254 MiB/srio-vt
ascii_plain835 MiB/s196 MiB/s279 MiB/srio-vt 3.0×
sgr_churn235 MiB/s349 MiB/s332 MiB/svt100
scroll_storm274 MiB/s101 MiB/s266 MiB/srio-vt
alt_screen_redraw588 MiB/s231 MiB/s282 MiB/srio-vt 2.1×
unicode_wide248 MiB/s203 MiB/s337 MiB/salacritty

Screen serialization (lower is better):

Operationrio-vtvt100winner
contents_formatted (ANSI)4.3 µs18.6 µsrio-vt 4.3×
contents_plain (text)3.8 µs13.9 µsrio-vt 3.7×

Resize 80x24 to 100x40 and back:

Operationrio-vtvt100alacrittywinner
resize5.0 µs7.5 µs227 µsrio-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.