Skip to content

0.0.10 plan — progress you can ship with

Status: approved by the maintainer on 2026-09-22, after the 0.0.9 cohort was published. Every workstream merged to main on 2026-09-23 (see Delivered), and the release test passed on the integrated tree; see the 0.0.10 release notes. The cohort was published on 2026-09-23 (tracker #198).

Goal: Close the most visible core parity gap for library users, which is Progress without time, rate or spinner columns. Add upstream's theme stack, widen the parity oracle around both, and finish the four small leftovers from 0.0.9. Also added at the maintainer's request (2026-09-22): multi-file debounced watch (#139), image colour modes and adjustments (#125, #126, quadrants from #124) and a Markdown strikethrough parity fix (#9).

Why this theme: 0.0.7–0.0.9 were CLI- and art-heavy while the oldest core trackers (#6 since August, #3 since August) aged. Both have a byte-parity oracle in real rich 15.0.0, so correctness is checkable rather than argued. They also unblock later work: Live-backed watch mode (#139) and a truthful compatibility surface for the Python wrapper (#197).

Scope

1. Progress time and rate columns (#6, core)

Faithful port of the deterministic parts of upstream progress.py:

  • An injectable clock mirroring upstream get_time. Tests and goldens use a fixed clock; the default uses a monotonic clock.
  • Task gains start and stop times, finished_time and a speed sample window (upstream ProgressSample, speed_estimate_period = 30.0), with elapsed, speed and time_remaining derived exactly as upstream computes them.
  • New columns: TimeElapsedColumn, TimeRemainingColumn (including compact and elapsed_when_finished), TransferSpeedColumn, SpinnerColumn, RenderableColumn, FileSizeColumn, TotalFileSizeColumn and TaskProgressColumn.
  • A task-driving API: task ids plus update, advance, start_task, stop_task, reset and remove_task.

Acceptance: - New golden cases in scripts/capture_golden.py for every new column, using a fixed clock and sample history, match rich 15.0.0 byte for byte. - Existing progress goldens remain unchanged. - DIVERGENCES.md §16 is updated to the remaining gap. PORTING.md rows and the stale spinner.rs module docs are corrected.

Out of scope: auto-refresh threads, track() over iterators, Live transient/alt-screen/redirect (§17) and the pulse bar. #6 stays open for these.

2. Theme stack (#3, core)

  • Console::push_theme(theme, inherit) / pop_theme() over a stack whose base cannot be popped (upstream ThemeStackError), plus a closure-scoped use_theme in place of the Python context manager.
  • Theme::new(styles, inherit) and Theme::read / from_file for upstream's [styles] config format.

Acceptance: - Goldens render the same markup under a pushed theme, a nested push and after a pop. - Popping the base theme is an error. - DIVERGENCES.md §14 is closed and PORTING.md theme.py is marked done. - A design note records how the stack interacts with Live owning the console. That interaction is the only known ownership constraint.

3. Parity oracle around the new surface (#15, #34)

  • 15: goldens for MofN, Download, custom column sets, spinner and Status frames

    at a fixed time, and LiveRender control sequences. Fill the missing parity cells in PORTING.md.
  • 34: add Table, Rule, Padding and Align generators to scripts/diff_rich.py, plus a

    scheduled (nightly) high-volume run that writes shrunk failures as corpus JSONL. Document the triage steps.

Acceptance: - The pull-request corpus stays fast. The nightly job runs from main only and fails loudly with a reproducible case.

4. 0.0.9 leftovers (ext)

  • 134: a regression or example with a Panel nested inside a LayoutNode.

  • 146 and #151: a real multi-level std::error::Error chain (A→B→C) with distinct,

    ordered "caused by" rows, plus RenderSnapshot fixtures for multiline, chained and source-context diagnostics.
  • 149 (kept in scope, 2026-09-22): Syntax and JSON adopt the shared

    OverflowPolicy through rich-ext wrappers that route their rendered lines through fit_segments. Core Syntax and JSON defaults stay byte-identical.

Acceptance: close #134, #146, #149 and #151 with evidence comments. For #149, tests cover each policy on long Syntax lines and deep or wide JSON, including wide glyphs, and a golden check shows the unwrapped core renderables unchanged.

5. Multi-file watch with debounce (#139, CLI)

  • --watch accepts several local files. A change to any of them re-renders only the changed resource. Each keeps its selected renderer and mode.
  • A debounce window collapses bursts of events (editor save patterns) into one re-render. The window is configurable and a documented default applies.
  • An event-driven backend via the notify crate, with the existing polling loop as the fallback when watching isn't available (network filesystems, unsupported platforms, or when polling is forced by a flag). Idle CPU stays negligible.
  • Interactive repaint goes through the rich-ext LiveCoordinator from 0.0.9, one region per resource, not a separate clear-and-home loop. Redirected output keeps today's single deterministic snapshot.
  • Errors stay visible per resource and recover when the file becomes valid. --watch-exit-on-error (or equivalent) ends the watch when requested.

Acceptance: - PTY tests for burst saves (one render per debounce window), atomic rename-over-save, delete and recreate, and two files changing independently. - A forced-polling test so the fallback stays covered. - No duplicate full renders appended in interactive mode. - Existing single-file watch and URL polling behaviour unchanged.

Out of scope: watching directories or globs recursively. URL watching stays polling-only.

6. Image colour modes and adjustments (#125, #126, quadrants; art + CLI)

  • #125 colour modes: ImageColorMode::Ansi16 and Grayscale (with a documented fixed palette for the themeable 0–15 entries), valid with every existing dither. CLI: --image-color ansi16|grayscale.
  • #126 adjustments:
  • ImageFit::Stretch
  • max_width / max_height caps
  • brightness, contrast and gamma in ImageTransforms, applied in a documented fixed order before quantisation
  • CLI flags for each
  • Quadrant blocks (#124 follow-up): a Quadrants mode rendering 2×2 pixels per cell with Unicode quadrant characters, choosing the best two-colour split per cell. CLI: --image-mode quadrants.
  • Route all of the above through the CLI (#144) with the existing strict rejection of unsupported combinations (for example GIF or diff where not implemented).

Acceptance: - Unit tests for nearest-colour mapping in ANSI16 and grayscale. - Deterministic fixtures for each adjustment, and quadrant cell selection covering odd sizes and transparency. - Every existing image output stays byte-identical when the new options are unset. - The same-source comparison image gains the new modes, rendered from actual output.

7. Markdown ~~~ strikethrough parity (#9, core)

  • Port markdown-it's delimiter-run pairing for strikethrough, so inline runs of three or more tildes match upstream. Verified against rich 15.0.0: a ~~~x~~~ b renders a ~ + struck x + ~ b; a ~~~~x~~~~ b strikes only x; a ~~x~~~ b strikes x and keeps one trailing ~. The port currently prints the first case literally.

Acceptance: - Goldens for inline runs of 1–4 tildes, unbalanced runs and runs mixed with emphasis match rich 15.0.0. A ~~~ at the start of a line stays a code fence. - DIVERGENCES.md §21 is removed. - #9 stays open only for inline styling in table cells.

8. Release hardening

  • Switch release.yml to crates.io Trusted Publishing: the publish job gets id-token: write and uses rust-lang/crates-io-auth-action@v1. Delete CARGO_REGISTRY_TOKEN after the first successful trusted publish. A crates.io publisher entry is required for each of the four crates (owner action).
  • Move to Node 24 action majors (actions/checkout, actions/github-script, pages actions), superseding Dependabot #52, #53 and #54.
  • Take Dependabot #179 (icy_sixel 0.7, art) and #194 (toml 1.x, CLI config). The toml major needs the strict-config regressions, including inactive profiles and themes, to stay green.
  • Add the release readiness lesson to the release skill: a review-body finding is not an inline thread, so check review summaries as well as threads.

Delivered

Workstream Pull request Merged as
8. Release hardening, cohort bump #201 7d6ca99
1. Progress columns and task model (#6) #383, carried by #496 e2939d3
3. Spinner/Status/LiveRender goldens (#15) #496 e2939d3
3. Differential generators and nightly run (#34) #450 7d8dec4
5. Multi-file debounced watch (#139) #438 8189175
6. Image colour modes, quadrants, adjustments (#125, #126, #199) #437 39eecd1
4. 0.0.9 leftovers (#134, #146, #149, #151) #436 fbed74b
7. ~~~ strikethrough parity (#9) #381 2ab4895
2. Theme stack and theme files (#3) #231 bb4a032

496 was stacked on #383 and squash-merged first, so it carried #383's changes; #383

then merged with an empty diff.

Differences from the plan, decided during implementation:

  • RenderableColumn was not ported. Its upstream form takes any renderable per task, which the port's ProgressColumn enum does not yet model; the module docs list it with custom task fields. #6 stays open for it and the Live-driven parts.
  • #15 went further than planned. Writing the Status and LiveRender goldens showed that Spinner always counted from t = 0. It now follows upstream's animation state: the first render starts it, update works mid-animation, and the text is markup. API change: Status::renderable returns &Spinner.
  • #149 was delivered as the maintainer chose: upstream __rich_measure__ for Syntax and Json in core (with Renderable::fit_to_measurement), and an explicit Overflowing wrapper in ext.
  • #134 leaves now fill their region, as upstream Layout does (maintainer choice). This changes how ext 0.0.7 layouts with height-aware leaves render.
  • #34's first generated runs found eight divergence families in core. Per the maintainer's decision they were filed, not fixed: #442 Padding, #443 Align, #444 Rule, #445 Table header, #446 printed-Text joining, #447 tabs with full justify, #448 emoji matching, #449 zero-width Panel. The nightly run reports them until fixed.
  • Watch: one file keeps the clear-and-home repaint (now event-driven); only two or more files use Live regions. Several resources must all be local files.

Package impact

Workstreams 1 and 2 change core, so core goes to 0.0.6. Internal requirements are exact 0.0.x pins, so every dependent must move with it to keep one rs-rich in the graph:

Package Proposed Reason
rs-rich 0.0.6 Progress columns, theme stack, strikethrough parity
rs-rich-ext 0.0.8 Core requirement; Syntax/JSON overflow wrappers (#149), leftover tests
rs-rich-art 0.0.8 Core requirement; ANSI16/grayscale, adjustments, quadrants; icy_sixel 0.7
rs-rich-cli 0.0.10 Core/ext/art requirements; multi-file watch; new image flags; toml 1.x

Publication order is the same as 0.0.9: core, then ext and art, then CLI, each verified from the registry before its dependents are tagged.

Order of work

  1. Release hardening first (8), so the 0.0.10 publish exercises Trusted Publishing.
  2. Theme stack (2) and strikethrough parity (7), both small, independent core changes.
  3. Progress clock and task model, then columns, then goldens (1 with the #15 part of 3).
  4. 34 generators and nightly job (3), in parallel with 1.

  5. Art modes and adjustments (6). Art is independent of core work, then CLI routing.
  6. Multi-file watch (5), CLI only, in parallel with the art work.
  7. Leftover ext work (4), at any point.
  8. Release test as in 0.0.9: validate_release.py, golden recapture, per-tag plans, packaged consumer install and installed-binary screenshots (scripts/terminal_shots).

Deferred, with the reason

  • #197 Python wrapper: its own track. Start with a naming/architecture decision record and a maturin spike outside the release cohort. It benefits from this release's parity work.
  • #10 RichHandler-style console sink: builds on the 0.0.9 adapters. It is the next observability slice.
  • Live-driven Progress (auto-refresh, track(), animated Status): the rest of #6.
  • CLI --theme-file reading upstream theme files: a follow-on to #3.
  • #9 inline styling in table cells: needs table cells to hold styled text.
  • Atkinson dithering, perceptual colour distance, Braille/Sixel/GIF colour modes (#125) and alpha/checkerboard modes (#126): after this slice.
  • #12 Windows legacy console: detection only, if ever. The Win32 renderer conflicts with the unsafe_code = deny lint.
  • intuiTUIve (#160–#171) and plugin layer #14: unchanged.

Review focus

  1. Clock injection must not change default output or make tests time-dependent.
  2. Speed and ETA arithmetic must match upstream exactly at sample-window boundaries.
  3. Theme stack ownership with Live and exported consoles.
  4. Watch: debounce timing must be testable without sleeps racing CI, the polling fallback must stay exercised, and a Live repaint must never interleave with error output.
  5. Image options: new modes and adjustments must reject unsupported combinations instead of silently ignoring them, and leave unset outputs byte-identical.
  6. The exact-pin cohort bump: no duplicate rs-rich versions in cargo tree.
  7. Trusted Publishing: the token exchange runs only for real uploads, and a failed exchange must abort before any crate is uploaded.