CLI 0.0.9 expanded release design¶
Status: architecture reviewed and approved by the user on 2026-09-21. The implementation plans are prepared for review. This document does not claim implementation or publication.
Tracker: #192.
Baseline: main fbb0887d0b41a290dc6b2cce9c3beb02884c42b4, including merged PR #191.
Outcome and scope¶
Make CLI 0.0.9 more useful for batch exports and image manipulation, and give Rust applications a consistent way to render structured events, diagnostics, constrained layouts and coordinated live regions to an explicit destination. Preserve existing CLI defaults and the faithful core's upstream behaviour.
The user approved batch naming/directory preservation, still-image transforms and Bayer dithering, then explicitly added #132, #134, #136, #145 and #146 and asked for related issues. The resulting scope has 18 existing issues:
| Workstream | Issues | Release completion contract |
|---|---|---|
| Render destinations and capabilities | #132, #133 | Explicit terminal, stream, capture, HTML, SVG and custom-writer contexts; injected capabilities; no ambient detection in explicit rendering |
| Constraints and overflow | #134, #149 | Deterministic fixed/min/max/flex/content sizing, alignment and Unicode-aware overflow for nested layouts |
| Structured events | #136, #151 | Typed ordered fields, context, themes, compact/expanded views and attached diagnostics |
| Error diagnostics and adapters | #146, #10 | Error chains, caller-supplied source snippets, notes/help, optional log/tracing adapters and examples |
| Coordinated live regions | #145, #6 | Independent region updates, ordinary-write coordination, resize and cleanup; baseline Live prerequisites |
| Reusable render regression support | #150 | Downstream capture/snapshot API with explicit dimensions/capabilities and useful diffs |
| Batch exports | #142, #157 | Directory preservation and filename templates using existing planning, collisions, dry-run and workers |
| Still-image workflows | #125, #126, #144 | Bayer dithering, rotation, flips and grayscale through library, CLI, config and batch workers |
| Raster acceptance evidence | #123, #124 | Individual Braille dots, partial cells, odd heights, transparency and same-source renderer examples |
The selected portions do not silently close larger roadmaps. Full #6 upstream parity, reflection-related #10 work, the entire #125 palette roadmap, all #126 transforms/alpha modes and future #144 protocols remain separately tracked. Closed duplicates #135/#147/#148 stay closed, represented by #150/#132/#133.
Architecture decision¶
Three approaches were considered:
- Extensions plus a shared protocol seam (recommended). Put new library policies and renderables in rich-ext, image processing in rich-art, and the small shared destination contract in core protocol.rs. Preserve legacy paths.
- Put all new behaviour into the core Console/Layout/Live classes. This makes discoverability straightforward but conflicts with AGENTS.md and increases upstream drift; it is not the selected approach.
- Implement these features only in CLI orchestration. This reduces public API work but cannot satisfy downstream-library acceptance in the requested issues.
The first approach follows AGENTS.md. In particular, #146's old request to put the new diagnostic implementation in core conflicts with that contract. This design proposes an additive rich-ext diagnostic renderable composed from public core primitives. Existing core Traceback and LogRender keep their behaviour. Issue closure requires the reviewed placement decision to be recorded there.
The dependency directions remain CLI → ext/art → core. Neither core nor art depends on ext. Detection, terminal writes and rendering are separate operations.
A. Explicit targets, capabilities and snapshots¶
Shared contract¶
Add an object-safe rendering-environment extension trait in core protocol.rs. It exposes dimensions, colour depth, interactivity, Unicode/hyperlink policy and generic graphics support hints. It contains no environment lookup, writer, CLI config, backend implementation or process-global mutable state. Existing Renderable implementors gain no required method and ConsoleOptions retains its public struct shape.
The seam must reach nested rendering, not only a new top-level art method. Add a companion protocol extension trait for attaching/querying an immutable owned environment on a Console, backed by an optional private Console field. The environment is Send + Sync and stored behind Arc so Console retains its current Send property. Attachment is per constructed console, not a thread-local or process-global override; there is no temporary mutation needing unwind cleanup. Its implementation carries data only and does not change legacy core rendering. All nested renderables receive that same Console, while ConsoleOptions carries their allocated local dimensions. Legacy Console construction stores no context. This small protocol implementation/private-field change is part of the core package impact; the proposal is not limited to editing protocol.rs alone.
rich-ext owns a concrete RenderTarget and capability snapshot implementing that trait. Target kinds distinguish Terminal, PlainStream, Capture, Html, Svg and Custom. Writer ownership is separate; rendering can produce segments without performing I/O. Capture has explicit plain/ANSI policy; a custom writer declares capabilities rather than inheriting stdout's capabilities.
Explicit construction supplies every rendering-relevant value, including width, height, colour, no-colour policy, terminal/Unicode policy and theme. Its adapter fully configures ConsoleBuilder so its fallback environment probes are not used. Negative/unrepresentable dimensions are rejected before rendering; zero width or height is a valid empty viewport. The target adapter returns an empty render without invoking children for that viewport, avoiding legacy minimum-one-cell paths. Detection is an optional boundary factory: snapshot the intended stream and selected environment once, then apply explicit overrides. Unknown graphics capability is unsupported.
HTML/SVG targets retain styles through the existing public export path and never contain cursor or raster-protocol bytes. Plain streams suppress terminal control and hyperlinks. Capability provenance distinguishes configured, detected and inferred values; doctor uses that distinction without active probes.
The ext target adapter obtains styled segments using a fully configured Console and explicit ConsoleOptions, then applies destination policy before serialising or writing. Unsupported hyperlinks are removed through Style::update_link(None), preserving other style attributes; capture/plain/export destinations discard terminal-control segments. Use the public HTML/SVG segment exporters on those filtered segments, retaining link metadata only when the destination permits it. ConsoleBuilder has no hyperlink switch today, so configuring it alone is not an implementation of this policy. Use this one normalised segment stream for plain, ANSI, exports and snapshots; no capture callback may bypass the adapter's policy.
Art and legacy compatibility¶
Add an explicit-target entry point in rich-art using the shared trait. Auto selection consumes its supplied capabilities instead of calling the Sixel environment heuristic again. Preserve ImageMode, ImageOptions and the legacy entry points; the current defaults and unsupported-mode errors remain intact. CLI terminal, export and capture routing uses the new entry point deliberately.
ImageArt's existing Renderable implementation also queries the attached protocol environment when present. Its direct and nested paths must use the same resolved capabilities; only the absent-context legacy path may detect the environment. Panel/Layout wrapping must not reintroduce Sixel selection from TERM. Explicit Sixel on capture/HTML/SVG remains unsupported; the infallible Renderable fallback must produce safe text and must not emit protocol bytes. Cover nested ImageArt in both legacy core containers and new ext layouts with contradictory TERM, NO_COLOR and stdout capability inputs, as well as an ANSI target with hyperlinks disabled and zero-sized targets.
This is additive: arbitrary third-party Renderable implementations that probe the process themselves are outside the guarantee. Built-in renderers and the new rich-ext renderables must be target-deterministic.
Snapshots (#150)¶
Provide a rich-ext testing module with fixed-width/height capture of plain text, ANSI and resolved segment/style metadata. A versioned snapshot representation preserves line breaks, Unicode cells, colours and hyperlink data; synthetic identifiers are caller-controlled. Serialisation and comparison are opt-in; downstream users can use any assertion framework. A first-difference/unified diff reports changed content without requiring the CLI or terminal mutation.
Test identical targets under contradictory environment settings; terminal vs pipe vs HTML/SVG; art Auto; Unicode/hyperlinks; nested capture; same-input snapshot stability and meaningful style-only diffs. Existing Python parity goldens remain separate from extension snapshots.
B. Constraints and overflow (#134, #149)¶
Introduce rich-ext layout nodes and constraint policies that compose public Layout, ratio, Measurement and Segment operations. The faithful Layout API stays available. Nodes support fixed and min/max bounds on both axes, positive flex weights, content measurement, horizontal/vertical alignment and nested splits.
Resolution is deterministic. Validate min ≤ max and reject a requested fixed size outside explicit bounds. Compute preferred sizes for fixed/content nodes; flex nodes start at their minimum. If their sum exceeds the available extent while minima fit, reduce preferred sizes proportionally to each node's slack above its minimum. If minima themselves exceed the extent, proportionally reduce minima below their requests. In either reduction case use integer arithmetic with checked intermediates and assign residual cells to eligible nodes in source order. Fixed sizes are requests that can shrink only under viewport pressure, and the result exposes which requests/bounds were relaxed. For example, fixed 8 + fixed 8 in width 10 with zero minima resolves to 5 + 5; minima 8 + 8 in width 10 also resolves to 5 + 5 with the minima-violation condition reported. Two content nodes with preferred width 80 and minimum zero in width 100 resolve to 50 + 50 under the same slack-proportional rule.
If space remains, distribute it by positive flex weight, iteratively respecting maxima, with integer remainder cells in source order. When no child can grow, remaining cells are container padding, positioned by container alignment. Children never exceed the available extent; zero space yields zero allocations without invoking legacy children. Use an ext bounded allocator rather than delegating to core ratio_resolve when it would exceed the available extent or force a one-cell minimum. Preserve core ratio_resolve and Layout behaviour. For two flex nodes capped at 3 and 4 in width 10, the result is 3 + 4 with 3 padding cells, not an allocation beyond either maximum.
Widths are terminal cells, not bytes or scalar counts. Content width uses public Measurement; content height measures rendering at its allocated width. Height measurement may be cached for the render pass, never across changed content.
Wrap, fold, crop and ellipsis share core Unicode/Segment primitives. Text and raw segments follow the same explicit policy where the caller selects one. Existing default CSV ellipsis and legacy renderable choices remain unchanged. Visible overflow is allowed for unbounded output but a bounded layout cell clips at its region edge to protect siblings. Styles and hyperlinks survive wrapping; a wide glyph that cannot fit is handled without half-glyph corruption.
Acceptance examples: sidebar/content, two equal columns, nested panels, narrow and zero-sized targets. Tests cover impossible bounds, flex rounding, intrinsic sizes, tabs, combining characters, wide glyphs and styled raw segments.
C. Events, diagnostics and adapters (#136, #146, #151, #10)¶
Data and rendering¶
rich-ext owns StructuredEvent, Diagnostic and their presentation options. Event fields use typed values (null, boolean, integer, float, string, ordered list and ordered map); preserve insertion order unless the caller supplies a field order. Duplicate keys replace the existing value without moving its position. Unlisted fields follow specified fields in insertion order. Field hiding is explicit.
Optional context includes timestamp supplied by the caller, severity, target/ module, source location, thread/task and correlation identifiers. Capture no clock or environment implicitly. Messages and fields are literal text by default; markup requires an explicit typed opt-in. Severity and field styles resolve through the supplied theme, with documented fallback styles.
Compact and expanded renderables retain typed fields until render time and use the selected overflow policy. Attached diagnostics are rendered as distinct blocks, not embedded preformatted strings. Terminal, HTML and SVG retain the same content ordering and style semantics.
Diagnostics¶
Diagnostic models a primary message, ordered causes/contexts, optional labels, source snippets, metadata, notes and help. Source text is supplied explicitly; rendering does not read source paths. Snippet spans are UTF-8 byte ranges validated at construction, then converted to display-cell positions for underlines. Render line numbers and a bounded context window; narrow targets wrap or crop according to the explicit policy without invalid UTF-8 or misleading offsets.
Provide std::error::Error conversion with a bounded chain walk and repeated-error identity detection. Truncation/cycles are visibly marked. Compact mode shows the primary message and cause summary; expanded mode includes snippets, notes and metadata. Existing core Traceback remains source- and output-compatible.
Integration¶
Optional log and tracing features provide adapters/examples using the shared event model. Neither facade enters the default dependency set. No adapter installs a global logger/subscriber implicitly; callers choose ownership and filtering. Tracing field visitors preserve primitive values where the facade provides them; Debug-only values are clearly represented as strings. Adapter write failures must not recursively log through the same adapter.
CLI structured-log rendering gains an explicit rich presentation option; the existing default output and machine report envelopes remain stable. Demonstrate both library adapters and attached diagnostics in the demo/examples. Tests cover multiline values, ordering/hiding, themed exports, error cycles, invalid source spans and widths 1/2/20/80. Optional dependencies must meet Rust 1.90.
D. Coordinated Live regions (#145 and #6 prerequisites)¶
rich-ext owns one LiveRegions coordinator per output sink. Stable opaque region IDs support insert, update, remove and explicit redraw in insertion order. Nested layouts/renderables are content within a region; they do not start an independent terminal-writing loop. Reuse core Control/LiveRender primitives for cursor operations rather than constructing escape codes in a second encoder.
All mutations and ordinary writes are serialised through the coordinator. Its print/suspend/resume API moves above the live area, emits normal content and redraws active regions. Uncoordinated raw stdout writes cannot be made safe and are documented as outside the API contract. A Console extension facade routes ordinary application prints through that coordinator.
The facade is a distinct scoped handle: it does not transparently intercept inherent Console::print calls. Each writer belongs to one coordinator. The coordinator's final flush/finish reports errors and restores state once; callers must not simultaneously run legacy Live/AutoLive on that writer.
Rendering tracks previous shaped lines. Unchanged regions do not re-render on content-only updates; dirty line ranges are repainted when geometry is stable. Insert/remove/resize invalidates affected geometry and repaints the managed area, never clears the entire terminal as a normal update. Target dimensions are explicitly refreshed; a test can inject resize without process-global signals.
Manage an inline viewport of at most height minus one rows, reserving a final cursor/log insertion row. A height of zero or one shows no dynamic rows and does not hide the cursor. Lay out regions in insertion order within that budget and clip excess rows/cells before computing cursor movements; hidden regions retain content but emit no controls. Nested content never owns a second viewport. Do not accept raster-protocol/control segments as region content in this first slice; return an unsupported-content error before updating terminal state.
Keep the cursor at column zero on the reserved row after every update. Render bounded rows with wrapping disabled only if the coordinator also saves/restores that mode; the initial implementation instead avoids printable output in the terminal's final column, reserving it as an unwritten guard column. All shape and clipping calculations use that reduced drawable width. Explicit finish restores cursor visibility and emits at most one final newline.
Before ordinary writes, clear the managed area and move to its top; render the normal content there, let its linefeeds scroll naturally, then redraw regions and reset the anchor at the current insertion row. Cursor movement never uses unclipped content height. On resize, cap cleanup to the new viewport, invalidate all cached geometry and repaint the managed area; cleanup never erases above the new safe anchor budget. Height shrink to zero invalidates the old shape and suppresses relative cursor movement until a nonempty viewport is re-established. Any transition from active rendering to height 0/1 or drawable width zero restores a previously hidden cursor once and suspends dynamic output. Growing the viewport starts a fresh bounded area before hiding the cursor again. Verify these state transitions as well as sessions that start with an empty viewport. Tests must simulate bottom-row log insertion, content taller than the viewport, one/zero rows, shrink/grow and full-width glyphs; sequence inspection alone is not proof that the visible terminal state is correct.
The coordinator owns cursor visibility and restores it on explicit finish, early return, I/O error and panic unwinding using a guard. Cleanup is best-effort if the writer has failed and is not guaranteed on process abort/SIGKILL. A poisoned or failed session cannot continue emitting inconsistent cursor updates. Explicit methods return I/O errors; Drop never panics. Redirected output produces a final finite snapshot without cursor sequences.
Before integration, validate existing manual/auto Live assumptions from #6; fix necessary upstream-parity defects under the repository porting workflow. This does not commit to unrelated progress-column or full Live parity work.
Tests use a captured/virtual terminal plus real PTY checks: two independent regions, ordinary log output between updates, resize, shorter content clearing, region deletion, repeated stop, failing writer, unwind cleanup and unchanged single-region goldens. No background thread is required by the initial coordinator API; applications can schedule explicit refreshes.
E. Batch exports (#142, #157)¶
Add opt-in directory preservation using an explicit input root, and a filename template with a small fixed token set: stem, input extension, output extension and stable planned index. Paths relative to the input root supply preserved directories; templates name the leaf only. Literal braces have an escape form; unknown tokens, empty output names and embedded path separators are errors.
Use tokens {stem}, {input_ext}, {output_ext} and {index}; extensions have
no leading dot, the index is one-based in the fully expanded stable input order,
and {{/}} escape braces. The template supplies the complete leaf filename,
including any desired dot/extension, with no automatic second extension appended.
For example, {index}-{stem}.{output_ext} produces 1-report.html. Expansion
for each HTML/SVG destination uses its own output_ext. Reject ./.., absolute
or drive-qualified leaves and platform-invalid names during planning.
Require local inputs under the explicit root when preserving directories; URL inputs cannot masquerade as relative filesystem paths. Reject destination escape, input overwrite and filesystem aliases using existing canonicalisation and collision checks. Detect collisions after both directory/template expansion and extension selection, before starting workers. Use existing suffix/skip/error/ overwrite policy and stable resource order. Validate future parent paths before creation and recheck aliases at write time.
Dry-run displays exact planned destinations and never creates directories or files. Execution may create required parents only after validation. Keep bounded worker execution, ordered replay, fail-fast, progress, SIGINT cleanup and a single machine envelope. New options participate in config and worker snapshots.
For the new preservation mode, a missing parent directory is a planned creation, not a dry-run error, provided the nearest existing ancestor is a directory and the prospective path stays inside the destination root. Legacy flat-mode dry-run behaviour remains unchanged. Record planned directories in the preview; create them once before workers start, and leave them on cancellation just as completed exports are retained. A recheck reduces alias races but is not a security guarantee against another process swapping symlinks during a write.
Tests cover equal basenames in different directories, mixed extensions, invalid tokens, escaping paths, aliases/symlinks, platform case handling, collisions, parallel errors and cancellation. Flat naming remains the default.
F. Image additions (#125, #126, #144; evidence #123/#124)¶
Public art builders add rotation by 0/90/180/270 degrees clockwise, horizontal and vertical flip, grayscale, and ordered 4x4 Bayer dithering. Preserve the ImageOptions struct shape. Explicit flags and config map to those builders.
Order: decode → rotate → horizontal flip → vertical flip → alpha/background composite → optional grayscale → fit/crop → final sampling → palette/dither → glyph selection. Grayscale applies to the composited image including the chosen background so the requested result is actually grayscale. No transform mutates the original shared decoded image or performs a second decode in the CLI.
The transformed background is also the fill used for contain padding, which is created after grayscale conversion. Test a coloured explicit background with transparent pixels and contain borders; both must be gray before quantisation. Geometry-only rotation/flips preserve RGBA when no fitting/background/grayscale processing was requested, so Braille transparency does not change accidentally.
Grayscale uses (77*R + 150*G + 29*B + 128) >> 8 on encoded RGB, retaining the
existing alpha-compositing rounding. Bayer uses the 4x4 matrix with rows
[0,8,2,10], [12,4,14,6], [3,11,1,9], [15,7,13,5], anchored at final
sampled origin (0,0). Add 2*matrix[y%4][x%4]-15 to each RGB channel, clamp to
0–255, then use the existing nearest ANSI256 mapping and lowest-index tie rule.
This bounded encoded-RGB policy is deterministic and uses no diffusion state.
Tests and docs pin these constants; no perceptual-quality or speed claim is
made without evidence.
Dithering remains opt-in and requires ANSI256 ASCII/half-block still rendering. Geometry/grayscale operate in the shared still-image preparation path; unsupported GIF/diff combinations fail explicitly rather than silently ignoring options. Unselected options preserve existing output byte-for-byte.
Complete #123/#124 evidence with all eight Braille dots, partial 2x4 cells, odd block heights, transparent edges and same-source ASCII/Braille/half-block renders. Record quadrant rendering as an optional evaluated follow-up rather than claim it exists. Demonstrate none/Floyd–Steinberg/Bayer side by side using actual output.
Package impact and sequencing¶
This design adds a shared protocol seam, so it proposes core 0.0.5 and ext 0.0.7, alongside the already prepared art 0.0.7 and CLI 0.0.9. These are proposed future manifest versions, not current published versions. Current manifests are untouched by this design-only change. A core bump is justified only by the actual shared extension API change, not by placing new feature implementations in core.
Update direct internal dependency requirements and lockfile when implementation lands. Publication order becomes core 0.0.5 first, ext 0.0.7 and art 0.0.7 after their exact registry dependencies verify, then CLI 0.0.9. No existing tag moves, coordinated workspace tag or registry upload is part of design preparation.
Implementation is split into reviewable workstreams A–F above, with A preceding B/C/D and the shared part of image integration. E and standalone image transforms can be developed independently. Do not claim completion for an issue merely because its foundation or one example landed.
Release verification and documentation¶
Use failing acceptance regressions before implementation and test actual API contracts, not mirrors of implementation details. Required gates are formatting, Clippy, default/all-feature/lean workspace tests, Rust 1.90, fresh upstream parity, target/snapshot/PTY regressions, package verification and exact registry consumer checks at publication time. Record issue-to-test evidence and review findings.
Update CLI help/config documentation, public Rust examples, docs/PORTING.md, docs/PLUGINS.md, the release plan/notes and the site. Re-record the expanded demo from an optimized build, preserving raw recordings and provenance. Include real terminal and HTML/SVG screenshots of targets, layouts, events/diagnostics and Live regions, plus image comparisons and batch destination previews.
Only close an issue after its acceptance checklist is verified. Keep residual scope visible on broad issues, and update overlap trackers #149/#151 consistently. The prior PR #191 evidence remains baseline evidence, not proof of this expansion.
Review handoff¶
An independent architectural review on 2026-09-21 identified four substantive gaps; all were corrected and re-reviewed. See the review record. The revised design is technically ready for implementation planning; this is not user approval or evidence that the features have been implemented.
Review the shared protocol seam, rich-ext placement, Live write-ownership contract and package impact before writing the implementation plan. The Brainstorming workflow requires review of this architectural design, then review of the written implementation plan, before product changes. Scope selection is already approved; this review is about the newly expanded architecture, not reauthorising those issue selections.