tuika 0.9.0
Released 2026-08-15. crates.io · docs.rs · GitHub release
Released alongside tuika-charts 0.1.0 — its first release — plus
tuika-mermaid 0.3.0, tuika-codeformatters 0.4.2, and tuika-html 0.1.2.
Their tuika dependency requirements now track 0.9.
Highlights
Charts, in a companion crate. tuika-charts renders line, area, bar, step,
and scatter series and picks its own fidelity: terminal graphics where the
emulator supports them, portable half-block and Braille cells everywhere else,
from the same Chart description.

Trees and multi-pane interaction. TreeList brings stable-id expansion,
selection, and persistent scrolling over host-owned rows, on the same reusable
primitives — SelectViewportState, FocusRegistry::focus, hit testing — that
let several panes in one screen share focus and the mouse.

- Runner: one
FrameSourceboundary and oneSignalreplace tenrun*methods across the two runtimes; the synchronous loop is now drivable from a caller-owned terminal, so a whole application can be tested with no tty.
Added
-
Runner::run_driven_byis the synchronous counterpart toAsyncRunner::run_driven_by: the loop against a caller-owned terminal and event source, with noTerminalSession, no terminal construction, and no stdout-facing graphics or clipboard work.runandrun_with_backendare now built on it, so every synchronous entry point shares one loop. It is generic over the run’s error type, so an infallible backend (TestBackend) pairs with an infallible source. -
EventSourceis where that loop gets its input, andscripted_eventsbuilds one from a known sequence of events — replaying them in order, then going idle like a quiet terminal so tick pacing is unchanged. Together they let a host (or a test) drive a whole application with no tty: previously the synchronous loop could not be exercised at all, and only its pieces (RunnerCore, the clock, selection) had coverage. Seven end-to-end tests now cover the initial frame, event-driven state, clean updates neither rebuilding nor repainting, the borrowed application boundary, redraw requests, deferred resize repaints, and split-footer publishing. -
SelectViewportStatecouples index selection to a persistent top row forSelectListandTable. Its resolvedVirtualWindowis shared with mouse hit testing, so selection scrolls only across viewport edges and clicks do not recenter the list. The existing selection-centered.viewport(rows)remains available; persistent hosts migrate toresolveplus.visible_window. -
TreeList,TreeRow, andTreeStateprovide stable-id expansion, selection, keyboard/mouse navigation, ancestor fallback across refreshes, persistent scrolling, branch rendering, and a scrollbar over host-provided tree rows. -
FocusRegistry::focus(id)lets aHitMapfocus a registered pane while rejecting unknown ids and requests blocked by overlay input ownership. -
AsyncRunner::run_with_messagesdelivers a typed application stream beside terminal events and ticks, with deterministic completion/error/redraw/exit behavior and no shared mutable state or polling. -
AsyncApplicationis the borrowed-view application boundary for the async runner, the counterpart toApplication: previously a frame could borrow application state only on the synchronous runner, so a Tokio host had to clone into an owned tree or share throughRc<RefCell<_>>. -
FrameSource/AsyncFrameSourceis the single boundary every run method now takes, with exactly two implementors:&mut appfor anApplication, andfrom_fn(&mut state, view, update)(async_from_fnfor the async runner) for the closure form.no_messages()is the empty application-message stream. -
AsyncRunner::redraw_handlematchesRunner::redraw_handle, so aLivevalue (or any background producer) can mark the next frame stale on either runner.RedrawHandlenow registers the waiting task’s waker, so the async loop is woken by a request instead of only noticing one on its next tick; bursts still coalesce into a single repaint. -
RunnerandAsyncRunnerrestore drag-to-select behavior by default when their terminal session captures the mouse. Selection is painted over the final cell frame, releasing copies through OSC 52 on real-terminal runs, and wheel events continue to reach application scrolling. Applications can claim gestures withUpdateResult::Consumed/Dirtyor opt out withwith_text_selection(false). -
Hover styling:
mouse::HoverTrackerpairs the pointer-motion stream with the existingHitMaphit-testing, reporting when the hovered region changes so a host can restyle it (and knows a redraw is warranted). -
Timed style transitions:
anim::Transitionis a retargetable eased ramp for state-driven motion (hover on/off, focus, expansion) — retargeting mid-flight continues from the current value instead of jumping.anim::lerpandstyle::lerp_colorinterpolate scalars and 24-bit colors; non-RGB colors snap to the nearer endpoint, since an indexed color has no blendable value. -
Gradient color spans:
style::Gradientis a multi-stop color ramp with even (across) or explicit (with_stops) stops;Gradient::linesweeps a string’s foreground across the ramp per display column (wide glyphs get one coherent color) for use withText.TransitionandGradient(withlerp_color) are also in the prelude.
Fixed
- The published crate excludes the generated
site/bundle and the publicdocs/tree, keeping the 0.9.0 archive below crates.io’s 10 MiB upload limit. README guide and image links now use release-tag-pinned absolute URLs, and the split-footer recording lives in its focused guide rather than being duplicated in the README. Committed IAI benchmark result snapshots are excluded from both crates that own them; benchmark source remains available. tuika-mermaidrenders a Mermaid decision node with a<br/>label as a diagram, with every label line inside the shape. The upstream defect (mmdflux#387) — which unwound out ofView::renderand took the host application down, and failed silently in release builds by dropping the label or painting a short one over the shape’s own borders — is fixed in mmdflux 2.6.1, whichtuika-mermaidnow requires. The adapter-side guard that degraded these fences to the code-block fallback is gone with it; the unconditional panic containment around the layout engine stays.
Changed
Breaking: the runner’s run* surface is one boundary plus two axes. The methods
were a cross product of frame source, runtime, terminal ownership, and extra
input, encoded as names — ten of them on AsyncRunner — with arbitrary holes in
the product. The frame source is now a FrameSource argument rather than a
name, and messages ride the existing Signal, which leaves only where the
terminal and the input come from in the names.
Signal gained a message type parameter, Signal<M = Infallible>. Because the
default is uninhabited, an existing match signal { Signal::Tick => …, Signal::Event(e) => … } still compiles and stays exhaustive. A match on a
reference (match &signal) does need a _ arm added. AsyncSignal<M> is now
a deprecated alias for Signal<M>.
| Before | After |
|---|---|
Runner::run(theme, state, view, update) |
Runner::run(theme, from_fn(state, view, update)) |
Runner::run_app(theme, app) |
Runner::run(theme, app) (shim, deprecated) |
Runner::run_with_backend(theme, backend, state, view, update) |
Runner::run_with_backend(theme, backend, from_fn(state, view, update)) |
Runner::run_app_with_backend(theme, backend, app) |
Runner::run_with_backend(theme, backend, app) (shim, deprecated) |
AsyncRunner::run(theme, state, view, update) |
AsyncRunner::run(theme, async_from_fn(state, view, update)) |
AsyncRunner::run_with_messages(theme, state, messages, view, update) |
AsyncRunner::run_with_messages(theme, async_from_fn(state, view, update), messages) |
AsyncRunner::run_with_backend(theme, backend, state, view, update) |
AsyncRunner::run_with_backend(theme, backend, async_from_fn(state, view, update)) |
AsyncRunner::run_with_events(terminal, theme, state, events, view, update) |
AsyncRunner::run_driven_by(terminal, theme, async_from_fn(state, view, update), events, no_messages()) (shim, deprecated) |
AsyncRunner::run_with_events_and_messages(terminal, theme, state, events, messages, view, update) |
AsyncRunner::run_driven_by(terminal, theme, async_from_fn(state, view, update), events, messages) (shim, deprecated) |
Methods marked shim keep working with a deprecation warning. The rest changed shape under the same name, so they fail to compile rather than warn — the migration is mechanical and the table above is exhaustive.
-
Paragraphnow treats its input as human-facing prose: barehttp(s)URLs use the stylesheet’s link role and carry OSC 8 targets through wrapping by default.Paragraph::link_policy(LinkPolicy::NONE)restores literal, single-style rendering;Text,Wrap,CodeBlock, andConsoleremain inert. -
Breaking:
UpdateResult::Cleannow means an input was unhandled and may receive runner default behavior; return the newUpdateResult::Consumedfor a handled input that does not need a repaint. Exhaustive matches overUpdateResultmust add the new variant. -
tuika-mermaidre-lays out a diagram wider than the available columns at progressively tighter node separation until it fits, instead of letting it be clipped at the pane’s right edge. Fitting is best-effort: a graph with many parallel branches can be irreducibly wider than the terminal, and the narrowest layout is used there. Diagrams that already fit keep mmdflux’s own spacing, so existing renders are unchanged.