oxi-tui
ratatui-based widgets, theming, and a differential-rendering backend for oxi.
Overview
oxi-tui is a widget, theme, and tape-rendering library built on top of
ratatui 0.30 + crossterm
0.29. It does not ship its own event loop. Production oxi-cli renders chat
transcripts on the terminal main screen through tape::TapeEngine; ratatui
remains for transient overlays and off-screen line formatting.
It provides:
- A differential-rendering backend (
DiffBackend) that wrapsCrosstermBackend, diffs frame buffers line-by-line, and flushes inside a CSI 2026 synchronized-update window — flicker-free streaming without the per-frame I/O cost of a full repaint. - A typed theme system (
Theme/ThemeManager/ThemeStyles) with hot-reload from TOML or JSON, plus Catppuccin-inspired dark/light presets. - A glyph-set system (
GlyphSet: Unicode / ASCII / Nerd Font) so the entire UI degrades cleanly from emoji-rich terminals down to 7-bit serial consoles. - Production widgets: a streaming
ChatView, multi-lineInput, fuzzyCompletionPopup,DashboardWidget,Footer,RoutingStatus,TodoPanel, and genericStatefulList/TableList. - Rendering helpers: terminal capability detection, inline image (Kitty / iTerm2) decoding, ANSI-width text utilities, unified-diff coloring, and Mermaid → ASCII diagram rendering.
- CJK-aware line wrapping and wide-character-safe cell writes
(
Buffer::set_line) — ratatui's built-inWordWrapperdoes not CJK-break.
oxi-tui has no
oxi-*dependencies. It is a pure widget library any ratatui application can adopt.
Quick Start
Add to your Cargo.toml:
[]
= { = "path/to/oxi-tui" }
= "0.30"
= "0.29"
The host owns the terminal. Ordinary chat output can be painted on the main
screen through TapeEngine; ratatui remains useful for transient overlays:
use ;
use stdout;
Architecture
oxi-tui
├── tape/ TapeEngine, memoized Container, TranscriptRenderer, ANSI style
├── render/ DiffBackend (overlay), capability probe, inline image,
│ ANSI/diff coloring, Mermaid ASCII
├── theme.rs Theme, ColorScheme, Spacing, ThemeStyles, ThemeManager
├── symbols.rs GlyphSet (Unicode/ASCII/Nerd) → Symbols table
├── widgets/ ratatui widgets (chat, input, completion, dashboard, …)
├── keybindings/ declarative key registry + conflict detection
└── lib.rs re-exports the public surface
Each widget is a ratatui StatefulWidget (or Widget) with a sibling
*State. The widget layer defines its own domain types
(ChatMessage, MessageRole, ContentBlock, ToolCallStatus) so it can be
reused by any product; products implement the conversion in their own
composition root.
Differential Rendering
DiffBackend<W> implements ratatui::backend::Backend. On each draw():
- Ratatui builds the frame buffer as usual.
- The backend collects cells into per-row byte rows and checksums each.
- Only rows whose checksum differs from the previous frame are written.
- The whole repaint is wrapped in
\x1b[?2026h … \x1b[?2026l(synchronized output) to eliminate mid-frame tearing.
use DiffBackend;
use ;
let backend = new;
let mut terminal = new?;
// Force a full repaint on the next frame (e.g. after a resize you handled):
backend_ref.invalidate;
This matters most for streaming AI chat, where the overwhelming majority of the
screen is static between frames. The ChatView widget additionally caches its
computed layout and only recomputes when messages / width / spinner change.
Theme System
use ;
let mut manager = dark; // or ::light(), ::new(theme)
// Hot-reload a file: applies it immediately, then polls for changes.
manager.watch_file?;
// In your event loop:
if manager.check_reload
manager.set_theme_by_name; // switch programmatically
let theme = manager.theme; // &Theme for widget constructors
A Theme is a typed ColorScheme + Spacing + active Symbols. Widgets
consume theme.to_styles() → ThemeStyles (a Copy bundle of ratatui::Styles),
so there are zero allocations per frame for styling.
Color formats (theme files)
Hex, named, or indexed — accepted in both TOML and JSON:
= "midnight"
[]
= "#cdd6f4"
= "#1e1e2e"
= "#89b4fa"
= "#f38ba8"
= "#a6e3a1"
Color accepts #rrggbb / #rgb, named ANSI (red, bright-black), or
indexed (i42).
Glyph Sets
One setting switches every box-drawing, status, and spinner glyph across the whole UI, so the app renders correctly from a Nerd-Font terminal down to a 7-bit serial console:
use ;
let symbols = unicode; // default — works on any UTF-8 terminal
let symbols = ascii; // 7-bit fallback for CI logs / serial
let symbols = nerd; // richest icons; needs a patched font
GlyphSet is stored in settings.toml as glyph_set = "unicode". Symbols
is Copy and rides inside ThemeStyles with no allocation.
Widgets
| Widget | State type | Notes |
|---|---|---|
chat::ChatView |
ChatViewState |
Streaming message list, tool-call boxes, markdown, layout cache |
Input |
InputState |
Multi-line editor (ratatui-textarea), undo/redo, history |
CompletionPopup |
CompletionState |
Fuzzy file/slash-command autocomplete |
DashboardWidget |
DashboardState |
Sectioned status dashboard |
Footer |
FooterState |
Model / provider / context status line |
RoutingStatus |
RoutingStatusState |
Auto-routing & fallback panel |
TodoPanel |
TodoPanelState |
Phased todo list |
StatefulList<T> |
— | Generic filterable list with ratatui list state |
TableList<T> |
— | Generic multi-column table with selection |
Plus tool_renderer (state-aware formatting of tool calls / results / diffs)
and table_renderer (compact table → line rendering).
Keybindings
A declarative layer over crossterm key events, with normalized KeyId,
a default Action registry, user rebinding, and conflict detection:
use ;
let mgr = new;
let key_id = from;
match mgr.match_action
Rendering Helpers
- Terminal capabilities (
render::terminal): detects image protocol (Kitty / iTerm2), true color, OSC 8 hyperlinks, Kitty keyboard protocol, and cell size. - Inline images (
render::image): PNG / JPEG / GIF / WebP decode and Kitty / iTerm2 inline emission. - ANSI utilities (
text,render::ansi): display-width-aware truncation, wrapping, and string-width that ignore SGR escapes. - Diffs (
render::diff): colored unified-diff rendering with+/-stat counting. - Mermaid (
render::mermaid): pure-Rust Mermaid → ASCII diagram renderer (flowchart, sequence, state, class) with a process-level render cache. No externalmmdc/ Node dependency — single-binary self-contained.
Conventions
parking_lot::RwLockoverstd::sync::RwLock; never hold a guard across an.await.- Atomic file writes use the temp + rename pattern.
- Shipped (non-test) code
warns onclippy::unwrap_used; tests relax that one lint via#![cfg_attr(test, allow(...))].