tuika
A small retained-tree 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 self-contained — it depends only on ratatui, crossterm, textwrap,
and unicode-width, and knows nothing about yolop — and is staged for
extraction into its own crate. Today yolop drives it from
src/app/fullscreen.rs (the --fullscreen renderer) and src/main.rs (the
tuika-gallery demo).
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. - 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 text |
Flex |
Flexbox container (the composition primitive) |
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 |
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 |
(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 Sparkline; crateview! ;
First-class Sparkline { … } syntax for external components (with their own
constructors and named attrs) needs a proc-macro; that lands when tuika
becomes its own crate. Until then, node(...) covers every third-party
component. The tuika-gallery demo is built entirely with view!.
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.
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, and mouse-wheel
scroll:
- 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.
Extending
Add a component by implementing view::View in a new module under
components/. There is no registration step — containers accept any boxed
View.