# tuika
[](https://crates.io/crates/tuika)
[](https://docs.rs/tuika)
[](https://crates.io/crates/tuika)
[](https://github.com/everruns/tuika/blob/main/LICENSE)

<p align="center">
<img src="docs/hero.gif" width="880" alt="Animated tuika gallery: a terminal window with tabs, an activity panel of spinners, progress bars and a loader, a command palette, a commit-message input, and a status bar — all animating.">
</p>
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-core`
(plus `ratatui-crossterm` for the terminal backend), `crossterm`, `textwrap`,
`unicode-segmentation`, and `unicode-width`, and is host-agnostic — it knows
nothing about the application embedding it. tuika renders none of ratatui's own
widgets, so it builds against `ratatui-core` directly rather than the `ratatui`
umbrella — keeping `ratatui-widgets`, `ratatui-macros`, and their transitive
weight out of its dependency tree. Your application still uses any ratatui
widget it likes (see [Compatibility](#compatibility)). (The optional `async`
feature adds Tokio for [`AsyncRunner`](#terminal-lifecycle-and-runner); it is
off by default.)
## Install
```bash
cargo add tuika
```
`ratatui` and `crossterm` are part of `tuika`'s public interoperability
surface, so add `ratatui` to your own crate and pin a compatible minor version.
tuika depends on `ratatui-core`, which the `ratatui` umbrella re-exports, so
Cargo unifies the shared `Buffer`/`Rect`/`Style` types and your widgets compose
with tuika's surfaces (see [Compatibility](#compatibility)).
## Model
- **Views** (`view::View`) are rebuilt from application state every frame. This
is cheap because `ratatui` diffs the resulting cell buffer, so there is no
reconciler.
- **State** that must survive across frames — scroll offset, selection index,
focus — lives in host-persisted `*State` structs (the `StatefulWidget`
idiom), 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.
- **Keymap** ([`keymap`](docs/keymap.md)) resolves declarative key bindings to
named commands: chords (`ctrl+r`) and multi-stroke sequences (`g g`) grouped
into prioritized, mode-gated `Layer`s, dispatched from a translated `Key` and
queryable for help/`KeyHints` surfaces. Host-agnostic, so it unit-tests
without a terminal. See the [keymap guide](docs/keymap.md).
- **Motion** (`anim`, `components::{Spinner, ProgressBar, Loader}`,
`term::progress::TerminalProgress`) animates from a host-supplied frame counter and
can drive the terminal's own OSC 9;4 progress indicator. `anim::Timeline` adds
a scheduler-free keyframe track (values eased over frame offsets, with
looping/ping-pong) sampled purely from that counter.
- **Pixels** (`framebuffer`) — a mutable RGBA `FrameBuffer` the host draws into
(`set`/`blend`/`fill_rect`/`blit`, a per-pixel `shade` shader post-pass, and
`Sprite` spritesheet frames). `FrameBufferView` paints it into cells with
half-blocks on any terminal, or hand `to_image_data()` to the crisp graphics
protocols.
## Crate layout
Four places, so you can guess where something is:
| `tuika::` | the framework spine — `View`, `Element`, `RenderCtx`, layout, events, `Theme`, `Surface`, the host seam |
| `tuika::components` | every widget: `Flex`, `Boxed`, `Text`, `Scroll`, `Markdown`, `Table`, … |
| `tuika::term` | everything out-of-band: `clipboard` (OSC 52), `hyperlink` (OSC 8), `progress` (OSC 9;4), `pointer` (OSC 22), `image`, `capabilities`, `palette` (the terminal's own colors) |
| `tuika::prelude` | the spine and the components in one glob import |
Application code usually wants the prelude:
```rust
use tuika::prelude::*;
```
Everything else stays behind its module path on purpose — `themes::by_name`,
`probe::RectProbe`, `width::str_cols`, `term::clipboard::write` — so a short
path always means "you will use this constantly".
## Components
See the [component gallery](docs/components.md) for an animated demo of each
component. Linked names below jump straight to their demo.
| [`Text`](docs/components.md#text) / `Paragraph` | Styled lines / word-wrapped plain text |
| `Wrap` | Word-wraps pre-styled lines, preserving per-span styles |
| [`Markdown`](docs/markdown.md) (+ `MarkdownState`) | CommonMark → styled lines; `MarkdownState` streams incrementally — see the [markdown guide](docs/markdown.md) |
| [`CodeBlock`](docs/components.md#codeblock) | Themed, framed code block with a pluggable `Highlighter` and optional line-number gutter |
| `Diff` | Line diff (LCS), unified or side-by-side, with `+`/`-` gutters and line numbers |
| `AsciiFont` | Large "figlet-style" block-letter banner text |
| `QrCode` (+ `QrEcc`) | QR code (byte-mode v1–4 encoder) rendered with half-blocks |
| [`Rule`](docs/components.md#rule) | Horizontal separator: optional title + fill glyph to width |
| [`Flex`](docs/components.md#flex) | Flexbox container (the composition primitive) |
| `Responsive` / `Constrained` | Breakpoint selection and min/max measurement |
| [`Boxed`](docs/components.md#boxed) | Border + padding + title, focus-aware |
| `Scene` / `Dialog` | Owned root + anchored overlays; modal composition |
| `Spacer` | Flexible filler |
| [`Scroll`](docs/components.md#scroll--scrollstate) (+ `ScrollState`) | Vertical scroll viewport + scrollbar over lines |
| [`ItemScroll`](docs/components.md#itemscroll) | The same viewport over laid-out items (panels, tables, nested layouts) |
| `Viewport` | Two-dimensional clipping/panning over any child view |
| `Form` / `FormField` (+ `FormState`) | Responsive labeled controls and validation |
| `DrawView` / `CanvasView` | Closure-based custom cell drawing |
| [`SelectList`](docs/components.md#selectlist--selectstate) (+ `SelectState`) | Selectable list |
| `Slider` (+ `SliderState`) | One-row value picker over a numeric range |
| [`TextInput`](docs/components.md#textinput--textinputstate) (+ `TextInputState`) | Multi-line composer: soft-wrap, placeholder, highlighted ranges, `@`/`/` tokens |
| [`StatusBar`](docs/components.md#statusbar) | One-row left/right status segments |
| [`Tabs`](docs/components.md#tabs--tabsstate) / `KeyHints` | Host-state tab navigation and command hints |
| `TabSelect` (+ `TabSelectState`) | Value-selecting segmented control |
| `Toasts` / `ToastList` | Transient notification stack with frame-driven expiry |
| `Console` (+ `ConsoleLog`) | Captured stdout/log ring buffer + tailing overlay view |
| [`Spinner`](docs/components.md#spinner) | Frame-cycled activity glyph |
| [`ProgressBar`](docs/components.md#progressbar) | Determinate (sub-cell) / indeterminate bar |
| [`Loader`](docs/components.md#loader) | Spinner + message + hint row |
## Example
Layout reads top-down with the [`view!`](#declarative-dsl-view) DSL:
```rust
use tuika::prelude::*;
let theme = Theme::default();
let root = view! {
col(gap = 1) {
fixed(1) { node(Spinner::new(frame)) }
fixed(1) { node(ProgressBar::determinate(0.6).percent(true)) }
grow(1) { text("body") }
}
};
// In a `terminal.draw(|f| ...)` closure:
paint(f.buffer_mut(), f.area(), &theme, root.as_ref(), &[]);
```
## Owned scenes, dialogs, and forms
`Scene` owns a root `Element` and ordered `SceneOverlay`s. Each layer retains
its `OverlaySpec`, so it resolves against the current terminal size inside
rendering; callers do not retain borrowed views or pre-resolved `Rect`s.
`Dialog` composes `Boxed`, `Flex`, and optional `KeyHints` into a centered modal
with size clamps, clear/dim behavior, and an optional focus-owner id:
```rust
use tuika::prelude::*;
let scene = Scene::new(element(base)).dialog(
Dialog::new("Confirm", element(Text::raw("Delete this item?")))
.min_size(30, 7)
.max_size(70, 20)
.key_hints([("enter", "delete"), ("esc", "cancel")])
.dim_backdrop(true)
.focus_owner("confirm"),
);
scene.sync_focus(&mut focus);
paint_scene(buffer, area, &theme, &scene);
```
`Form` lays out arbitrary control `Element`s beside responsive labels, stacking
on narrow terminals. Help and validation rows are built in; `FormState` owns
only focus traversal and submit/cancel outcomes, while values and cursor state
stay in existing host-owned `TextInputState`, `SelectState`, or application
models.
## Arbitrary-child viewports and drawing
`Viewport` clips and pans any child view in both axes. The host supplies the
full content `Size` and mirrors offsets through the same `ScrollState` used by
line-oriented `Scroll`. It renders only the visible source window, so a large
logical canvas does not allocate a full off-screen buffer.
`DrawView` (also named `CanvasView`) turns a closure receiving `(Rect,
&mut Surface, &RenderCtx)` into a normal view. The surface is already clipped,
making it suitable for terminal grids, charts, emulators, and incremental
migrations. Import it explicitly from `tuika::view`; custom canvases stay
outside the application prelude.
Run `cargo run --example primitives` for one composition using `Scene`,
`Dialog`, `Form`, `Viewport`, and `DrawView`.
### Builder syntax (alternative)
`view!` expands to plain builder calls, so the same tree can be written without
the macro:
```rust
use tuika::prelude::*;
let root = Flex::column()
.gap(1)
.fixed(1, element(Spinner::new(frame)))
.fixed(1, element(ProgressBar::determinate(0.6).percent(true)))
.grow(1, element(Text::raw("body")));
```
## Markdown and syntax highlighting
`Markdown` renders CommonMark to styled lines, word-wrapping prose while drawing
code and tables verbatim. `MarkdownState` is its streaming form: fed deltas as a
message arrives, it re-parses only the in-flight tail and caches everything
before the last stable block boundary, so long transcripts don't re-tokenize and
settled code blocks aren't re-highlighted every frame.
Highlighting is a seam, not a dependency: `tuika` owns the *presentation* of code
(framing, background, language label, wrapping) via `CodeBlock`, and takes token
colors from any `Highlighter` you supply — keeping the toolkit free of grammar
crates. The companion crate
[`tuika-codeformatters`](https://crates.io/crates/tuika-codeformatters) ships a
ready-made tree-sitter `Highlighter`.
Fenced blocks can replace their source with a different, width-aware
presentation through `FencedBlockRenderer`. The
[`tuika-mermaid`](crates/tuika-mermaid/) companion uses that seam with mmdflux:
a `mermaid` fence becomes a Unicode cell diagram inside the surrounding
Markdown, with no browser, SVG, or image protocol. Unsupported or invalid input
falls back to the ordinary code block.
```rust
use tuika::prelude::*;
use tuika_mermaid::MermaidRenderer;
let mermaid = MermaidRenderer::new();
let document = Markdown::new(
"```mermaid\nflowchart LR\n Parse --> Layout --> Paint\n```",
)
.block_renderer(&mermaid);
# let _ = document;
```
Run the complete integration demo with
`cargo run -p tuika-mermaid --example mermaid_markdown`.
<img src="https://raw.githubusercontent.com/everruns/tuika/main/crates/tuika-mermaid/examples/mermaid_markdown/mermaid.gif" width="880" alt="Mermaid diagram rendered as Unicode cells inside tuika Markdown">
Images use the same host-extension pattern: supply an `ImageResolver` and
`` renders as real pixels (see [Images](#images)).
## Theming
Every component styles itself from a `Theme` passed through the render context —
no color is hard-coded, so swapping the theme handed to `paint` restyles the
whole tree at once. A `Theme` is a plain `Copy` struct of colors, and tuika
bundles a few standard palettes as full `const Theme` structures in the
[`themes`](https://docs.rs/tuika/latest/tuika/themes/index.html) module —
reachable directly, by constructor, or by name:
```rust
use tuika::themes;
let a = themes::GRUVBOX_DARK; // the struct
let b = tuika::Theme::gruvbox_dark(); // named constructor
let c = tuika::themes::by_name("gruvbox-dark").unwrap(); // config / --theme
```
See the [theme gallery](docs/themes.md) for a screenshot of each bundled
palette, or `themes::PRESETS` to enumerate them for a picker.
An app can also inherit the palette the user already configured in their
terminal, rather than bringing its own — either implicitly with `themes::TERMINAL`
(ANSI slots, no I/O) or by asking the terminal for its actual colors and deriving
a full theme from the reply. It is opt-in: tuika never probes unless a host asks
it to. The query lives with the other out-of-band escapes, in `term::palette`. See
[inheriting the terminal's colors](docs/features.md#inheriting-the-terminals-colors).
Where a `Theme` is the color *tokens*, a `StyleSheet` is the *rules* — a mapping
from a semantic role (heading, link, inline code, a panel's border and fill, …)
onto the style it draws with. Override a role in one place and every element with
that role restyles at once; markdown parts and bare URLs are role-driven too. See
the [styling guide](docs/styling.md).
### Runnable examples
Each enters the alternate screen; press `q` (or `esc`) to quit.
| [`gallery`](examples/gallery.rs) | `cargo run --example gallery` | motion components + native OSC 9;4 progress |
| [`markdown`](examples/markdown.rs) | `cargo run --example markdown` | streaming `MarkdownState` + highlighted `CodeBlock`, following the stream until you scroll back |
| [`select`](examples/select.rs) | `cargo run --example select` | `SelectState` + `SelectList` (stateful-widget idiom) |
| [`overlay`](examples/overlay.rs) | `cargo run --example overlay` | `OverlaySpec` centered dialog + input routing |
| [`primitives`](examples/primitives.rs) | `cargo run --example primitives` | owned dialog scene + form + arbitrary-child viewport |
| [`ratatui_dashboard`](examples/ratatui_dashboard.rs) | `cargo run --example ratatui_dashboard` | mixed Ratatui widgets + responsive live data |
| [`async_dashboard`](examples/async_dashboard.rs) | `cargo run --example async_dashboard --features async` | `AsyncRunner` polling on a Tokio runtime, no shared state |
| [`mouse`](examples/mouse.rs) | `cargo run --example mouse` | drag-to-select + highlight + OSC 52 copy, clickable buttons |
| [`image`](examples/image.rs) | `cargo run --example image` | `Image` over reserved cells (Kitty/iTerm2/Sixel), alt-text fallback |
| [`inherit`](examples/inherit.rs) | `cargo run --example inherit` | adopting the terminal's own palette — probe, derive, and the no-I/O fallback |
| [`codex`](examples/codex) | `cargo run --example codex` | a whole coding-agent TUI — [demo](docs/showcases.md#codex-cli-replica-in-repo-example) (a UI replica, see below): streaming transcript, composer, `@`/`/` pickers, approval prompt |
Each of the single-topic examples above quits on `q`/`esc`. [`codex`](examples/codex)
is the composite one — those keys are text there, so it quits with `⌃C` — and it
prints canned frames without a terminal at all with
`cargo run --example codex -- --dump`.
It is a **replica of the OpenAI Codex CLI's interface, not the Codex CLI**: an
example built on tuika, unaffiliated with and unendorsed by OpenAI, with no model,
network, or shell behind it — a turn is a scripted sequence revealed frame by
frame. See the [showcase entry](docs/showcases.md#codex-cli-replica-in-repo-example).
<p align="center">
<img src="https://raw.githubusercontent.com/everruns/tuika/main/examples/codex/codex.gif" width="880" alt="The codex example running: a slash-command popup filtering as it is typed, an @-file picker completing a path in the composer, then a scripted turn — streamed reasoning, an approval prompt for a destructive command answered from the keyboard, command output, and a streamed markdown answer.">
</p>
## 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:
```rust
let root = crate::view! {
col(gap = 1, padding = tuika::Padding::all(1)) {
boxed(title = " body ") { text("hello") }
grow(1) { spacer() }
node(status_bar) // any expression that is `impl View`
}
};
```
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 any `impl View`. This is the escape hatch, and how
a component **from another crate** participates in the DSL:
```rust
use other_crate::CustomView;
crate::view! { col { node(CustomView::new(&data)) } };
```
`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:
```rust
use ratatui::widgets::{Sparkline, Widget};
use tuika::prelude::*;
let values = vec![1, 4, 2, 8];
});
```
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, enhanced
keyboard reporting, alternate screen, mouse capture, and cursor visibility,
including rollback after partial initialization. Enhanced reporting preserves
non-character modifiers, so `Shift+Enter` reaches `TextInputState` as a
different chord from `Enter`; iTerm2 and tmux get their required protocol
variants, while Windows uses the modifier state already carried by its native
console events. It preserves raw mode and any keyboard-reporting stack entries
the caller had already enabled. `AltScreen` remains available for hosts that
intentionally own raw mode, keyboard modes, 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.
`AsyncRunner` (behind `features = ["async"]`) is the same loop for applications
that already have a Tokio runtime — anything doing network or disk I/O. It ties
`TerminalSession`, `paint`, and `translate_event` to crossterm's async
`EventStream` and a tick timer in one `tokio::select!`, so the host keeps a
single event loop instead of bolting `spawn_blocking` + a shared `Live` +
`Notify` + a stop flag onto the synchronous `Runner` to feed it. The loop threads
one owned `state` value through a `view` closure (`&state` → the frame) and an
`async` `update` closure (`&mut state` on each `Signal` — a tick or an event —
and it may `.await`):
```rust,ignore
use std::ops::ControlFlow;
use std::time::Duration;
use tuika::prelude::*;
let runner = AsyncRunner::new(RunnerConfig { tick_rate: Duration::from_secs(2) });
let mut stats = Stats::default();
runner.run(
&Theme::default(),
&mut stats,
|stats, _frame| element(Text::raw(stats.summary())),
async |stats, signal| match signal {
Signal::Tick => { stats.ingest(fetch(&url).await?); ControlFlow::Continue(()) }
Signal::Event(Event::Key(k)) if k.plain() && k.code == KeyCode::Char('q') =>
ControlFlow::Break(()),
_ => ControlFlow::Continue(()),
},
).await?;
```
Enabling `async` adds Tokio (timer + `select!`) and crossterm's `event-stream`
feature; it stays off by default so sync-only hosts pull in no runtime. The
[`async_dashboard`](examples/async_dashboard.rs) example is the runnable
counterpart to `ratatui_dashboard` with no shared state at all.
## Native terminal progress
`term::progress::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. A host typically shows it (indeterminate)
while long work runs and clears it when idle.
## Images
`Image` paints real pixels — an avatar, a chart, a rendered diagram — over the
cells it reserves, using whichever terminal graphics protocol
`ImageSupport::detect()` finds: **Kitty** (Kitty, Ghostty, WezTerm, Konsole),
**iTerm2**, or **Sixel** (foot, xterm +sixel, mlterm, contour). Terminals with
none show the alt text, so the same view tree renders everywhere.
<img src="docs/demos/image.svg" width="880" alt="Two terminal windows side by side: on a Kitty/Ghostty/WezTerm/Konsole terminal an Image view renders a red/green gradient in place; on every other terminal the same view shows a dimmed italic '[image: a red/green gradient]' placeholder.">
Decoding stays in the host — a heavy dependency, kept out like the highlighter
seam — so you hand in raw RGBA via `ImageData::from_rgba` and `tuika` owns the
protocol encoding (base64, PNG, and Sixel encoders are inline, so no image-codec
dependency). A graphics escape paints at the cursor, unlike the cursor-neutral
OSC sequences, so emission is split from layout: `Image` reserves cells and
records its placement into an `ImageLayer`, then the host calls
`ImageLayer::emit` after `terminal.draw()` to paint the pixels over them.
```rust
use tuika::prelude::*;
use tuika::term::image::{ImageData, ImageLayer, ImageSupport};
let data = ImageData::from_rgba(2, 2, vec![0u8; 2 * 2 * 4]).unwrap();
let layer = ImageLayer::new();
let _image = Image::new(data, 20, 10) // 20×10 cells on screen
.support(ImageSupport::detect())
.in_layer(&layer)
.alt("a 2×2 swatch"); // shown where graphics aren't supported
```
Markdown `` renders too, in both the one-shot `Markdown` view and the
streaming `MarkdownState`: attach a host `ImageResolver` (URL → `ImageData`, the
same seam as the highlighter) and resolved images become real pixels — a
link-styled placeholder for the rest, never a dropped URL.
To check support across every terminal feature in one place — `graphics`,
`hyperlinks`, `clipboard`, `progress`, `truecolor` — use `Capabilities`:
`Capabilities::from_env()` is an instant advisory guess, and
`Capabilities::query(timeout)` adds a Device Attributes probe that confirms Sixel
(the one protocol the environment can't reliably reveal).
## Mouse, selection, and clipboard
Enabling mouse capture (which `AltScreen` / `TerminalSession` do) means the
terminal stops doing its own click-drag text selection and hands every drag to
the app instead. The `mouse` module rebuilds those affordances over the grid
you already rendered:
- **Text selection.** `SelectionState` turns a left-button `Down → Drag → Up`
gesture into a `SelectionRange` (a plain click selects nothing; a new press
clears the old selection). `selected_text(buffer, area, range)` reads the text
back out of the rendered `ratatui::Buffer` — linear/stream selection like a
terminal's own, wide glyphs intact — and `mouse::paint_selection(buffer, area, range,
style)` paints it in.
- **Clicks and regions.** `HitMap<T>` maps screen rects to values (a button, a
link, a row); the last-pushed match wins, so children/overlays registered
after their parents take precedence. `ClickTracker` turns a same-cell
`Down`/`Up` into a `Click` and lets an intervening drag cancel it.
- **Clipboard.** `clipboard::write(out, text)` copies via **OSC 52**
(`clipboard::osc52` is the pure encoder) — no platform clipboard library,
works over SSH. Same tmux caveat as OSC 8: needs `allow-passthrough on`.
The enriched event model carries what selection and clicks need: `MouseKind` is
`Down/Up/Drag(MouseButton)`, `Moved`, and `ScrollUp/Down/Left/Right`, and every
`Mouse` reports `shift/ctrl/alt`. **Shift-drag** is deliberately left to the
terminal — most emulators use it to bypass app mouse capture for a native
selection — so a host should act on `plain()` left-drags.
**Touch** arrives as mouse events: terminal emulators translate a tap to a
`Down`+`Up` and a swipe to scroll or a drag, so touch flows through this same
path — there is no separate touch event to handle.
> See the [terminal features guide](docs/features.md) for these
> terminal-integration capabilities — OSC 8 hyperlinks, mouse selection and
> clicks, OSC 52 clipboard, OSC 9;4 progress, and Kitty/iTerm2/Sixel images —
> plus `Capabilities` detection, with demos and runnable examples.
## Testing your UI
Rendering is deterministic, so UI built on tuika can be tested without a real
terminal or `TestBackend` setup. The [`testing`](https://docs.rs/tuika/latest/tuika/testing/index.html)
module draws a `View` into an in-memory ratatui `Buffer` and reads it back:
- `render(view, width, height, &theme) -> Buffer` — draw once at a fixed size.
- `grid(&buffer) -> String` — the buffer as a plain glyph grid, ready for a
snapshot assertion.
- `render_sizes(view, sizes, &theme) -> Vec<Buffer>` — the same view across a set
of sizes, for resize and degenerate-size sweeps.
```rust
use tuika::testing::{grid, render};
use tuika::Theme;
let buffer = render(my_view.as_ref(), 20, 3, &Theme::default());
assert!(grid(&buffer).contains("expected text"));
```
## Used in
- [**yolop**](https://github.com/everruns/yolop) — a terminal coding agent whose
experimental full-screen renderer is built on tuika.
- [**LLMSim**](https://github.com/chaliy/llmsim) — an LLM traffic simulator whose
live stats dashboard is a tuika screen.
See the [showcases](docs/showcases.md) for a recording of each. Building
something on tuika? Open a PR adding it here.
## Compatibility
- Minimum supported Rust version: **1.88**, declared as `rust-version` and
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 builds against `ratatui-core` (and `ratatui-crossterm`) directly, not
the `ratatui` umbrella; the umbrella re-exports that same `ratatui-core`, so a
matching `ratatui` minor line in your application resolves to one shared
`ratatui-core` and Cargo deduplicates the core types. Widgets such as
`Block`/`Paragraph`/`Table` live in `ratatui-widgets` (pulled in by your
`ratatui` dependency, not by tuika) and compose through
[`Surface::render_ratatui`](https://docs.rs/tuika/latest/tuika/surface/struct.Surface.html#method.render_ratatui)
and [`RatatuiView`](https://docs.rs/tuika/latest/tuika/interop/struct.RatatuiView.html),
whose seam is a raw `ratatui-core` `Buffer`.
## Extending
tuika is extended from your own crate — no fork, no registration step, no trait
the built-ins get that yours don't:
- **Custom components.** Implement [`View`](https://docs.rs/tuika/latest/tuika/view/trait.View.html)
on your own type and splice it anywhere with `node(your_view)`, or hand it to
any container — they accept any `impl View`. The built-in components are on
equal footing with yours; nothing special-cases them.
- **Existing Ratatui widgets.** Wrap one in `RatatuiView` rather than
reimplementing it — see [Ratatui interoperability](#ratatui-interoperability).
The [`view!`](#declarative-dsl-view) DSL reaches your components through the same
`node(...)` escape hatch, so they compose exactly like the built-ins.
## Contributing
Issues and pull requests are welcome at
[everruns/tuika](https://github.com/everruns/tuika). See
[CONTRIBUTING.md](CONTRIBUTING.md) for the local checks (`cargo fmt --check`,
`cargo clippy --all-targets --all-features -- -D warnings`,
`cargo test --all-features`) and the commit and review conventions.
The separately published companion crates live in this repository:
- [`tuika-codeformatters`](crates/tuika-codeformatters/) supplies the
tree-sitter `Highlighter`.
- [`tuika-mermaid`](crates/tuika-mermaid/) renders Mermaid fences as Unicode
terminal diagrams through mmdflux.
Both keep their heavier parsers and grammars out of tuika core.
## License
MIT — see [LICENSE](LICENSE).