Skip to content

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 CodeHighlighter trait and HighlightedCode types, and

    routes Syntax and Markdown code through them.
  • 523 makes syntect the default adapter, byte-identical to 0.0.11, with

    ansi_dark/ansi_light themes.
  • 524 adds the new rs-rich-lumis crate: a tree-sitter adapter, kept out of

    everyone else's build because of the tree-sitter linking conflict.
  • 525 adds a console-wide default, --highlighter, config and doctor

    support.
  • 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 Plugin trait with metadata() and register(&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_plugin in 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/linkme auto-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 FenceRenderer extension point in protocol.rs, so Markdown can ask "does anyone render ```lang?" before falling back to Syntax. 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 mmdc backend (Mermaid's own CLI: Node plus headless Chromium), behind an off-by-default mmdc feature. It renders every Mermaid diagram type with Mermaid's own layout, to PNG, shown through rich-art: Sixel or Kitty where the terminal supports it, otherwise quadrant blocks.
  • Native text: flowcharts (graph/flowchart with TD/TB/LR/RL, the common node shapes, and edges with labels), drawn with box-drawing characters and an ASCII fallback. It is used when mmdc is 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) and highlight (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, Style and markup, Table and Panel, 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.1 tag and workflow. A rs-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.yml and the GitHub environment pypi. The workflow must keep that file name, and its publish job must run in that environment with id-token: write, or the upload is refused.
  • Create the pypi environment under the repository's Settings → Environments. Protect it like crates-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.rs into 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. Complete Console.print (style, markup, highlight, overflow, end, justify for any renderable), log, input, and export_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, the rich-ext renderables and tools (diagnostics, data inspection, diffs, transforms, workflow renderables, the plugin host).
  • Art and Mermaid: under rs_rich.art and rs_rich.mermaid: images in every mode and fit, FIGlet, GIFs, image diff, Mermaid.
  • Plugins: rs_rich.plugins, the rs-rich-plugin-api contract from Python: a Python class can be a code highlighter, theme, renderer, fence renderer or transform, and registers with the ext host.
  • CLI: the rich command ships in the wheel, as python -m rs_rich and a rich-rs console script (the name rich belongs 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-rich on 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

  1. #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.
  2. Workstream 2, the rs-rich-plugin-api crate, so everything after it registers through it.
  3. In parallel:
  4. the fence seam, then rs-rich-mermaid (workstream 3);
  5. rs-rich-lumis (#524);
  6. native sizing (workstream 6);
  7. the Python bindings (workstream 7), which depend only on core.
  8. #525 and #526, which need both adapters.
  9. Workstream 4, transforms, which can use plugin-contributed transforms.
  10. Workstream 5, the spike, at any point; it doesn't block anything.
  11. 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 bumps PLUGIN_API_VERSION.
  • Mermaid: an mmdc backend with native text fallback (decided 2026-09-24). The options considered were:
  • native flowcharts only;
  • native flowcharts plus sequence diagrams;
  • the mmdc backend 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

  1. Core stays upstream by default. With no plugin registered, every golden, the differential corpus and the Markdown fuzz corpus are byte-identical.
  2. No new dependency reaches a default build of rs-rich. Check cargo tree -e normal. tree-sitter and Mermaid code live only in their own crates.
  3. First-party plugins use only public items. A test crate proves a third party can do the same.
  4. Adapter and plugin output is validated. Nothing a plugin returns can crash rendering or write terminal control sequences.