---
title: "tuika 0.11.0"
description: "Release notes for tuika 0.11.0 (2026-08-22). Highlights: One route for every event; Streaming markdown renders identically to a one-shot render."
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.11.0

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

Released alongside `tuika-charts` 0.1.2, `tuika-codeformatters` 0.5.0,
`tuika-html` 0.1.4, and `tuika-mermaid` 0.3.2. Their tuika dependency
requirements now track 0.11.

## Highlights

**One route for every event** — a host registers each surface once and every
`Event` kind (key, paste, mouse, focus) follows the same path to whichever
surface owns input this frame, so a paste can no longer take a different route
from a key and land behind an open overlay. `Router` resolves stages in a fixed
order and `Delivery` reports which surface received what, including the case
where nothing did.

**Streaming markdown renders identically to a one-shot render** — two cache
boundaries that only misfired at particular chunk splits are fixed, so a
document fed in as it arrives renders exactly like the same document rendered
whole.

![markdown demo](https://raw.githubusercontent.com/everruns/tuika/v0.11.0/docs/demos/markdown.gif)

- **Binary size**: a default `tuika-codeformatters` build measured ~24.0 MiB on
  a highlighting probe binary; a `rust` + `python` build is ~4.8 MiB. A tuika
  host that places no images drops ~8.4 KiB of graphics encoders.
- **Dependencies**: 66 crates to 55 (the `async` graph, 71 to 59).

## Breaking Changes

- **`tuika-codeformatters`**: every tree-sitter grammar now sits behind its own
  cargo feature. All fourteen are **on by default**, so a dependant taking the
  crate normally is unaffected. A dependant that already passes
  `default-features = false` now gets no grammars until it names them:

```toml
  tuika-codeformatters = { version = "0.4", default-features = false, features = ["rust", "python"] }
```

  Feature names are the language keys lowercased without punctuation — `rust`,
  `python`, `typescript` (covers TSX/JSX), `go`, `java`, `ruby`, `css`, `html`,
  `csharp`, `php`, `zig`, `scala`, `sql`. A language whose feature is off
  behaves exactly like one the crate never supported: `highlight` returns
  `None` and the caller renders plain code.

- **`Paragraph` and dialog copy wrap differently.** Both now use tuika's own
  wrap solver — the one `Wrap`/`wrap_lines` has always used — instead of
  `textwrap`. Three visible differences, all of them making `Paragraph` agree
  with the rest of the toolkit:
  - Line breaks are chosen **greedily** (first-fit) rather than by textwrap's
    optimal-fit minimum-raggedness pass, so a paragraph's rows may break at
    different words than in 0.10.
  - A run of whitespace between two words is one break opportunity and renders
    as a single space, instead of being copied through verbatim.
  - Breaks happen only at whitespace. textwrap also broke at Unicode line-break
    opportunities such as `/`, which could split a linkified URL after its
    scheme; a URL now stays on one row unless it is wider than the row.

  Widths are counted in tuika's grapheme-aware display columns, which fixes a
  latent bug: `Paragraph` wrapped with textwrap's per-`char` width model and
  then measured the result with `str_cols`, so text containing a ZWJ emoji
  (a family, a flag, a skin-tone sequence) wrapped earlier than its own
  measurement said it needed to.

## Changed

- **`tuika-codeformatters` binary size**: grammars are selected by a runtime
  language string, so the config builder named all fourteen and anchored every
  parse table into any binary that highlighted anything — roughly 21 MiB of
  read-only data, C# alone about 5 MiB. Selecting grammars by feature lets a
  host drop the ones it does not use: measured on a probe binary highlighting a
  single snippet, the default build is ~24.0 MiB and a `rust` + `python` build
  ~4.8 MiB. Highlighting output under default features is unchanged.
- **Graphics protocol encoders are now pay-for-use.** `ImageLayer::emit` runs
  after every frame in every `Runner` host and chose its encoder by matching on
  `ImageSupport`, which linked the Kitty, iTerm2, and Sixel encoders plus the
  PNG writer into every binary that used tuika at all. The encoder is now
  resolved where an image is recorded — reachable only from the `Image` view —
  so a host that places no images no longer carries any of them: about 8.4 KiB
  less tuika code in a minimal host. The emitted bytes are unchanged for all
  three protocols, and no public API changed.

- **Dependencies: 66 crates to 55.** Three runtime dependencies were removed by
  taking work tuika was already half-doing:
  - `textwrap` (with `smawk` and `unicode-linebreak`) — replaced by the wrap
    solver `Wrap` already used. See **Breaking Changes** for the visible
    difference.
  - `ratatui-crossterm` (with the `instability`/`darling` proc-macro tree behind
    it) — tuika now writes to the terminal through its own crossterm `Backend`.
    It already implemented that whole trait once, in `HyperlinkBackend`, to wrap
    the one being removed. The emitted byte stream is held **byte-for-byte
    identical** by a differential test that draws the same cells through both
    backends. The crate remains an *optional* dependency of the
    `scrolling-regions` feature, which forwards its matching flag so a host's
    own copy still compiles.
  - `tokio-stream` (under `async`) — replaced by `futures-core`, which the
    build already contained: `async` enables `crossterm/event-stream`, and that
    pulls it in. `tokio_stream::Stream` *is* `futures_core::Stream`, so a host
    passing a `ReceiverStream` is unaffected; `tokio-stream` stays a
    dev-dependency and the doc example still uses it. The `async` graph goes
    from 71 crates to 59.

  No public API changed, and no rendered output changed outside the wrapping
  differences above.

## Added

- **Input routing**: the new `routing` module (`Router`, `Delivery`,
  `RouteStage`, `InputTarget`, all re-exported from the crate root and the
  prelude) delivers an event to the surface that owns input this frame. A host
  registers each surface once and every `Event` kind follows the same route, so
  a paste can no longer take a different path from a key and land behind an open
  overlay. `Router` reads the `FocusRegistry` an overlay-bearing `Scene` already
  synchronized — and does not hold it, so a stage closure may take `&mut` on the
  host — resolves stages in a fixed order (global chord, declared exceptions,
  the active surface, last-chance global), and reports through `Delivery` which
  surface received what, including the case where nothing did. See the
  [routing guide](/routing/).

- **`term::hyperlink::apply_buffer_links_in`**: the area-clipped counterpart to
  `apply_buffer_links`, for a component embedding OSC 8 runs into a sub-rect of
  a shared buffer. `apply_buffer_links` is unchanged and still clips to the
  whole buffer.

- **Fuzz and black-box robustness suites** (`src/tests/fuzz.rs`,
  `tests/robustness.rs`): adversarial text and event streams through the wrap
  solver, composed view trees, the stateful components, and the parsers that
  read untrusted terminal replies; plus a sweep of every component against every
  corpus at every degenerate size, asserting no panic, no paint outside the
  component's own rect, and no control byte in a cell. Both fixes below came out
  of them. Test-only — no API change.

- **Session stress suite** (`tests/stress_ui.rs`): every screen mode driven
  through `Runner` and `AsyncRunner` over an in-memory screen that resizes
  between frames, plus mode changes mid-session, adversarial scrollback
  publishing, and shell/overlay/dock composition at degenerate sizes. The
  split-footer fix below came out of it. Test-only — no API change.

## Fixed

- **A split footer is pinned and published at the terminal's current size.**
  Both runners write the footer's reserved rows, and render each queued
  scrollback block, at the geometry the `Terminal` last observed — and neither
  observed a resize before the loop's first frame or before draining the
  publish queue. Two consequences, both on the ordinary path: a window resized
  between the terminal being constructed and the first frame pinned the footer
  at the old size, and a block published in response to a resize (a host
  logging the new dimensions, a status line reflowing) was committed to the
  scrollback at the previous width. Both loops now learn the size first.

- **A component no longer stamps hyperlink markers outside its own rect.**
  `Paragraph` and `Markdown` embed OSC 8 runs into the cells of a linked label
  after painting, and the placement was clipped to the *buffer* rather than to
  the rect the component was laid out in. A markdown block whose links ran past
  the rows or columns it was given wrote an escape onto a neighbouring
  component's cell — visible as a stray link, or an orphaned escape once the
  neighbour repainted. Placements outside the area are now dropped.

- **Streamed markdown renders identically to a one-shot render.** The settled
  prefix is cached at the last blank line that ends a block, and two constructs
  were mistaken for one:
  - A fence line carrying an info string (`` ```rust `` inside an already-open
    block) was treated as a closer, so a later blank line settled a boundary
    *inside* a code block; everything after it rendered as markdown while a
    re-render of the same source rendered it as code.
  - A blank line between list items settled the prefix, so the rest of the list
    was parsed in isolation and its numbering restarted — a streamed
    `1. … 2. … 3. …` rendered as `1. … 1. … 1. …`. A blank line inside a list is
    a boundary only once a following top-level block proves the list ended, and
    never on the strength of an in-flight partial line (`1` before it becomes
    `1.`).

  Both were found by a new streaming/one-shot fuzz differential, and both
  depended on where the stream's chunk boundaries happened to fall.

- **A link's underline stops at the URL.** When markdown wrapped a run of spans,
  the space it re-inserted between two words inherited the style and OSC 8
  target of the word *before* it, so the underline and the link's hit area ran
  one cell past a `[label](url)` or a bare URL into the text that followed. The
  separator now carries the style of the source whitespace it stands for, and a
  boundary with no whitespace behind it no longer has a space invented for it.

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