Skip to content

0.0.13 cohort: interactive CLI, first slice

Published on 2026-09-29. Every workstream in the 0.0.13 plan is merged, the release test below passed on the integrated tree, and every package is on crates.io and PyPI; see Publication.

0.0.13 lets a command line ask, choose and browse, not only print:

  • a new crate, rs-rich-interact, with interactive components (a fuzzy selector and multi-selector, input, confirm, form, a pager with search, a multi-line text area, and file, colour and asset pickers), each run either as a blocking call that returns a value or inside a small event loop, with optional mouse support;
  • frames: a styled-cell frame with a cell diff, so interactive views and Live repaint only what changed, with semantic regions and HTML and SVG export;
  • the same components from the rich binary (rich choose, filter, input, confirm, pager, write, file, color, asset) and from Python (rs_rich.interact);
  • third-party plugins, compiled in or loaded at run time as native libraries or sandboxed WASM modules, both off by default;
  • the rest of rich-cli 1.8.1's options, including --rst;
  • rich record: every terminal recording in the docs is a tape, run by the CLI's own recorder, with PNG, SVG, GIF, cast and HTML output.

A default build of rs-rich still renders exactly like upstream rich 15.0.0. Core gains one opt-in extension point, RegionSink, which reports where panels, tables, headings and code blocks land; without a sink installed, output is byte for byte as before.

Independent package versions

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

