saorsa-core
A retained-mode, CSS-styled terminal UI framework for Rust.
Overview
saorsa-core is a full-featured TUI framework that brings web-like development patterns to the terminal:
- Retained-mode rendering - Widgets persist in a tree; the framework handles diffing and efficient updates
- CSS-styled - Style everything with TCSS (Terminal CSS), including variables, themes, and live hot-reload
- Reactive state - Signal-based reactivity with automatic dependency tracking and batch updates
- Rich widget library - 24+ built-in widgets: tables, trees, markdown, diffs, modals, sparklines, and more
- Compositor - Layer-based rendering with z-ordering, clipping, and overlay support
- Differential rendering - Double-buffered with SGR-optimized escape sequences; only changed cells are written
- Full Unicode support - Grapheme clusters, CJK wide characters, emoji sequences, combining marks
Architecture
┌─────────────────────────────────────────────────────────────┐
│ Application Layer │
│ (Widget tree, CSS styles, reactive signals & bindings) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Layout Engine (Taffy) │
│ TCSS → ComputedStyle → taffy::Style → computed rects │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Widget Rendering System │
│ Widget::render() → Vec<Segment> → Lines of styled text │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Compositor (Layers, Z-ordering, Clipping) │
│ Base layer + overlays → CompositorRegion → final buffer │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Renderer (Differential, SGR optimization) │
│ ScreenBuffer → DeltaBatch → optimized escape sequences │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Terminal Backend (Crossterm) │
│ Raw mode, cursor control, alternate screen, events │
└─────────────────────────────────────────────────────────────┘
Quick Start
Add saorsa-core to your Cargo.toml:
[]
= "0.1"
Render a styled label into a screen buffer:
use ;
// Create a screen buffer (80x24 terminal)
let size = new;
let mut buf = new;
// Create a styled label
let style = new
.fg
.bold;
let label = new
.style;
// Render into a region
let area = new;
label.render;
Split a terminal area into layout regions:
use ;
let area = new;
// Vertical layout: 3-line header, fill for content, 1-line footer
let regions = split;
// regions[0] = header area (80x3)
// regions[1] = content area (80x20)
// regions[2] = footer area (80x1)
Widget Catalog
Text Widgets
| Widget | Description |
|---|---|
Label |
Single-line styled text with alignment (left, center, right) |
StaticWidget |
Renders pre-built Vec<Segment> directly |
TextArea |
Multi-line editable text with undo/redo, selection, and soft wrap (Ropey-based) |
RichLog |
Scrollable log viewer with syntax-highlighted entries |
MarkdownRenderer |
Markdown to styled terminal output (via pulldown-cmark) |
DiffView |
Side-by-side or unified diff display (via similar) |
Data Widgets
| Widget | Description |
|---|---|
DataTable |
Scrollable table with sortable columns, row selection, and keyboard navigation |
Tree |
Hierarchical tree with expand/collapse and keyboard navigation |
DirectoryTree |
Filesystem tree navigator with lazy loading |
SelectList |
Searchable selection list with fuzzy filtering |
OptionList |
Radio-style option selector |
UI Widgets
| Widget | Description |
|---|---|
Container |
Layout container with optional titled border |
Modal |
Centered modal dialog with overlay dimming |
Toast |
Notification popup with configurable position and timeout |
Tooltip |
Contextual tooltip anchored to a position |
Tabs |
Multi-tab interface with configurable tab bar position |
Collapsible |
Expandable/collapsible section with header |
ProgressBar |
Determinate and indeterminate progress display |
LoadingIndicator |
Animated spinner with multiple styles (dots, braille, line, arc) |
Sparkline |
Inline data visualization with bar characters |
Form Controls
| Widget | Description |
|---|---|
Checkbox |
Toggle checkbox ([x] / [ ]) |
RadioButton |
Radio button ((*) / ( )) |
Switch |
Toggle switch with on/off states |
Widget Traits
All widgets implement the Widget trait. Interactive widgets additionally implement InteractiveWidget, and widgets with intrinsic dimensions implement SizedWidget:
/// Render into a screen buffer region.
/// Widget with size preferences for layout.
/// Widget that handles input events.
TCSS (Terminal CSS)
saorsa-core includes a full CSS engine adapted for terminals. Stylesheets are parsed using a Servo-derived cssparser backend.
Selectors
/* Type selector */
}
/* Class selector */
}
/* ID selector */
}
/* Pseudo-classes */
}
}
}
/* Child combinator */
}
/* Descendant combinator */
}
/* Adjacent sibling */
}
Properties
Colors & Text
| Property | Values | Description |
|---|---|---|
color |
Named, #rgb, #rrggbb, indexed |
Foreground color |
background |
Named, #rgb, #rrggbb, indexed |
Background color |
border-color |
Named, #rgb, #rrggbb, indexed |
Border color |
text-style |
bold, italic, underline, strikethrough, dim, reverse |
Text decorations |
text-align |
left, center, right |
Horizontal text alignment |
content-align |
top, middle, bottom |
Vertical content alignment |
Dimensions
| Property | Values | Description |
|---|---|---|
width / height |
Integer, percentage, auto |
Widget dimensions |
min-width / min-height |
Integer | Minimum dimensions |
max-width / max-height |
Integer, none |
Maximum dimensions |
Box Model
| Property | Values | Description |
|---|---|---|
margin |
1-4 integers | Outer spacing |
margin-top/right/bottom/left |
Integer | Individual margins |
padding |
1-4 integers | Inner spacing |
padding-top/right/bottom/left |
Integer | Individual padding |
border |
1-4 values (ascii, round, heavy, double, none) |
Border style |
Flexbox Layout
| Property | Values | Description |
|---|---|---|
display |
flex, grid, block, none |
Display mode |
flex-direction |
row, column, row-reverse, column-reverse |
Main axis |
flex-wrap |
nowrap, wrap, wrap-reverse |
Wrapping behavior |
justify-content |
flex-start, flex-end, center, space-between, space-around, space-evenly |
Main axis alignment |
align-items |
flex-start, flex-end, center, stretch, baseline |
Cross axis alignment |
align-self |
auto, flex-start, flex-end, center, stretch |
Individual cross alignment |
flex-grow / flex-shrink |
Number | Flex factors |
flex-basis |
Integer, auto |
Initial size |
gap |
Integer | Gap between children |
Grid Layout
| Property | Values | Description |
|---|---|---|
grid-template-columns |
Sizes (integer, fr, auto) |
Column track definitions |
grid-template-rows |
Sizes (integer, fr, auto) |
Row track definitions |
grid-column |
start / end |
Column placement |
grid-row |
start / end |
Row placement |
Positioning
| Property | Values | Description |
|---|---|---|
dock |
top, bottom, left, right |
Dock to edge |
overflow |
visible, hidden, scroll, auto |
Overflow behavior |
overflow-x / overflow-y |
Same as overflow |
Per-axis overflow |
visibility |
visible, hidden |
Visibility |
opacity |
0-1 |
Opacity level |
Variables & Theming
Define variables in :root or scoped selectors, reference with $:
}
}
}
}
Built-in Themes
saorsa-core ships with popular color schemes:
| Theme | Variants |
|---|---|
| Catppuccin | catppuccin_latte, catppuccin_frappe, catppuccin_macchiato, catppuccin_mocha |
| Dracula | dracula_dark, dracula_light |
| Nord | nord_dark |
| Solarized | solarized_dark, solarized_light |
Live Hot-Reload
TCSS files can be watched for changes at runtime using the notify-based file watcher. When a stylesheet is modified, it is re-parsed and styles are re-applied without restarting the application.
Layout Engine
Manual Layout
Split areas using constraints:
use ;
let terminal = new;
// Split horizontally: 30-cell sidebar + fill for main content
let cols = split;
// Split the main content vertically
let rows = split;
// Dock a widget to the bottom
let = dock;
Constraint types:
| Constraint | Behavior |
|---|---|
Fixed(n) |
Exactly n cells |
Min(n) |
At least n cells |
Max(n) |
At most n cells |
Percentage(p) |
p% of available space |
Fill |
Distribute remaining space equally among all Fill constraints |
Taffy-Powered Flexbox & Grid
For complex layouts, saorsa-core integrates with Taffy (from the Servo/Dioxus project) for full CSS Flexbox and Grid support:
use ;
let mut engine = new;
// Add nodes with Taffy styles (converted from TCSS ComputedStyle)
let root = engine.add_root;
let child_a = engine.add_child;
let child_b = engine.add_child;
// Compute layout for a given available space
engine.compute;
// Retrieve computed rectangles
let rect_a: LayoutRect = engine.layout;
TCSS properties are automatically converted to Taffy styles via computed_to_taffy().
Scroll Management
use ;
let mut scroll_mgr = new;
let widget_id = 42;
// Register a scroll region
scroll_mgr.register;
// Update when content changes
scroll_mgr.set_content_size; // content 200w x 500h
scroll_mgr.set_viewport_size; // viewport 80w x 24h
// Scroll programmatically
scroll_mgr.scroll_to;
Reactive System
saorsa-core provides a fine-grained reactive system inspired by SolidJS. Changes to signals automatically propagate to computed values, effects, and bound widgets.
Signals
A Signal<T> holds a mutable value. Reading it inside a tracking context records a dependency; setting it notifies all subscribers:
use Signal;
let count = new;
assert_eq!;
count.set;
assert_eq!;
// Update with a closure
count.update;
assert_eq!;
Computed Values
A Computed<T> derives its value from one or more signals. It re-evaluates only when dependencies change:
use ;
let width = new;
let height = new;
let area = new;
assert_eq!;
width.set;
assert_eq!; // Automatically recomputed
Effects
An Effect runs a side-effect function whenever its dependencies change:
use ;
let theme = new;
let _effect = new;
Data Bindings
Bind signals to widget properties:
use ;
let source = new;
let target = new;
// One-way: source → target
let _binding = new;
// Two-way: changes propagate in both directions
let _binding = new;
Batch Updates
Coalesce multiple signal changes into a single notification pass:
use ;
let x = new;
let y = new;
// Subscribers are notified only once, after the batch completes
batch;
Reactive Scopes
A ReactiveScope manages the lifetime of effects and subscriptions. When the scope is dropped, all its effects are cleaned up:
use ReactiveScope;
let scope = new;
// Effects created within this scope are cleaned up when `scope` is dropped
Compositor
The compositor manages overlapping widget layers and produces the final screen buffer.
Layers
Each widget renders into a Layer with a position, z-index, and content:
use ;
let mut compositor = new;
// Add a base layer
compositor.add_layer;
// Add a modal overlay on top
compositor.add_layer;
// Compose all layers into the final buffer
let mut buf = new;
compositor.compose;
Composition Algorithm
- Cut finding - Collects x-offsets at every layer edge to define vertical strips
- Chop extraction - Extracts the segment slice from each layer for each strip
- Z-order selection - For overlapping strips, the highest z-index layer wins
- Concatenation - Merges selected chops into final segment lines
Overlay System
The ScreenStack manages modal overlays, tooltips, and toasts:
use ;
let mut stack = new;
// Push a centered modal overlay
let id = stack.push;
// Pop when dismissed
stack.pop;
Terminal Backends
Backend Trait
All terminal I/O goes through the Terminal trait, making the framework backend-agnostic:
Capability Detection
saorsa-core automatically detects the terminal emulator and its capabilities:
use ;
let info = detect;
println!;
println!;
println!;
println!;
Detected terminals: Alacritty, Kitty, WezTerm, iTerm2, Windows Terminal, GNOME Terminal, Konsole, Xterm, and more.
Detected multiplexers: tmux, screen, Zellij.
Capabilities tracked:
| Capability | Description |
|---|---|
color |
NoColor, Basic16, Extended256, TrueColor |
unicode |
Full Unicode grapheme support |
synchronized_output |
CSI ?2026 synchronized output |
kitty_keyboard |
Kitty keyboard protocol |
mouse |
Mouse event support |
bracketed_paste |
Bracketed paste mode |
focus_events |
Focus in/out notifications |
hyperlinks |
OSC 8 clickable hyperlinks |
sixel |
Sixel graphics protocol |
Test Backend
For testing, use TestBackend which stores output in memory:
use TestBackend;
let backend = new;
// Use for snapshot testing and unit tests without a real terminal
Rendering Pipeline
Double Buffering
ScreenBuffer maintains a grid of Cell values. The renderer diffs the current buffer against the previous frame and only emits escape sequences for changed cells:
use ;
let prev = new;
let curr = new;
// ... render widgets into `curr` ...
// Compute delta
let changes: = curr.diff;
// Batch adjacent changes for fewer cursor movements
let batches = batch_changes;
// Render to escape sequences
let renderer = new;
let output = renderer.render_batched;
SGR Optimization
The renderer minimizes escape sequence output:
- Style diffing - Only emits changed attributes (e.g., if bold is already on, it won't re-emit it)
- SGR coalescing - Combines multiple attributes into a single
\x1b[...msequence - Cursor tracking - Skips cursor movement when the cursor is already at the target position
- Continuation cells - Skips zero-width continuation cells from wide characters
- Synchronized output - Wraps frame updates in CSI ?2026h/l to prevent tearing
Core Types
Segment
The fundamental rendering unit. A Segment is a piece of styled text:
use ;
// Plain text
let seg = new;
// Styled text
let style = new.fg.bold;
let seg = styled;
// Blank padding
let spacer = blank; // 10 spaces
// Control sequence (not rendered as visible text)
let ctrl = control; // hide cursor
Cell
A single terminal cell. Stores one grapheme cluster, its style, and its display width:
use Cell;
let cell = new;
assert_eq!;
let wide = new;
assert_eq!; // CJK character takes 2 columns
Style
Builder-pattern text attributes:
use ;
let style = new
.fg
.bg
.bold
.italic
.underline;
Color
Four color modes with automatic downgrading based on terminal capabilities:
use Color;
use NamedColor;
let rgb = Rgb ; // True color
let indexed = Indexed; // 256-color palette
let named = Named; // 16 ANSI colors
let reset = Reset; // Terminal default
// Parse hex strings
let hex = from_hex.unwrap;
let short_hex = from_hex.unwrap;
Unicode Support
saorsa-core handles the full range of Unicode correctly:
- Grapheme clusters - Characters with combining marks are kept together (via
unicode-segmentation) - Wide characters - CJK ideographs and some emoji occupy 2 terminal columns; continuation cells are tracked automatically
- Emoji sequences - ZWJ (zero-width joiner) families, flag sequences, and skin tone modifiers
- Display width - All width calculations use
unicode-widthfor accurate column counts - Safe truncation - Text truncation respects grapheme boundaries, never splitting a character
- Tab expansion - Configurable tab stops with proper column alignment
- Control character filtering - Non-printable characters are stripped or replaced
The ScreenBuffer::set() method automatically handles wide character edge cases: writing over a continuation cell blanks the preceding wide character, and writing a wide character at the buffer edge replaces it with a space.
Testing
Snapshot Testing
Widget rendering is verified with insta snapshot tests:
use ;
Tests are organized by widget category in tests/snapshot_*.rs.
Property-Based Testing
Layout and CSS parsing are fuzz-tested with proptest:
use *;
proptest!
Tests live in tests/proptest_layout.rs and tests/proptest_css.rs.
Benchmarks
Performance-critical paths are benchmarked with criterion:
Benchmarks cover:
- Rendering - Cell diffing, SGR sequence generation, batch optimization
- Layout - Constraint solving, Taffy layout computation
- CSS parsing - Stylesheet parsing, selector matching, cascade resolution
Error Handling
All fallible operations return Result<T, SaorsaCoreError>:
Dependencies
| Crate | Purpose |
|---|---|
crossterm |
Terminal backend (events, raw mode, cursor) |
taffy |
CSS Flexbox and Grid layout engine (from Servo/Dioxus) |
cssparser |
CSS tokenizer and parser (from Servo) |
ropey |
Rope data structure for TextArea editing |
pulldown-cmark |
Markdown parsing for MarkdownRenderer |
similar |
Diff algorithm for DiffView |
fuzzy-matcher |
Fuzzy string matching for SelectList |
unicode-width |
Display width calculation |
unicode-segmentation |
Grapheme cluster segmentation |
notify |
Filesystem watcher for TCSS hot-reload |
tracing |
Structured logging |
thiserror |
Error type derivation |
Minimum Supported Rust Version
The MSRV is 1.88 (Rust Edition 2024). This is enforced in CI.
License
Licensed under either of:
at your option.
Contributing
Part of the saorsa-tui workspace. See the workspace root for contribution guidelines.