Skip to content
Updated View as Markdown

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.

chart demo

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.

tree list demo

  • Runner: one FrameSource boundary and one Signal replace ten run* 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_by is the synchronous counterpart to AsyncRunner::run_driven_by: the loop against a caller-owned terminal and event source, with no TerminalSession, no terminal construction, and no stdout-facing graphics or clipboard work. run and run_with_backend are 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.

  • EventSource is where that loop gets its input, and scripted_events builds 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.

  • SelectViewportState couples index selection to a persistent top row for SelectList and Table. Its resolved VirtualWindow is 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 to resolve plus .visible_window.

  • TreeList, TreeRow, and TreeState provide 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 a HitMap focus a registered pane while rejecting unknown ids and requests blocked by overlay input ownership.

  • AsyncRunner::run_with_messages delivers a typed application stream beside terminal events and ticks, with deterministic completion/error/redraw/exit behavior and no shared mutable state or polling.

  • AsyncApplication is the borrowed-view application boundary for the async runner, the counterpart to Application: 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 through Rc<RefCell<_>>.

  • FrameSource / AsyncFrameSource is the single boundary every run method now takes, with exactly two implementors: &mut app for an Application, and from_fn(&mut state, view, update) (async_from_fn for the async runner) for the closure form. no_messages() is the empty application-message stream.

  • AsyncRunner::redraw_handle matches Runner::redraw_handle, so a Live value (or any background producer) can mark the next frame stale on either runner. RedrawHandle now 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.

  • Runner and AsyncRunner restore 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 with UpdateResult::Consumed / Dirty or opt out with with_text_selection(false).

  • Hover styling: mouse::HoverTracker pairs the pointer-motion stream with the existing HitMap hit-testing, reporting when the hovered region changes so a host can restyle it (and knows a redraw is warranted).

  • Timed style transitions: anim::Transition is 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::lerp and style::lerp_color interpolate 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::Gradient is a multi-stop color ramp with even (across) or explicit (with_stops) stops; Gradient::line sweeps a string’s foreground across the ramp per display column (wide glyphs get one coherent color) for use with Text. Transition and Gradient (with lerp_color) are also in the prelude.

Fixed

  • The published crate excludes the generated site/ bundle and the public docs/ 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-mermaid renders 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 of View::render and 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, which tuika-mermaid now 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.

  • Paragraph now treats its input as human-facing prose: bare http(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, and Console remain inert.

  • Breaking: UpdateResult::Clean now means an input was unhandled and may receive runner default behavior; return the new UpdateResult::Consumed for a handled input that does not need a repaint. Exhaustive matches over UpdateResult must add the new variant.

  • tuika-mermaid re-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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close