tuika
A small composable terminal UI toolkit. tuika provides the layout,
overlay, focus, and component primitives that ratatui leaves to you, while
letting ratatui keep ownership of the cell buffer and its diff against the
terminal.
It is a published, self-contained crate that depends only on ratatui,
crossterm, textwrap, and unicode-width. It knows nothing about yolop;
yolop is one production consumer.
Model
- Views (
view::View) are rebuilt from application state every frame. This is cheap becauseratatuidiffs the resulting cell buffer, so there is no reconciler. - State that must survive across frames — scroll offset, selection index,
focus — lives in host-persisted
*Statestructs (theStatefulWidgetidiom), not in the view tree. - Live data (
Live/LiveView) is shared application state read at render time. Updates request a redraw from the runner; Tuika does not spawn data sources or reconcile a retained widget tree. - Layout is a flexbox subset (
layout):Dimension(Auto/Fixed/Percent/Flex),Align,Justify,Direction, over a direction-agnostic axis so rows and columns share one solver. - Overlays (
overlay) anchor a view over the base tree; the host (host) owns the alternate screen, translates crossterm input, and composites the frame. - Motion (
anim,components::{Spinner, ProgressBar, Loader},native::TerminalProgress) animates from a host-supplied frame counter and can drive the terminal's own OSC 9;4 progress indicator.
Components
| Component | Purpose |
|---|---|
Text / Paragraph |
Styled lines / word-wrapped plain text |
Wrap |
Word-wraps pre-styled lines, preserving per-span styles |
Flex |
Flexbox container (the composition primitive) |
Responsive / Constrained |
Breakpoint selection and min/max measurement |
Boxed |
Border + padding + title, focus-aware |
Spacer |
Flexible filler |
Scroll (+ ScrollState) |
Vertical scroll viewport + scrollbar |
SelectList (+ SelectState) |
Selectable list |
StatusBar |
One-row left/right status segments |
Tabs / KeyHints |
Host-state tab navigation and command hints |
Spinner |
Frame-cycled activity glyph |
ProgressBar |
Determinate (sub-cell) / indeterminate bar |
Loader |
Spinner + message + hint row |
Example
use ;
let theme = default;
let root = column
.gap
.fixed
.fixed
.grow;
// In a `terminal.draw(|f| ...)` closure:
paint;
Runnable examples
Each enters the alternate screen; press q (or esc) to quit.
| Example | Command | Shows |
|---|---|---|
gallery |
cargo run -p tuika --example gallery |
motion components + native OSC 9;4 progress |
select |
cargo run -p tuika --example select |
SelectState + SelectList (stateful-widget idiom) |
overlay |
cargo run -p tuika --example overlay |
OverlaySpec centered dialog + input routing |
ratatui_dashboard |
cargo run -p tuika --example ratatui_dashboard |
mixed Ratatui widgets + responsive live data |
(Embedded in yolop, the gallery is also reachable as yolop tuika-gallery.)
Declarative DSL (view!)
view! is optional sugar over the builders — it expands to the exact same
Flex/Boxed/element(...) calls, so there is no runtime cost and nothing new
in the model. It just makes nested layout read top-down:
let root = crateview! ;
Grammar (each keyword consumes exactly one node):
-
col(attrs) { … }/row(attrs) { … }— flex containers. Attrs (all optional):gap,padding,align,justify,background. -
boxed(attrs) { child }— bordered container. Attrs:title,border,padding,background. -
text(expr),spacer()— leaves. -
grow(n) { node }/fixed(n) { node }— set a child's main-axis size (default auto). -
node(expr)— splice anyimpl View. This is the escape hatch, and how a component from another crate participates in the DSL:use CustomView; crateview! ;
node(...) accepts any type that already implements Tuika's View; it does
not make a Ratatui Widget implement View. Use RatatuiView for Ratatui
widgets. The tuika-gallery demo is built entirely with view!.
Ratatui interoperability
Tuika deliberately does not duplicate Ratatui's widget catalog. Wrap existing
widgets in RatatuiView; they render into an isolated buffer and only the
assigned clip is composited into the frame:
use ;
use ;
let values = vec!;
let chart = sized;
The closure form supports widgets that borrow captured data. Stateful widgets
can capture host-owned synchronized state and call StatefulWidget::render
inside the same closure. Surface::render_ratatui is the lower-level escape
hatch for custom views that need several widgets. Neither API exposes the
frame's mutable buffer.
Responsive and live views
Responsive chooses complete compact/wide view trees from the current width;
this supports row-to-column reflow and intentionally omitted secondary
content. Constrained supplies min/max intrinsic measurements to flex layout.
Live<T> is shared application data with a narrow read/update API. LiveView
derives a fresh view from its current value each frame. Connect it to
Runner::redraw_handle() when background producers should invalidate the
screen. Producers retain ownership of their threads, tasks, retries, and
lifecycle.
Terminal lifecycle and runner
TerminalSession is the complete RAII guard: it owns raw mode, alternate
screen, mouse capture, and cursor visibility, including rollback after partial
initialization. It preserves raw mode when the caller had already enabled it.
AltScreen remains available for hosts that intentionally own raw mode and
cursor visibility themselves.
Runner is an optional synchronous event loop for dashboards and small tools.
It owns TerminalSession, frame scheduling, Crossterm event translation, and
data-driven redraw checks. Async applications can keep their existing loop and
call paint directly.
Native terminal progress
native::TerminalProgress emits the OSC 9;4 sequence, which drives the
terminal's own progress indicator — a bar across the top of the window in
Ghostty, the taskbar in Windows Terminal / ConEmu, and similar in
WezTerm / Konsole / mintty. It is out-of-band (no cursor movement, no cells),
so it works in both the inline and full-screen renderers; terminals that don't
understand it ignore the sequence. yolop shows it (indeterminate) while a turn
runs and clears it when idle.
Testing
Layout and rendering are tested hermetically by rendering into an in-memory
ratatui Buffer and reading cells back — no real terminal:
- Unit tests (
src/tuika/tests.rs) — layout math, component rendering, interactive state (scroll/select/focus), compositor, easing, OSC encoder, and palette (every themed cell pinned to itsThemeslot). - Property tests (
src/tuika/proptests.rs,proptest) — solver and overlay invariants for any input (children stay in bounds, flex fills exactly). - Golden snapshots (
src/tuika/snapshots.rs) — whole screens diffed against checked-in glyph grids; refresh withUPDATE_SNAPSHOTS=1. - Resize / degenerate sizes — a size sweep from
0×0up asserts no panic and no out-of-clip writes. - PTY smoke (
tests/tuika_pty.rs) — drives the real binary under a pseudo-terminal and asserts the terminal-facing protocol: alternate-screen enter/leave, OSC 9;4 progress, resize survival, clean exit.
Downstream crates can use tuika::testing::{render, render_sizes, grid} for
buffer assertions, resize sweeps, and stable glyph snapshots without a real
terminal or TestBackend setup.
Compatibility
- Minimum supported Rust version: 1.88, declared as
rust-versionand checked in CI. - Tuika 0.x follows Cargo semver: minor releases may make deliberate breaking API changes; patch releases do not.
- Ratatui and Crossterm are part of Tuika's public interoperability surface. Tuika tracks compatible minor lines deliberately; applications should use matching versions so Cargo can deduplicate them.
Manual terminal matrix
The automated tests cover the protocol a terminal receives; they cannot
verify how a specific emulator actually paints it. This is a checklist to run
before a release, not a record of verified results — tick a box only after
confirming it yourself. Run cargo run -- tuika-gallery in each terminal and
check alt-screen enter/exit, Braille/wide glyphs, truecolor, mouse-wheel
scroll, and — with YOLOP_HYPERLINKS=1 — that the footer URL is a clickable
OSC 8 link:
- Ghostty
- iTerm2
- WezTerm
- Kitty
- Windows Terminal
- Konsole
- tmux (truecolor needs
Tc/RGBinterminal-overrides)
Native OSC 9;4 progress support is a fixed property of each terminal (not something to re-verify per release). Terminals that render it: Ghostty (bar at top of window), Windows Terminal and ConEmu (taskbar), WezTerm, Konsole, mintty. Others (e.g. iTerm2, Kitty) silently ignore the unknown OSC, so emitting it is safe everywhere — the in-terminal UI is unaffected. See the OSC 9;4 references linked from the motion-module PR.
OSC 8 hyperlinks (HyperlinkBackend) wrap http(s)
URL runs so a supporting terminal makes them clickable: Ghostty, iTerm2,
WezTerm, Kitty, Konsole, recent GNOME Terminal / VTE. Others
ignore the escape and render the URL as plain (usually still auto-linkified)
text, so emitting it is safe everywhere. Unlike OSC 9;4, this one is worth
re-checking, because it writes styled spans straight to the terminal: confirm
the link is clickable and that surrounding text, colors, and wrapping are
undamaged. In yolop it is opt-in (YOLOP_HYPERLINKS=1), default-off until this
matrix is walked — that is what the checkbox above verifies.
Extending
Add a component by implementing view::View in a new module under
components/. There is no registration step — containers accept any boxed
View.