Package Version Release role
rs-rich 0.0.9 The opt-in RegionSink extension point (#226, DIVERGENCES §36); default output unchanged
rs-rich-plugin-api 0.0.2 export_plugin!, the runtime plugin ABI for native and WASM plugins, action registration
rs-rich-macros 0.0.3 Manifest only: requires core 0.0.9
rs-rich-art 0.0.11 Manifest only: requires core 0.0.9
rs-rich-mermaid, rs-rich-lumis 0.0.2 Manifest only: require core 0.0.9 and plugin API 0.0.2
rs-rich-ext 0.0.11 Frames, RenderTarget::frame, the live cell diff, snapshot schema 3 with regions, frame HTML/SVG export, the rich-rst port behind --rst, linked plugins and the runtime loaders
rs-rich-interact 0.0.1 (new) The interactive layer: session, events, event loop, viewport, degradation policy, components, mouse, actions
rs-rich-record 0.0.1 (new) Tapes and rich record: PTY, VT emulator, PNG/SVG/GIF/MP4/HTML and cast renderers
rs-rich-cli 0.0.13 Interactive commands, rich-cli options, --rst, rich record, plugins, non-UTF-8 paths
rs-rich on PyPI 0.0.2 rs_rich.interact (with the text area and pickers), frames, the new CLI commands, nesting depth as Rich's

What changed

The changelog has the full list. By workstream:

Workstream Pull requests
1. Frames (#226) #597
2. rs-rich-interact foundation (#451, #452, #489, #492, #495) #603
3. First components (#287, #288, #289, #291, #454, #457, #470, #472) #604
4. rich-cli options (#542), --rst, and the interactive commands (#493, #494) #605, #606, #607
5. Python: rs_rich.interact and frames #609
6. Hardening carried over from 0.0.12 #608
7. Tapes and rich record (#598, #599, #600) #601, #602
Scope added before release (#617)
9. Plugins: compiled in, native, WASM (#14, #232) #618
10. rich write, file, color, asset, mouse and actions (#493, #476, #491) #620
11. Regions, frame HTML/SVG export, rich record outputs (#226, #598, #600) #619

Migration

  • -h means --head, as in rich-cli 1.8.1, so rich -h 20 file works as upstream scripts expect. Help is --help only. Anyone who typed rich -h for help now gets a usage error that points at --help.
  • New command words. choose, filter, input, confirm, pager, write, file, color, asset, plugins and record are commands, as view and inspect are. A file literally named one of them needs a path: rich ./choose.
  • New default features in rs-rich-cli: interact and record. Build with --no-default-features and the features you want for a smaller binary.
  • Table cells from strings are markup (security note). This is not new, and it is upstream's behaviour, but it is easy to miss in Rust: add_row(&[..]), Cell::from(&str) and String/&str .into() all parse console markup, with no compiler warning. A model, column or file name containing [ can restyle, hide or link the rest of the cell. For any data you do not control, use a Text cell, which is never parsed: add_row_text(vec![Text::new(name)]) or Cell::from(Text::new(name)), as upstream's add_row(Text(name)). See Tables.
  • syntax and markdown are optional (core 0.0.9, ext 0.0.11). Both are default features, so nothing changes unless you opt out. An application that never renders Syntax or Markdown can set default-features = false on rs-rich (and rs-rich-ext) and drop syntect, bincode 1.x (RUSTSEC-2025-0141), a second fancy-regex and pulldown-cmark. If you already build with default-features = false and use either, add features = ["syntax", "markdown"].
  • Python nesting depth. Renderables used to stop at a fixed 100 levels; they now stop where Rich does (about 123 at the default recursion limit), following the recursion limit.

Release test (2026-09-28)

Check Result
python scripts/validate_release.py --tag rs-rich-cli-v0.0.13 All 15 steps pass: fmt, three Clippy configurations (default, all features, lean CLI), 1,848 Rust tests in 120 suites (0 failed, 2 ignored), 345 lean CLI tests, the release, readiness and package tests, version and CLI-reference checks, locked build and check, staged packages for every crate, tag plan
capture_golden.py against real rich 15.0.0 (its own venv, no rich-cli) All 36 golden files regenerate byte-identically
capture_rst_golden.py against rich-rst 1.3.2 on rich 15.0.0 Every --rst fixture regenerates byte-identically, including the new structure case
release.py plan per tag rs-rich-ext-v0.0.11, rs-rich-interact-v0.0.1, rs-rich-record-v0.0.1 and rs-rich-cli-v0.0.13 each select exactly their own package; python-v0.0.2 belongs to pypi-release.yml
crates.io and PyPI 404 for all five new versions
Consumer install cargo install from the packaged rs-rich-cli-0.0.13 source, with only the three unpublished siblings (ext 0.0.11, interact and record 0.0.1) patched to their packaged .crate contents and every other crate from crates.io: rich --version reports 0.0.13, and it recorded the release tape below
Docs tapes rich record --check passes for all seven tapes (the tour with its new "Ask in a script" section, and the release tape)
Python wheel maturin build --release, installed into a fresh Python 3.13 venv with rich==15.0.0 and pytest: the whole suite passes against the installed wheel (1,151 passed, 2 skipped); rich-rs --version reports the 0.0.13 CLI
Python sdist Installed from source into a fresh venv: builds, renders a Table, answers a headless Select from rs_rich.interact, and rich-rs filter works
Security cargo audit: nothing reported (the two unmaintained warnings are recorded in .cargo/audit.toml)
Docs mkdocs build --strict passes for the main site and the Python site; gen_python_api.py --check (98 pages), gen_versions.py --check and gen_cli_reference.py --check match

What the release test found and fixed

Four independent audits covered the whole 0.0.13 delta: - frames and rs-rich-record; - rs-rich-interact and the interactive commands; - the rich-cli options, --rst and paths; - the Python bindings.

About fifty findings were each reproduced first, then fixed with a regression test that failed before the fix. The changelog lists every one. The main ones:

  • Data loss.
  • A tape named ...tape made rich record write outside its output directory, and its orphan cleanup delete files there. Such names are now refused, and only screenshots the recorder itself listed are ever removed.
  • rich $'\xff.txt' --export-html '�.txt' overwrote its own input: a non-UTF-8 argument shared its lossy spelling with a real file. Non-UTF-8 arguments and paths are now spelled in a form no real path or argument can take.
  • Crashes and hangs.
  • --rst overflowed the stack on about 3 KB of nested list markers.
  • The pager's search panicked on text whose lower case changes byte lengths (Ⱥẞ).
  • A Python validator that started another run segfaulted the interpreter.
  • A Pretty of an endless iterable in 32 or more panels hung.
  • The fuzzy matcher could exhaust memory on a long query.
  • Terminal-emulator panics, and huge sizes or durations in a tape, now fail the tape instead.
  • Recording fast output grew without bound (about 4 GB in 15 s).
  • RST inline parsing was quadratic.
  • The terminal.
  • SIGTERM, SIGHUP and SIGQUIT left raw mode and the alternate screen behind.
  • Escape sequences in pager content, --preview output, pasted text, item labels and prompts reached the terminal. They are now painted as text.
  • The live cell diff left stale cells where a terminal measures an emoji differently from rich.
  • Scripts.
  • Without a terminal, confirm --default yes, input --default and choose --selected ignored their default at the end of input.
  • Invalid UTF-8 on stdin failed the command.
  • stdin was unbounded.
  • A slow --preview froze the picker.
  • --multi answered with nothing; --no-color, --report json, -J/-u with a configured mode, and --sanitize with --rule-char were ignored.
  • Parity.
  • Python nesting stopped one level short of Rich on CPython 3.12 and later. It now matches rich 15.0.0 exactly on 3.9 to 3.13, including max_depth and max_length.
  • --rst grid tables drop no text in spanned cells, section titles render their markup, and attributions keep the text after them.
  • --head/--tail count lines as Python does.
  • The pager no longer shows a file's final newline as an extra line.

Release test, round 2 (2026-09-29)

The scope added before release (plugins #618, regions and recordings #619, the new commands #620) went through the same test. Three independent audits covered it. Each finding was reproduced, then fixed with a regression test that failed before the fix; the changelog lists every one. The main ones:

  • Plugins.
  • A WASM plugin's table was unbounded: about 1.6 GB outside the 64 MiB memory cap. Tables are now capped too.
  • Plugin output could carry a hyperlink, showing one URL and linking to another. Links are now removed from all plugin output.
  • A native plugin with a null function in its vtable crashed rich on first use; it is now refused when it loads.
  • A linked plugin that clashed with a built-in went missing silently, with every linked plugin after it; the clash is now reported.
  • The new commands.
  • A huge --height hung write, color and file and used gigabytes.
  • rich file froze on a preview whose read blocks (/proc/kmsg, a stalled mount), Esc and Ctrl+C included. Previews now read on a thread of their own.
  • Escape sequences in --header reached the terminal through the line-prompt fallback (CI=1, TERM=dumb), in every interactive command.
  • rich write turned tabs into spaces and a piped \r\n into two lines; --char-limit could cut an emoji in half.
  • With --mouse, one click on the focused row picked it.
  • Recordings. SVG screenshots drew text in the wrong columns after a character rich and the terminal measure differently (a skin-tone emoji, a Devanagari vowel sign), and SVG export was quadratic in the number of styles (45 s for a large screenshot). The HTML page's player stopped at five minutes without a word; it is now refused like GIF and MP4.

After the round, three follow-ups from its known limits:

  • Ctrl+Z suspends an interactive command, and a SIGTSTP from outside does the same: the terminal is given back, and fg takes it again and repaints.
  • Python threads running the CLI at once no longer clear each other's plugins: in-process runs take turns.
  • --panel takes every box style rich asset --kind box offers, not only rich-cli's six (a CLI convenience, recorded in PORTING).

Validation on the fixed tree (ee738c9):

Check Result
python scripts/validate_release.py --tag rs-rich-cli-v0.0.13 All 16 steps pass: fmt, three Clippy configurations, 2,369 Rust tests in 166 suites (0 failed, 2 ignored) including the lean CLI's, the native and WASM plugin loaders with both features on, the release, readiness and package tests, version and CLI-reference checks, locked build and check, staged packages for every crate, tag plan
capture_golden.py against real rich 15.0.0 (its own venv) Every golden regenerates byte-identically: the region seam leaves default output unchanged
release.py plan per tag Each of the ten crate tags, from rs-rich-v0.0.9 to rs-rich-cli-v0.0.13, selects exactly its own package; python-v0.0.2 belongs to pypi-release.yml
Docs tapes rich record --check passes for all 12 tapes (49 screenshots)
Python wheel Installed into a fresh venv with rich==15.0.0: 1,162 passed, 1 skipped
Security cargo audit: no vulnerabilities (advisory database of 2026-09-28)
Docs mkdocs build --strict passes

Known limits

  • Windows --preview: items that cmd.exe cannot quote safely are refused rather than passed on. See the CLI guide.
  • Without a terminal: the Python Pager writes its content to stderr, because the line fallback writes there.
  • Stray preview processes: a killed preview command can leave its children running until they finish.
  • Suspending: SIGSTOP (not Ctrl+Z or SIGTSTP, which give the terminal back and take it again on fg) cannot be caught, so it leaves raw mode, the alternate screen and mouse reporting on while an interactive command is stopped; they are restored when it continues and on every exit.
  • Blocked previews: a rich file preview whose read never finishes keeps one thread waiting until the command exits; the picker stays usable.
  • rich record: a tape is capped at 500 columns by 200 rows. GIF, MP4 and the HTML page's player take at most five minutes; record a longer tape with --no-video.

Installed-binary screenshots

Each image is a real PTY session of the rich installed from the packaged crates, recorded by that binary's own rich record from docs/tapes/release-0.0.13.tape. CI replays the tape on every change and compares each screenshot's text. The cast, the GIF and provenance.json (the tape's fingerprint, versions and commit) are in docs/media/tapes/release-0.0.13/.

rich choose, answering a captured substitution

The answer, back in the shell with exit 0

rich filter over a piped list: keys come from the terminal

rich confirm answers with its exit code

rich pager with a search

rich-cli's line numbers, guides and --head

--rst: reStructuredText as rich-rst renders it

Publication

Independent annotated tags, published one 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, then its Trusted Publishing entry;
  7. rs-rich-record-v0.0.1, after ext, then its Trusted Publishing entry;
  8. rs-rich-cli-v0.0.13;
  9. python-v0.0.2.

crates.io offers Trusted Publishing only on a crate that already exists, so the two new crates' first versions upload with the crates-io environment's token: the workflow sees that the crate is new and uses it for that upload only. Both must be on crates.io before the CLI tag. See Registry authentication.

All eleven tags went out from main at edbf73f on 2026-09-29.