0.0.12 plan — plugin platform and extensibility¶
Status: every workstream merged; the release test is under way (see the 0.0.12 release notes). The maintainer started it on 2026-09-24, after 0.0.11 was published. It tracks milestone 3.
Goal: Make rs-rich something other crates plug into, not just something
they call. That means:
- one public, documented way to register extensions;
- a pluggable code highlighter that proves the pattern;
- Markdown code fences that plugins can render;
- two first-party plugins (lumis and Mermaid) built only on the public API, so anything they do a third-party crate can do too;
- a first slice of Python bindings, so Rich users can try the Rust engine by changing imports.
Why this theme: 0.0.11 gave Rust CLI authors a wide set of renderers, but
every one of them is wired in by us. The registry in rich-ext is still internal
(see Plugins), and the
highlighter review during the 0.0.11 release test showed users want to choose
their own engine. The invariant from AGENTS.md doesn't change: core never
learns about a specific extension. It gains extension-point traits only, and
a default build still behaves like upstream rich 15.0.0.
Carried over from 0.0.11¶
In milestone 3 as of 2026-09-24:
| Issue | What remains |
|---|---|
| #144 Art in the CLI | The native-sizing route (#519). Animation (#127), Kitty (#129) and FIGlet (#130) stay outside this milestone |
| #519 (new) | Native image sizing, split out of #126 |
| #521–#526 (new) | Pluggable code highlighters, from the 0.0.11 highlighter review |
| #16 Roadmap epic | Ongoing; updated when this plan ships |
The 0.0.11 CLI README still says "prepared, not yet published". It ships inside
the published rs-rich-cli 0.0.11 package, so it can only change with the next
CLI version. Workstream 1's version bump fixes it.
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:
- extension-point traits go in core
protocol.rs, with core's default behaviour unchanged; - the registry and plugin API go in
rich-ext; - adapters that bring heavy or native dependencies get their own crates;
- the CLI only composes.
1. Code highlighter adapters (#521–#526) — core, then new crate, ext, CLI¶
The epic as filed:
-
522 adds the
routesCodeHighlightertrait andHighlightedCodetypes, and¶Syntaxand Markdown code through them. -
523 makes syntect the default adapter, byte-identical to 0.0.11, with¶
ansi_dark/ansi_lightthemes. -
524 adds the new
everyone else's build because of the tree-sitter linking conflict.rs-rich-lumiscrate: a tree-sitter adapter, kept out of¶ -
525 adds a console-wide default,
support.--highlighter, config anddoctor¶ -
526 adds a conformance kit, a "write your own adapter" guide and¶
benchmarks.
Acceptance:
- Default output is unchanged: goldens pass, and the 558-file differential run
from #520 is byte-identical with and without onig.
- Both adapters pass the conformance kit.
- Each adapter keeps its own theme names, and all provide ansi_dark/ansi_light.
2. Public plugin API (#14, #232 phase 1) — new crate rs-rich-plugin-api, then ext¶
A new, deliberately small crate, rs-rich-plugin-api (library rich_plugin_api),
holds the plugin contract. It depends only on core. rs-rich-ext depends on it
and hosts plugins through ExtensionRegistry. A plugin crate depends on the
plugin API, and on core for the types it renders, but never on ext. So:
rs-rich <- rs-rich-plugin-api <- plugins (rs-rich-lumis, rs-rich-mermaid, third-party)
^ ^
+----- rs-rich-ext (hosts plugins) <- rs-rich-cli
The contract:
- a
Plugintrait withmetadata()andregister(&mut PluginContext); PluginMetadata: id, name, version, and the plugin API version it targets;- capability registration for highlighters (regex and
CodeHighlighter), renderables, Markdown fence renderers (workstream 3), themes and box styles; ExtensionRegistry::add_pluginin ext, with duplicate-id and conflicting-capability errors that name both plugins;- a
PLUGIN_API_VERSION. A plugin built for an incompatible API version is refused with a message naming both versions; - explicit registration only, as Plugins already decides.
No
inventory/linkmeauto-discovery, no dynamic or WASM loading, and no marketplace; those are #232's later phases.
Acceptance:
- The shipped highlighters and the adapters from workstream 1 register through
Plugin with no private hooks.
- A test crate outside the workspace's own crates registers a plugin using
only public items, and depends on rs-rich-plugin-api without pulling in
ext.
- cargo tree for rs-rich-plugin-api shows only rs-rich and its
dependencies.
- Registration order is deterministic, and rich doctor lists the registered
plugins.
3. Markdown fence extensions and Mermaid (#222) — core seam, then new crate¶
- Core: add a
FenceRendererextension point inprotocol.rs, so Markdown can ask "does anyone render```lang?" before falling back toSyntax. With nothing registered, Markdown is byte-identical to today: the goldens and Markdown fuzz corpus must not move. - New crate
rs-rich-mermaid, built on the plugin API from workstream 2. It has two renderers, and the first one that works wins: - The
mmdcbackend (Mermaid's own CLI: Node plus headless Chromium), behind an off-by-defaultmmdcfeature. It renders every Mermaid diagram type with Mermaid's own layout, to PNG, shown throughrich-art: Sixel or Kitty where the terminal supports it, otherwise quadrant blocks. - Native text: flowcharts (
graph/flowchartwithTD/TB/LR/RL, the common node shapes, and edges with labels), drawn with box-drawing characters and an ASCII fallback. It is used whenmmdcis not installed, not enabled, or fails.
Anything neither renderer can draw, or a parse error, renders the source
as a normal code block, with a one-line note saying why.
- mmdc rules:
- It runs only where the user asked for it: rich mermaid FILE,
--mermaid-backend mmdc, or a mermaid_backend setting in the user's
own config. A working-directory rich.toml cannot turn it on, the same
trust rule as theme_file and export_*, because it starts a browser.
- Diagram source goes through a temporary file, never the shell.
- There is a timeout (default 20 s) and an input size cap.
- Nothing goes to the network.
- Readability: diagrams drawn as images need a graphics protocol to keep
text legible. With only block characters available, flowcharts prefer the
native text renderer. Other diagram types show the image and a note.
- CLI: rich mermaid FILE and fenced ```mermaid blocks in
rich --markdown, behind a mermaid feature. The mmdc backend sits
behind its own mmdc feature.
Acceptance:
- The plugin works through the public API alone.
- Native text renderer:
- snapshot tests cover each shape and direction, edge labels, a cycle, a
disconnected graph and a large graph that must be cropped;
- a fuzz test checks the parser never panics.
- mmdc backend:
- an integration test in CI installs @mermaid-js/mermaid-cli and renders
a flowchart, a sequence, a class and a state diagram;
- fallback tests cover mmdc missing, timing out, and exiting with an
error;
- a config-trust test checks a project rich.toml cannot enable it.
- Default Markdown output is unchanged.
4. Composable transforms (#216) — ext, then CLI¶
A Transform trait over the structured-data Node and over Text, with the
existing operations re-expressed as transforms:
--select,--redact, sort/group and the diff filters;- new:
filter(keep matching nodes or lines) andhighlight(apply a highlighter to selected paths).
The CLI builds one pipeline from its flags in a documented, fixed order.
Acceptance:
- Output of every existing CLI flag combination is unchanged: the snapshot
suite and the inspect tests pass.
- The pipeline order is written down and tested.
- A plugin can contribute a transform.
As delivered: the plugin-facing kind is a text transform, because the plugin
API depends only on core and the data Node lives in ext. Data, table and
patch transforms are ext types that host code composes. The CLI has no sort,
group or patch-filter flags yet, so those transforms are library-only for now.
The CLI flags are --filter and --highlight, and they work on text,
--print, --syntax and --inspect.
5. Render tree spike (#226) — design only¶
An architecture spike, not an API change: a design note
(docs/design/render-tree.md) plus a prototype on a branch. It should cover:
- what an intermediate frame (cells with styles, links, and semantic roles) would give export, snapshots, accessibility and a future TUI;
- the memory and speed cost, measured on the benchmark cases;
- how existing renderables would migrate;
- a recommendation, with a proposed milestone if the answer is yes.
Acceptance: the maintainer reviews the note. Nothing ships in the crates.
As delivered: the note is docs/design/render-tree.md.
The plan's "prototype on a branch" is a standalone crate in
docs/design/render-tree/prototype, outside the workspace, so that it can be
reviewed next to the note. It recommends styled runs in rich-ext, proposed
as 0.0.13 "Frames".
6. Art: native sizing (#519, #144) — art, then CLI¶
- Art: a native size mode that renders at the image's own pixel size mapped per backend, never enlarges, and is capped by the console width and the max-size options.
- CLI:
--image-fit native. -
144 stays open for animation, Kitty and FIGlet.¶
Acceptance: as listed in #519.
As delivered: ImageFit::Native (the variant, as the plan's CLI flag
suggests) and --image-fit native. Each mode's density is the one fitting
already used (cell_pixels): quadrants take 2×4 pixels a cell, not 2×2, so
the aspect ratio holds on a cell twice as tall as it is wide.
7. Python bindings, first slice (#197) — new crate and PyPI package¶
A thin Python layer over the Rust crates, so a Rich user can move a small program by changing imports:
- Packaging: PyO3 and maturin, in
crates/rich-py. The crate is not published to crates.io; it ships as a wheel on PyPI. - All rendering stays in Rust. Python holds only API adaptation, object conversion and packaging glue. Any real logic found in Python moves into Rust first.
- First slice:
Console(print,rule,export_text),Text,Styleand markup,TableandPanel, following #197's vertical-slice order. Nothing else is exposed until it is tested. - Wheels for CPython 3.9–3.13 on Linux (x86-64, arm64), macOS (arm64, x86-64) and Windows (x86-64), built in CI.
- PyPI publishing through PyPI's own Trusted Publishing, from its own
python-v0.0.1tag and workflow. Ars-rich-v…tag would be read as a core release, so the Python package never uses one. - The pending publisher on PyPI (added 2026-09-24) expects exactly this
repository, the workflow file
.github/workflows/pypi-release.ymland the GitHub environmentpypi. The workflow must keep that file name, and its publish job must run in that environment withid-token: write, or the upload is refused. - Create the
pypienvironment under the repository's Settings → Environments. Protect it likecrates-io: tags only, with a required reviewer if wanted.
Acceptance:
- A small Rich example runs with only its imports changed.
- A compatibility test prints the same program under Python rich 15.0.0
and under the wrapper, and compares the bytes, like scripts/diff_rich.py.
- The wheels install and import on every target in CI.
- The package and import names are decided (see
Decisions).
As delivered: crates/rich-py is outside the Cargo workspace, because
PyO3's build needs a Python interpreter, which the workspace's jobs and the
MSRV check should not. It has its own CI (python.yml) and release
(pypi-release.yml). The slice is Console (print, rule,
export_text, file=, record=), Text, Style, markup, Table,
Panel and box, byte-identical to rich 15.0.0 in tests/test_compat.py.
The issue's wrapper-overhead benchmark is left for the next slice.
8. Python bindings, full parity (#197) — crates/rich-py¶
Decided 2026-09-25: 0.0.12 waits until the Python package exposes everything the Rust crates do, art included. The first slice (workstream 7) is the base.
- Foundation first. Split
crates/rich-py/src/lib.rsinto modules and add one conversion from any Python object to a Rust renderable:str, the wrapped classes, and Rich's protocol (__rich__,__rich_console__,__rich_measure__), so user classes render inside tables, panels and columns. CompleteConsole.print(style,markup,highlight,overflow,end,justifyfor any renderable),log,input, andexport_html/export_svg. - Then, in parallel:
- Text, Style, Color, markup, emoji, highlighters and themes: the rest of their APIs.
- Static renderables:
Rule,Padding,Align,Columns,Group,Constrain,Tree,Layout,Bar,Spinner,Segment,Styled. - Code and data:
Markdown,Syntax(with the code-highlighter choice),JSON,Pretty,inspect,Traceback. - Live and interactive:
Live,Progress(columns,track,wrap_file,open),Status,Screen,Pager, prompts, and a logging handler. - Ext: under
rs_rich.ext, therich-extrenderables and tools (diagnostics, data inspection, diffs, transforms, workflow renderables, the plugin host). - Art and Mermaid: under
rs_rich.artandrs_rich.mermaid: images in every mode and fit, FIGlet, GIFs, image diff, Mermaid. - Plugins:
rs_rich.plugins, thers-rich-plugin-apicontract from Python: a Python class can be a code highlighter, theme, renderer, fence renderer or transform, and registers with the ext host. - CLI: the
richcommand ships in the wheel, aspython -m rs_richand arich-rsconsole script (the namerichbelongs to rich-cli). - Scope is every crate. The exception is
rs-rich-macros, whose compile-time macros have no Python meaning. - Testing. Anything upstream Rich has is compared byte for byte with
rich 15.0.0, as in
test_compat.py. Features Rich lacks (ext, art, Mermaid) are compared with the Rust crates' own output for the same input. - Packaging.
rs-richon PyPI depends on the Rust crates it wraps by path; the wheels grow accordingly. lumis stays optional (its grammars make the wheel very large), behind a separate build.
Order of work¶
- #522 and #523 first. Every other workstream needs the core seam and the unchanged default. With them, bump the cohort versions (see Package impact); that also fixes the CLI README wording.
- Workstream 2, the
rs-rich-plugin-apicrate, so everything after it registers through it. - In parallel:
- the fence seam, then
rs-rich-mermaid(workstream 3); rs-rich-lumis(#524);- native sizing (workstream 6);
- the Python bindings (workstream 7), which depend only on core.
- #525 and #526, which need both adapters.
- Workstream 4, transforms, which can use plugin-contributed transforms.
- Workstream 5, the spike, at any point; it doesn't block anything.
- Release test, as in 0.0.11.
Decisions (defaults, open to the maintainer)¶
- The plugin API is its own crate,
rs-rich-plugin-api(decided 2026-09-24). It depends only on core, and ext hosts plugins. It stays at 0.0.x, and each breaking change bumpsPLUGIN_API_VERSION. - Mermaid: an
mmdcbackend with native text fallback (decided 2026-09-24). The options considered were: - native flowcharts only;
- native flowcharts plus sequence diagrams;
- the
mmdcbackend with a native fallback (chosen); - a Kroki HTTP backend, rejected because it sends source over the network.
crates.io has no Mermaid parser or renderer, so the flowchart parser is
ours. The native layout starts from ascii-dag (MIT/Apache, text output,
handles cycles) if it fits; otherwise we write a small layered layout.
- The Python wrapper (#197) stays in 0.0.12 (decided 2026-09-24) as
workstream 7, first slice only.
- Package and import names: rs-rich on PyPI, import rs_rich
(decided 2026-09-24). richer is taken on PyPI by an unrelated Rich
add-on, and its module would clash with ours. rs-rich matches the
crates.io names. We don't claim the rich namespace.
- The PyPI name is reserved through a pending publisher (2026-09-24). The
first successful upload from pypi-release.yml creates the project.
- It has its own version (0.0.1) and its own tag.
- The render tree (#226) is a spike, not a feature. Nothing public
changes in 0.0.12.
- Theme names stay per adapter, with ansi_dark/ansi_light in every
adapter (#521).
- New crates start at 0.0.1 with a first manual token upload, like
rs-rich-macros 0.0.1. Add each one's Trusted Publishing entry right after
its first upload.
Package impact¶
| Package | Proposed | Reason |
|---|---|---|
rs-rich |
0.0.8 | CodeHighlighter and FenceRenderer seams, the syntect adapter; default output unchanged |
rs-rich-plugin-api |
0.0.1 (new) | The plugin contract |
rs-rich-macros |
0.0.2 | Exact core pin |
rs-rich-ext |
0.0.10 | Hosts plugins, transforms, conformance kit |
rs-rich-art |
0.0.10 | Core pin, native sizing |
rs-rich-cli |
0.0.12 | Pins; --highlighter, rich mermaid, --image-fit native; README wording |
rs-rich-lumis |
0.0.1 (new) | lumis adapter |
rs-rich-mermaid |
0.0.1 (new) | Mermaid as a plugin |
rs-rich (PyPI) |
0.0.1 (new) | Python bindings, first slice (import rs_rich); crates/rich-py is not on crates.io |
Publication order, one tag at a time:
1. core;
2. rs-rich-plugin-api, a first manual upload;
3. macros;
4. ext and art;
5. rs-rich-lumis and rs-rich-mermaid, each a first manual upload;
6. CLI;
7. rs-rich on PyPI, from its own python-v… tag after core is on crates.io.
The CLI depends on the lumis crate through its off-by-default lumis feature,
and on the Mermaid crate through its mermaid feature, which is on by default
(decided during workstream 3: it adds no heavy dependencies; the browser-based
mmdc backend stays off by default). It still needs both on crates.io before
its own upload.
Delivered¶
| Workstream | Pull request | Merged as |
|---|---|---|
| 1. Code highlighters: trait, syntect adapter and ANSI themes, cohort versions (#522, #523) | #531 | 7131fba |
2. Public plugin API: rs-rich-plugin-api, ext host, doctor |
#532 | f04db98 |
| 3. Markdown fence seam and Mermaid (#222) | #533 | 035f898 |
1. Code highlighters: rs-rich-lumis (#524) |
#534 | 8212d79 |
1. Code highlighters: console default, --highlighter, --code-theme (#525) |
#535 | 97e3884 |
| 1. Code highlighters: conformance kit, guide, benchmarks (#526) | #536 | 22d0e05 |
| 4. Composable transforms (#216) | #538 | 80019c9 |
| 5. Render tree spike (#226), design only | #539 | 22852a6 |
| 6. Native image sizing (#519) | #540 | 874b03c |
| 7. Python bindings, first slice (#197) | #541 | 36be88c |
| 8. Python bindings: full parity, core gaps, second audit, release test | #543 | 69dc7fe |
Review focus¶
- Core stays upstream by default. With no plugin registered, every golden, the differential corpus and the Markdown fuzz corpus are byte-identical.
- No new dependency reaches a default build of
rs-rich. Checkcargo tree -e normal. tree-sitter and Mermaid code live only in their own crates. - First-party plugins use only public items. A test crate proves a third party can do the same.
- Adapter and plugin output is validated. Nothing a plugin returns can crash rendering or write terminal control sequences.