---
title: "tuika 0.9.0"
description: "Release notes for tuika 0.9.0 (2026-08-15). Highlights: Charts, in a companion crate.; Trees and multi-pane interaction.."
image: "https://tuika.dev/social-card.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://tuika.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# tuika 0.9.0

Released 2026-08-15. [crates.io](https://crates.io/crates/tuika/0.9.0) · [docs.rs](https://docs.rs/tuika/0.9.0/tuika/) · [GitHub release](https://github.com/everruns/tuika/releases/tag/v0.9.0)

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](https://raw.githubusercontent.com/everruns/tuika/v0.9.0/docs/charts/line-graphics.png)

**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](https://raw.githubusercontent.com/everruns/tuika/v0.9.0/docs/demos/tree_list.gif)

- **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](https://github.com/kevinswiber/mmdflux/issues/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.

Source: https://tuika.dev/releases/0.9.0/index.mdx
