Skip to content

0.0.13 plan — interactive CLI, first slice

Status: agreed on 2026-09-26, after 0.0.12 was published. Every workstream, and the scope added on 2026-09-28, is merged. The release test ran again over the added scope (round 2, see the release notes); its fixes merged as #621, the follow-ups as #622 and the dependency updates as #623, and the optional syntax/markdown features with macOS CI as #624. Published on 2026-09-29: every tag went out from main at edbf73f. It tracks milestone 4, "0.0.13 - Interactive CLI / rs-rich-interact".

Goal: let a CLI ask, choose and browse, not only print. That means:

  • a new crate, rs-rich-interact, with a small set of interactive primitives (selector, input, form, confirm, pager with search). Each one runs either as a blocking call that returns a value, like Prompt.ask, or inside a small event loop, which is the first step towards intuiTUIve;
  • frames (#226): a styled-run frame with a cell diff, so interactive views and Live repaint only what changed;
  • the same primitives from the rich binary (rich choose, rich input, …) and from Python;
  • the rich-cli options the binary still lacks (#542).

Why this theme: milestone 4 already names it, and it is where the backlog points. Its 60 issues are almost all interactive components. The 0.0.12 render tree spike measured what they need underneath: a one-cell change in a 50-row table repaints in 25 bytes as a cell diff, against 4,385 for a full repaint (see the design note). So frames come first, and the components build on them.

What stays the same: core's behaviour. A default build still behaves like upstream rich 15.0.0. Key input, raw mode and repainting live in the new crate.

Changed on 2026-09-28: the semantic-regions trait, first deferred as the only frame item that needs core, was brought into 0.0.13 with stream 11 (render tree and recordings). It adds an opt-in extension point to crates/rich/src/protocol.rs (RegionSink, DIVERGENCES §36) that leaves default output byte for byte as it was. It is still a public API addition, and rs-rich 0.0.8 is already on crates.io, so rs-rich moves to 0.0.9 instead of staying at 0.0.8.

Carried over from 0.0.12

Item Where it goes
#542 rich-cli's missing rendering options Workstream 4
#226 render tree Workstream 1 builds the ext phase; semantic regions are deferred
0.0.12 audit follow-ups (below) Workstream 6
The CLI README still says 0.0.12 is "prepared, not yet published" Fixed by the CLI version bump. The README ships inside the crate, so it can only change with a new version
#529, #222, #197 Delivered in 0.0.12; closed at kickoff
#14, #232, #16 Ongoing epics; move to milestone 4 unchanged
#144 art in the CLI Stays open for animation (#127), Kitty (#129) and FIGlet (#130); moves to milestone 9 (rich-art)

The 0.0.12 audit follow-ups, recorded in the release notes and changelog:

  • batch globbing and the path checks in tools.rs still use a lossy spelling of file names that are not valid UTF-8 (opening such a file already works);
  • Pretty inside a Panel hits Python's recursion limit about 8 levels before Rich does, and a thread with a 256 KiB stack can still overflow on deep pretty output;
  • images are capped by a fixed options.height, not by the terminal height; whether a tall image should shrink to the terminal is an open question;
  • bincode 1, pulled in by syntect, is flagged unmaintained by cargo audit (no vulnerability).

Workstreams

Each workstream lands as its own pull request, or a short stack, with evidence in the PR and an entry under Delivered. Placement follows AGENTS.md: nothing in core, frames in rich-ext, interaction in the new rs-rich-interact crate, and the CLI only composes.

1. Frames (#226) — ext

The first phase of the design note's proposed milestone, without its core item:

  • rich_ext::frame: a frame of rows of styled runs, with interned styles, an exact encoder, a merged encoder, cells derived on demand, and a diff;
  • RenderTarget::frame, with the same filtering as segments();
  • a cell diff in the LiveCoordinator, so Live, Progress and the new interactive views repaint changed cells only;
  • snapshot schema 2 (frame based). Output that is resegmented but otherwise identical compares equal, and schema 1 fixtures still load.

Acceptance:

  • The exact encoding is byte-identical to segments_to_string on every golden and on the differential corpus.
  • Bytes written drop on the live tests, and the terminal-emulator tests pass unchanged.
  • Nothing changes default output.

2. rs-rich-interact foundation (#451, #452, #489, #492, #495) — new crate

The layer between static rendering and a full TUI framework (epic). Every component is a state machine: it handles one event and renders one frame. Two drivers run it (decided 2026-09-26, see Decisions):

  • blocking: component.run() takes the terminal, drives the component to a result and gives the terminal back, like Prompt.ask;
  • event loop: an EventLoop runs one or more components at once, with timers and ticks, and repaints only when something changed. It is the base the intuiTUIve track (milestone 11) builds on, not a second framework next to it.

Which driver a program uses is its choice, per call or through configuration; the same component works under both. The crate also contains:

  • a terminal session:
  • raw mode, an optional alternate screen, and cursor and mouse modes;
  • restoration on every exit path, including a panic, Ctrl-C and an external command handoff (#489);
  • key and mouse events, decoded by crossterm;
  • a viewport (#495): a scrollable window over a frame. Components render into it, and the session repaints it through the frame diff from workstream 1;
  • a universal item model (#452): one label, description, preview and metadata type for every picker;
  • a degradation policy (#492): with no terminal on stdin or stdout, under CI, with TERM=dumb, or when the caller asks, each component falls back to a line-based prompt, or returns its default, or errors, as the caller chooses. Nothing blocks on a pipe;
  • a headless driver for tests: scripted keys in, frames out. Every component is tested with it, and the goldens stay byte-exact.

Acceptance:

  • The terminal is restored after every way out, tested in a PTY.
  • Every component runs headless, and under both drivers with the same output.
  • The event loop repaints only on a change: an idle loop writes nothing.
  • The crate depends on core, ext and crossterm, and on nothing else heavy.

3. First components (#287, #288, #289, #291, #457, #470) — interact

Each component runs under either driver and returns a value:

  • a fuzzy selector, single and multi (#287), with an optional preview pane (#454);
  • text input with validation and async suggestions (#457, #289);
  • a multi-field form, with inline errors (#288, #472);
  • a confirmation sheet showing what will happen (#470);
  • a pager with search over any rendered output (#291).

Acceptance:

  • Each component has headless tests and a PTY test.
  • Each one works with the degradation policy.
  • Each one has a guide page with a recorded example.

4. CLI: interactive commands and rich-cli parity (#493, #494, #542) — CLI

  • Gum-style commands (#493, #494):
  • rich choose, rich filter, rich input, rich confirm and rich pager;
  • scriptable: the result goes to stdout and the exit code reports cancel;
  • they follow the degradation policy when piped.
  • The missing rich-cli 1.8.1 options (#542):
  • --head/--tail, -n/--line-numbers, --guides, --lexer, --emoji, --soft, --no-wrap, --max-width;
  • the --text-* alignments, --rule-style/--rule-char, --rst, --force-terminal;
  • the short aliases.

Each option is checked against the rich-cli oracle venv for semantics. AGENTS.md says to use that venv only for CLI semantics, never for rendering. -h becomes --head as upstream; see Decisions.

Acceptance:

  • Every #542 option has a test, and PORTING's 🔴 row goes green.
  • The interactive commands have PTY tests and piped tests.
  • gen_cli_reference.py --check passes.

5. Python: interactive primitives and frames — crates/rich-py

Keep the Python package level with the Rust crates, as 0.0.12 made it. This adds:

  • rs_rich.interact, with the workstream 3 components;
  • Console frame access from workstream 1;
  • the new CLI commands, through rich-rs;
  • type stubs, docs pages and tests, as for the other modules.

Rich has no interactive components, so there is nothing to compare against; the tests compare with the Rust crate, as rs_rich.ext does.

Acceptance:

  • The Python suite covers each component headless.
  • gen_python_api.py --check passes.
  • The wheel builds on all five platforms.

6. Hardening carried over from 0.0.12

  • Non-UTF-8 paths: keep OsString through batch globbing and tools.rs, with tests on Unix.
  • Pretty:
  • make the nested-Panel depth match Rich;
  • move deep pretty rendering off the caller's stack everywhere it can recurse, not only in the walk.
  • Tall images: decided to keep today's behaviour (a fixed options.height caps rows; the terminal height does not). Record it in the art docs and close the question (see Decisions).
  • syntect: check whether a release has dropped bincode 1. Upgrade if so, and record the advisory in deny/audit configuration if not.

7. Tapes: scripted capture of interactive sessions (#598) — tooling

Added on 2026-09-27. The interactive commands need to be shown being used, and no capture script so far could type. One runner replaces the per-release capture scripts:

  • a declarative tape per demo (docs/tapes/*.tape): Set, Type, keys, Wait "text" or Wait /regex/ (on the emulated screen, never a fixed sleep), Sleep, Screenshot name, Hide/Show, Resize;
  • a runner (first scripts/tape.py, now rich record) runs the real binary in a PTY at a fixed size with its environment pinned, and follows the screen through a terminal emulator;
  • from one tape: PNG, SVG and a text grid at each Screenshot, an asciinema .cast (played on the docs site), a GIF and an MP4 with a key overlay, and provenance. An animated SVG was considered and dropped: the playable cast already gives sharp, selectable text;
  • --check in CI re-runs every tape and fails when a screenshot's text grid differs from the committed one, so docs media cannot go stale and the tapes double as end-to-end tests;
  • a docs gallery page with playable recordings, and a hero recording for the README.

Acceptance: tapes for every interactive command and the guided tour, one command to regenerate all media, and a green --check job that fails on a deliberately stale screenshot.

Stage 2, rich record (#599, outputs in #600), moved into this release (decided with the maintainer on 2026-09-27):

  • a new crate, rs-rich-record (rich_record): the tape parser, a PTY session (portable-pty), a VT emulator (vt100) feeding rich_ext::frame::Frame, and the renderers: SVG and text through the frame, PNG and GIF rasterised with an embedded DejaVu Sans Mono (--font overrides), and the .cast, plus MP4 through FFmpeg when it is present;
  • rich record TAPE… [--check] [--output DIR] [--format …] in rich-cli, a thin command over the crate;
  • the same tape format as stage 1. rich record --check reproduced every screenshot scripts/tape.py committed, byte for byte, so CI switched to it and the Python runner is retired;
  • Linux and macOS tested in CI; Windows builds through ConPTY and is documented as experimental.

8. Release test

As in 0.0.11 and 0.0.12:

  • independent audits of the delta;
  • validate_release.py, goldens, tag plans, consumer installs from packages and then from the registries, the wheel and the sdist;
  • screenshots and the demo tour from tapes, which should now show a rich choose;
  • release notes.

Scope added before release (2026-09-28)

The first slice merged as #605 to #609. Before tagging, we decided to finish the partial issues and the plugin ecosystem in 0.0.13 rather than defer them. The tags wait for this work, and the release test runs again over all of it.

Stream Issues What it adds
9. Plugins #14, #232 Third-party plugins that load in three ways. Compile time: a registration macro, collected at link time. Runtime, native: dynamic libraries behind a versioned C ABI. Runtime, sandboxed: WASM modules. Loading is off by default. Plugins are trusted through the config trust list, and rich plugins list/info shows what is available. Both runtime loaders sit behind Cargo features.
10. Interactive commands #493, #476, #491 rich write (a multi-line editor), rich file (a file picker), rich color (a colour picker) and rich asset (an icon and emoji picker). Mouse support for links, buttons and pane resizing. Custom actions on tables, trees and files, which plugins can register.
11. Render tree and recordings #226, #598, #600 Semantic regions in frames, plus HTML and SVG export built from a frame, links included. rich record gains HTML output, switches for the window frame, captions and key overlay, a per-tape Output directive, and screenshots from rich-ext's exporters. A tape covers every interactive command, and the gallery cards play when you hover over them.

Constraints: - Core stays a faithful mirror. #226 needs a seam, which is added as an extension-point trait in crates/rich/src/protocol.rs. The default build of core behaves exactly as upstream does, and the goldens stay byte-identical. - Loading native and WASM plugins runs third-party code. Neither is ever on by default: it takes a Cargo feature, then config trust, then an explicit path. WASM plugins run in a sandbox with no host access beyond the plugin API. - #451 is closed: its acceptance was met by #603, #604, #607 and #609.

Order of work

  1. Workstream 1, frames. The viewport repaints through the frame diff, so everything interactive depends on it.
  2. In parallel:
  3. workstream 2, the interact foundation, starting from the session and headless driver while frames finish;
  4. workstream 4's #542 part, which is independent;
  5. workstream 6;
  6. workstream 7, tapes, starting with the existing commands and the tour. Every terminal recording is a tape recorded in Rust by rich record; the Python capture scripts (tour, release demos, screenshot kit) are retired.
  7. Workstream 3, components, one PR each after the foundation.
  8. Workstream 4's interactive commands, once the components they wrap exist.
  9. Workstream 5, Python, after the Rust API it wraps settles.
  10. Release test.

Decisions

Decided with the maintainer on 2026-09-26 unless marked as a default.

  • A new crate, rs-rich-interact (rich_interact), not a module in ext (decided). It brings crossterm and raw-mode code that ext users who only print should not compile. It starts at 0.0.1 with a first manual token upload, like the three new 0.0.12 crates.
  • crossterm for input and terminal modes. It is maintained and cross-platform, and Ratatui uses it, which eases the #170 adapter later. termion is Unix only.
  • Both a blocking call and an event loop (decided). Components are state machines, and the program chooses the driver per call or by configuration. The event loop stays minimal (events, timers, repaint on change) so that intuiTUIve's component tree, reactive state and focus routing (#160–#165) can grow on top of it instead of replacing it.
  • -h means --head, as upstream (decided 2026-09-27). Help is --help only, matching rich-cli 1.8.1, so upstream scripts that run rich -h 20 file work unchanged. This is a breaking change for anyone who types rich -h for help today; the CLI 0.0.13 CHANGELOG entry calls it out.
  • Merged-run encoding stays off by default (default). The ext live and export paths keep exact encoding, so a diff against an upstream-rendered file shows no SGR noise. Merged runs are opt-in.
  • Style interning hashes in ext (default), from Style's public accessors, so core does not change.
  • Wide-grapheme continuation cells are stored (default), as the prototype does, which keeps diffs simple.
  • Tall images still follow options.height only (decided). Capping them to the terminal height would shrink every image taller than the screen, which is a visible change for users who scroll. We would revisit that only with a --image-fit screen option.
  • --panel takes every box style (decided on 2026-09-29). rich-cli's --panel offers six names; rich asset --kind box offers all nineteen of rich's box constants, so the binary takes them all. It is a CLI boundary convenience (recorded in docs/PORTING.md): name lookup only, core unchanged, and rich-cli's six keep their meaning.

Not in this release

  • Semantic regions and the frame-based HTML/SVG export with links were deferred here, and are now in scope: see "Scope added before release" (stream 11). Markdown export (#274) and copy as Markdown, HTML or text (#370) stay in the reports milestone.
  • The rest of milestone 4:
  • the pickers for dates, themes and images, and the icon/emoji picker issue (#456, #458, #460, #462, #463). The colour and asset pickers that rich color and rich asset need are now in scope (stream 10), so #459 and #461 are covered by that work;
  • the explorers (#464–#466);
  • merge-conflict resolution (#467), REPL (#469) and approvals (#471);
  • the overlays (#473–#475), reorderable lists (#477), and tabs, splits and modals (#478–#480). Mouse links, buttons and pane resizing (#476) and custom actions on tables, trees and files (#491) are now in scope (stream 10);
  • navigation extras (#481–#487) and workspace persistence (#490);
  • interactive tables (#430–#434), trees (#428) and shell helpers (#404–#406).

They stay in milestone 4 as the second slice. Each builds on the foundation and components here. - Anything that makes core interactive. Core stays a faithful mirror, and upstream rich has no interactive layer to mirror.

Package impact

Package Proposed Reason
rs-rich 0.0.9 Stream 11 adds an opt-in extension-point trait, RegionSink, in protocol.rs for semantic regions (#226). The default behaviour is unchanged and the goldens stay byte-identical, but the public API grows and 0.0.8 is already on crates.io.
rs-rich-plugin-api 0.0.2 Stream 9 (#14, #232): export_plugin!, the runtime plugin ABI for native and WASM plugins, action registration. It also requires core 0.0.9. 0.0.1 is published.
rs-rich-macros 0.0.3 Manifest only: requires core 0.0.9, and 0.0.2 is published.
rs-rich-art 0.0.11 Manifest only: requires core 0.0.9, and 0.0.10 is published.
rs-rich-mermaid, rs-rich-lumis 0.0.2 Manifest only: they require core 0.0.9 and plugin API 0.0.2, and 0.0.1 of each is published.
rs-rich-ext 0.0.11 Frames, RenderTarget::frame, live cell diff, snapshot schema 3 with regions, frame HTML/SVG export, linked plugins and the runtime loaders
rs-rich-interact 0.0.1 (new) The interactive layer
rs-rich-record 0.0.1 (new) Tapes and rich record (#599, #600)
rs-rich-cli 0.0.13 Interactive commands, #542 options, --rst, non-UTF-8 paths, plugins, record outputs
rs-rich (PyPI) 0.0.2 rs_rich.interact, frames, the new CLI commands

Cargo reads a 0.0.x requirement as an exact version. Every published crate that depends on core, or on the plugin API, therefore moves with them, even when only its manifest changes.

Publication order, one tag at a time, each after its dependencies are on crates.io:

  1. rs-rich-v0.0.9, for the region seam;
  2. rs-rich-macros-v0.0.3 and rs-rich-art-v0.0.11, after core;
  3. rs-rich-plugin-api-v0.0.2, after core;
  4. rs-rich-mermaid-v0.0.2 and rs-rich-lumis-v0.0.2, after the plugin API;
  5. rs-rich-ext-v0.0.11;
  6. rs-rich-interact-v0.0.1, after ext: a first upload with the crates-io environment's token, then its Trusted Publishing entry;
  7. rs-rich-record-v0.0.1, after ext, the same way;
  8. rs-rich-cli-v0.0.13;
  9. python-v0.0.2.

Delivered

Workstream Pull request Merged as
1. Frames (#226) #597 9157264
7. Tapes and rich record (#598, #599, #600) #601 497fe23
7. Recorder follow-up: colour emoji, clusters at rich's width, Set Shell #602 ca684ac
2. rs-rich-interact foundation (#451, #452, #489, #492, #495) #603 08ab018 (rebased)
3. First components (#287, #288, #289, #291, #454, #457, #470, #472) #604 4f3c039
4. rich-cli 1.8.1 options (#542) #605 81f7137
4. --rst (the rich-rst port) #606 4e3880a
4. Interactive commands (#493, #494) #607 79963af
6. Hardening carried over from 0.0.12 #608 aff1cb9
5. Python: rs_rich.interact, frames, the commands in the wheel #609 0c3a77e
8. Release test: audits and fixes, tour, release tape, validation in #609's stack 0c3a77e
Scope added before release: this plan #617 d019de5
9. Plugins (#14, #232) #618 4fa3a61
11. Regions, frame HTML/SVG export, record outputs (#226, #598, #600) #619 d4d5769
10. rich write, file, color, asset, mouse, actions (#493, #476, #491) #620 07a9234
Release test, round 2: audits of 9–11 and fixes, validation (tested at ee738c9) #621 6455e3b
Follow-ups: Ctrl+Z suspend, in-process runs take turns, --panel box names #622 7dbfda2
Dependencies: syn 3, color_quant 2, log, thiserror, artifact actions #623 b93faa5
Optional syntax/markdown in core and ext, macOS CI and its fixes, table-cell security note #624 01386f9

Review focus

  1. Core is untouched. git diff shows nothing under crates/rich/src. Every golden and the differential corpus are byte-identical.
  2. The terminal is always restored. Raw mode and the alternate screen are undone on every exit, including a panic, Ctrl-C and SIGTERM, shown in a PTY.
  3. Nothing blocks on a pipe. Every component and command has a piped test that ends without input.
  4. Frames are exact. Exact encoding matches segments_to_string byte for byte, and a diff applied to the old frame gives the new frame.
  5. The rich-cli options match upstream semantics, checked against the isolated rich-cli oracle venv, never the rendering oracle.