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, likePrompt.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
Liverepaint only what changed; - the same primitives from the
richbinary (rich choose,rich input, …) and from Python; - the
rich-clioptions 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.rsstill use a lossy spelling of file names that are not valid UTF-8 (opening such a file already works); Prettyinside aPanelhits 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; bincode1, pulled in by syntect, is flagged unmaintained bycargo 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 assegments();- a cell diff in the
LiveCoordinator, soLive,Progressand 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_stringon 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, likePrompt.ask; - event loop: an
EventLoopruns 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, withTERM=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 confirmandrich 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 --checkpasses.
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;Consoleframe 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 --checkpasses.- The wheel builds on all five platforms.
6. Hardening carried over from 0.0.12¶
- Non-UTF-8 paths: keep
OsStringthrough batch globbing andtools.rs, with tests on Unix. - Pretty:
- make the nested-
Paneldepth 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.heightcaps 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
bincode1. Upgrade if so, and record the advisory indeny/auditconfiguration 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"orWait /regex/(on the emulated screen, never a fixed sleep),Sleep,Screenshot name,Hide/Show,Resize; - a runner (first
scripts/tape.py, nowrich 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; --checkin 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) feedingrich_ext::frame::Frame, and the renderers: SVG and text through the frame, PNG and GIF rasterised with an embedded DejaVu Sans Mono (--fontoverrides), and the.cast, plus MP4 through FFmpeg when it is present; rich record TAPE… [--check] [--output DIR] [--format …]inrich-cli, a thin command over the crate;- the same tape format as stage 1.
rich record --checkreproduced every screenshotscripts/tape.pycommitted, 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¶
- Workstream 1, frames. The viewport repaints through the frame diff, so everything interactive depends on it.
- In parallel:
- workstream 2, the interact foundation, starting from the session and headless driver while frames finish;
- workstream 4's #542 part, which is independent;
- workstream 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. - Workstream 3, components, one PR each after the foundation.
- Workstream 4's interactive commands, once the components they wrap exist.
- Workstream 5, Python, after the Rust API it wraps settles.
- 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 bringscrosstermand 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. crosstermfor input and terminal modes. It is maintained and cross-platform, and Ratatui uses it, which eases the #170 adapter later.termionis 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.
-hmeans--head, as upstream (decided 2026-09-27). Help is--helponly, matching rich-cli 1.8.1, so upstream scripts that runrich -h 20 filework unchanged. This is a breaking change for anyone who typesrich -hfor 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.heightonly (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 screenoption. --paneltakes every box style (decided on 2026-09-29). rich-cli's--paneloffers six names;rich asset --kind boxoffers all nineteen of rich'sboxconstants, so the binary takes them all. It is a CLI boundary convenience (recorded indocs/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 colorandrich assetneed 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:
rs-rich-v0.0.9, for the region seam;rs-rich-macros-v0.0.3andrs-rich-art-v0.0.11, after core;rs-rich-plugin-api-v0.0.2, after core;rs-rich-mermaid-v0.0.2andrs-rich-lumis-v0.0.2, after the plugin API;rs-rich-ext-v0.0.11;rs-rich-interact-v0.0.1, after ext: a first upload with thecrates-ioenvironment's token, then its Trusted Publishing entry;rs-rich-record-v0.0.1, after ext, the same way;rs-rich-cli-v0.0.13;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¶
- Core is untouched.
git diffshows nothing undercrates/rich/src. Every golden and the differential corpus are byte-identical. - 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.
- Nothing blocks on a pipe. Every component and command has a piped test that ends without input.
- Frames are exact. Exact encoding matches
segments_to_stringbyte for byte, and a diff applied to the old frame gives the new frame. - The rich-cli options match upstream semantics, checked against the isolated rich-cli oracle venv, never the rendering oracle.