0.0.18 plan: intuiTUIve grows up¶
Status: proposed on 2026-10-08. The 0.0.17 cohort (rs-rich-intuituive
0.0.2, rs-rich-interact 0.0.5 and six others) was validated on e9003a2
and waits for the maintainer to tag it. The scope and the decisions
marked "decided" were agreed with the maintainer on 2026-10-07 and
2026-10-08. The scope tracker is #682.
Goal: intuiTUIve becomes something people outside this repository can build on, style, ship to a browser, use from Python, and use with a screen reader.
- The rough spots and widget gaps that a gap assessment on 2026-10-07 found: starting focus, first-layout resize, horizontal scrolling, sortable tables with a cell cursor, a virtual tree, tooltips, drag-and-drop, hover styles, the kitty keyboard protocol, and recordings that use the mouse.
- A stylesheet with selectors and states that sets layout as well as colour.
- More places to run: termion and termwiz backends, and serving to a browser, first intuiTUIve apps and then any terminal program.
- Accessibility that helps a screen-reader user in a terminal today.
- Other programs and web pages inside an app: a terminal pane that runs any program, and a web view, each over a backend that can be swapped like the terminal backends.
- A full Python binding of the framework.
What stays the same: core. Nothing under crates/rich/src changes, and
every golden and the differential corpus stay byte-identical. The
intuiTUIve-against-ratatui benchmark gate keeps passing.
Research behind this plan¶
- The gap assessment (2026-10-07) listed what exists and what is
missing. It found four rough spots met while writing the examples
(
autofocus,Resizeon first layout,help/command_palettenaming, tapes without mouse or F-keys) and these widget gaps: vertical scrolling only, tables without sorting or a cell cursor, trees that are rebuilt whole on every draw, no tooltips, no drag-and-drop, and onlyfocus_styleto restyle a node. - A sharp-edge probe (2026-10-07) found eleven defects, all fixed in #681. Its "behaved well" list sets the bar for the new work: tiny sizes, wide text, broken markup and odd data must not panic.
- Terminal input: rs-rich-interact reads crossterm in legacy mode, so Ctrl+I arrives as Tab and key releases are lost. #681 made key names match what legacy terminals send; the kitty protocol has to keep that working.
- Backends: crossterm is used in three files of rs-rich-interact
(
session.rs,event.rs,policy.rs); everything above talks to theBackendtrait (#677, #678). - The widgets design note (
docs/design/intuituive-widgets.md) has first designs for accessibility and serving to a browser. This plan changes the accessibility one (see Decisions). - Layout today is one
Sizeper node along its parent's axis (Fixed,Percent,Flex,Auto), withmin/max, padding, gaps and grids. The stylesheet sets those same things; it adds no new layout model.
Workstreams¶
Each workstream lands as its own pull request, or a short stack, with an
entry under Delivered. Placement follows AGENTS.md: the
framework in rs-rich-intuituive, terminal input and backends in
rs-rich-interact, recordings in rs-rich-record, browser serving in a new
crate, Python in crates/rich-py.
1. Rough spots: intuituive, record¶
.autofocus()on a node: it takes the focus when its screen opens, instead of the first focusable node.Resizeon first layout: a widget is told its size the first time it is laid out, not only when the size changes.- One name for each overlay:
Ctx::help()andCtx::palette();command_palette()stays as a deprecated alias for one release. - Tapes drive the mouse and every key:
Click,RightClick,DoubleClick,Drag,ScrollUp/ScrollDownandMouseMoveat a cell, sent as SGR mouse sequences, and F1–F24 (every function key the key parser accepts), Alt and Shift key names. Thefilesandplannertapes then record their menus.
2. The kitty keyboard protocol: interact¶
- Turned on when the terminal answers the query, off with
SessionOptions::legacy_keys, and popped on exit, suspend and panic. - Key names mean the key.
"ctrl+i"parses as Ctrl+I, not Tab. Matching treats legacy terminals' ambiguous bytes as either key: a Tab from a legacy terminal fires atabor actrl+ibinding, a kitty Tab onlytab, a kitty Ctrl+I onlyctrl+i. The same for Ctrl+M/Enter, Ctrl+[/Esc and Ctrl+H/Backspace. #681's tests keep passing. - Releases and repeats reach widgets as events
(
WidgetEvent::KeyUp), and bindings stay press-only.
3. Widget gaps: intuituive¶
- Horizontal scrolling:
scroll_xandscroll_both, Shift+wheel and the arrow keys, and a horizontal bar; tables and code panes use it for wide content. - Tables: a
sort: Signal<Option<(usize, Order)>>that header clicks and a key set, drawn as an arrow; the rows closure reads it and the app sorts..sortable()sorts plain string rows itself. Columns resize by dragging their header edge. A cell cursor (cell: Signal<(row, col)>) with ←/→ and its own focus style. - A virtual tree:
tree_lazy(roots, children), wherechildren(path)is called when a node opens and may be a task, so a file tree reads a directory as you open it. Only the rows in view are built.treestays as it is. - Tooltips:
.tooltip(markup)shows after a short hover, near the pointer, and on focus with a key. - Drag-and-drop:
.draggable(value)and.on_drop(|value, cx| ..), with a type check on the value, a drop-target highlight and Esc to cancel. - Hover styles come from the stylesheet's
:hover, with.hover_style()as the builder twin of.focus_style().
4. The stylesheet: intuituive¶
- Syntax (decided): a CSS subset, in a
.tcss-like file or a string.
table { color: $text; }
#preview { size: 3fr; padding: 0 1; border: round $accent; }
.danger:focus { background: red; text-style: bold; }
panel table:hover { background: $surface-lighten; }
- Selectors: kind (
table,panel,label), name (#preview, from.name()), class (.danger, from a new.class()builder), states (:focus,:focus-within,:hover,:selected,:disabled), and the descendant combinator. No sibling selectors, no!important. CSS specificity orders rules. - Properties, styles (decided):
color,background,text-style,border(box and style),border-title, and the theme's variables ($accent). - Properties, layout (decided):
size(3,50%,2fr,auto),min-size,max-size,padding,gap,grid-columns,grid-rows,display: none, anddock: top | bottom | left | rightfor bars that stay at an edge. - Precedence (decided): the theme, then the stylesheet, then the
builders in code.
.fixed(3)in code beatssize: 5in the sheet, as an inline style beats CSS. A node with no size in code takes the sheet's. - Live reload with the theme file's watcher; a rule that does not parse is reported in a toast and the last good sheet stays.
- Cost: rules are matched when a node is built or its state or classes change, and cached; the benchmark gate must not move.
5. Backends: interact¶
- termion (#677, Unix only) and termwiz (#678), each behind an
off-by-default feature of rs-rich-interact, implementing
Backend(size, read, write, elapsed, hand-off, alternate screen, suspend and resume), with their own event conversion and capability probes. App::run_with(backend)picks one; crossterm stays the default.- CI builds and tests each feature on Linux and macOS; the headless backend's conformance tests run against all three.
6. Accessibility: intuituive, ext¶
- A role and an accessible name per node:
Widget::role()with a default, set by every built-in widget (button,list,listitem,table,row,cell,tree,treeitem,tab,tablist,dialog,menu,menuitem,textbox,status), and.label(text)for the accessible name, falling back to the node's text. - Cursor tracking: in accessible mode the terminal's real cursor sits on the focused item (the selected row, the cell, the input's caret), so NVDA, VoiceOver and Orca read what has the focus with no bridge.
- Text mode: no box drawing, no meaning carried by colour alone, selection shown with a marker, using rs-rich-ext's accessibility profile.
- Announcements: toasts, a modal opening and status changes go to an
Announcer, which browser serving reads into an ARIA live region. - On with
App::accessible(true), or theINTUITUIVE_ACCESSIBLEandNO_COLOR-style environment variable.
7. Serving to a browser: new crate rs-rich-web, CLI¶
- Phase 1, intuiTUIve apps (decided first):
rich_web::serve(addr, || my_app())runs oneAppper connection on its own thread, through a WebSocket backend. The page is xterm.js, vendored and pinned, served by the crate itself. - Phase 2, any terminal program (decided):
rich serve -- htopruns the program in a PTY per connection, reusing rs-rich-record's PTY code, and streams it to the same page. As built, it reuses rs-rich-embed'sPtyHostandLocalPty(portable-pty, as rs-rich-record uses), the seam workstream 9 made, so rs-rich-web depends on rs-rich-embed; a page that falls behind holds the program back through a newLocalPty::backpressure. - Security, not optional: binds to localhost unless told otherwise;
a random token in the URL, printed at start; the
Originheader checked (any website can otherwise open a WebSocket to localhost); a cap on sessions; no authentication of its own beyond the token, so exposing it is behind a proxy that has some. - Phase 3, if time allows: a DOM renderer instead of xterm.js, a grid of styled spans carrying workstream 6's roles as ARIA, so a browser's screen reader works too.
8. Python: crates/rich-py¶
- A full binding (decided) as
rs_rich.tui:App,Driver,signal,memo,watch, every node builder and widget, menus, the palette and help, toasts, animations, timers, the stylesheet, browser serving, and headless testing (Script,run). - The same API as Rust, so one guide serves both:
text(lambda: f"{count.get()}").on_key("+", lambda cx: count.update(...)). - Custom widgets by subclassing
Widgetin Python. - Threads and asyncio: the app runs on the Python thread that called
run();spawnruns Python work on a thread, andspawn_asyncawaits a coroutine on an asyncio loop. - Type stubs (
.pyi) for every class, the examples ported to Python, and a Python page in the guide.
9. Embedding: a terminal pane and a web view: new crate rs-rich-embed¶
Both follow the rule the terminal backends do (decided): the widget talks to a trait, and what runs behind it can be swapped for another implementation, including one an app writes itself.
terminal(cmd), a terminal pane: runs a program in a pane of the app: a shell,htop, or a terminal browser such as Carbonyl, Browsh, Chawan or w3m. Keys, the mouse, paste and resizes go to the program; its screen draws in the pane through rs-rich-record'sTerminalemulator (vt100, with rich's character widths). Scrollback, selection and copy as in the rest of the framework; the program's exit is an event.- Behind it,
PtyHost: starts the program and carries its bytes and size.LocalPty(portable-pty, already used by rs-rich-record: a PTY on Unix, ConPTY on Windows) is the first; an SSH host or a test host that replays bytes are the same trait. web_view(url), a web page in a pane, with back, forward, reload, the address and the loading state as signals, keys and the mouse forwarded, and the pane's size given to the page.- Behind it,
WebEngine: opens a page, resizes, takes input, and hands back frames, either cells (text engines) or pixels (graphical ones). The widget draws cells directly and pixels through rs-rich-art's kitty, sixel and iTerm2 protocols, falling back to coloured blocks in a terminal without them. Engines: ProgramEngine: a terminal browser (Carbonyl, Browsh, Chawan, w3m) in a terminal pane. No browser code of ours; the engine named by the app or the user.ChromeEngine(featurechrome): headless Chrome or Chromium, which the user installs, driven over the DevTools protocol: screencast frames, input dispatched to the page. The protocol subset is spoken directly overtungstenite, as in workstream 7, so no async runtime comes with it. Chrome's sandbox stays on, the profile is a temporary directory, and downloads are off.BrowshEngine(featurebrowsh): Browsh's HTTP mode, a page as text, for read-only views.- Not as dependencies: Carbonyl (no release since February 2023; its Chromium has had no security updates since) and Browsh (no release since January 2024) are run as programs the user chooses, never shipped or pinned by us.
10. Accessibility catch-up: intuituive¶
Added on 2026-10-08 (decided), after comparing the plan with other frameworks. Ink, a JavaScript terminal UI library, has the most complete screen-reader support we found; this brings workstream 6 level with it.
- States on the accessibility tree: expanded, checked, selected and busy, set by every built-in widget that has them, and builders for widgets of your own.
- A hidden flag: a node and everything under it left out of the accessibility tree, the linear output and announcements, but still drawn, for decoration.
- A linear mode for screen-reader users: no 2-D screen, the accessibility tree written as plain lines in reading order, then only what changed, the focus and announcements, as a transcript. Opt-in, from code or the environment.
- Less cursor travel in accessible mode: the cursor stays hidden while a frame is written and shows only once it is on the focus, so a screen reader that follows it never reads the regions being repainted.
- A screen-reader test protocol: a guide page with a manual checklist for Orca, NVDA and VoiceOver, and an example that exercises every role, state and mode. Run at the release test; CI cannot run a screen reader.
11. Synchronized output: interact¶
Added on 2026-10-08 (decided). Terminals that support DEC private mode 2026 hold the screen while an update is written and show it in one go, which stops tearing and the cursor jumps of workstream 10.
- Detected at start-up with a DECRQM query, in the same round trip as
the kitty keyboard query, so start-up waits no longer;
RICH_SYNC_OUTPUT=0or1overrides the answer. A session withlegacy_keyssends no kitty query, so it sends no DECRQM query either: synchronized output stays off there unlessRICH_SYNC_OUTPUT=1or the session option turns it on, and start-up stays query-free. - Every frame wrapped when it is on, on every backend, in inline and full-screen mode; a frame with nothing to paint writes nothing, and the restore path on exit, panic or suspend never leaves an update open.
- rs-rich-record's emulator accepts the mode, so tapes keep recording.
Order of work¶
- This plan.
- Workstream 1: small, and its tape changes help record everything after it.
- In parallel: workstream 2 then 5 (both in rs-rich-interact's
session, so one after the other); workstream 3; workstream 7 phase 1
(a new crate, needing only
Backend). - Workstream 4 after 3, since hover styles and tooltips use it, and workstream 6 after 4, since both add to every node.
- Workstream 7 phases 2 and 3, phase 3 after 6; workstream 9 after 3, the terminal pane first (it shares the PTY code with workstream 7 phase 2), then the web view's engines. Workstream 10 after 6, and workstream 11 on top of 5, which reworks the code it changes; the two run in parallel.
- Workstream 8 last, since it binds everything.
- Release test: audits, a real-terminal matrix (kitty, WezTerm, iTerm2, Windows Terminal, xterm, a screen reader on one of them), validation, notes.
Decisions¶
Decided with the maintainer on 2026-10-07 and 2026-10-08 unless marked as a default.
- Scope (decided): the gap assessment's items 2 to 6, the backends, accessibility, browser serving, Python bindings and a stylesheet.
- The stylesheet sets layout too (decided), with code winning over the sheet where both set something.
- Browser serving runs intuiTUIve apps first, then any program over a PTY (decided).
- A full Python binding (decided), mirroring the Rust API.
- A terminal pane and a web view (decided, 2026-10-08), each over a
swappable backend (
PtyHost,WebEngine) as terminals are overBackend. Researched before deciding: Carbonyl, Browsh, Chawan, awrit, casty, terminal-browser and Brow6el all ship as whole programs, not libraries, so the pane runs them and a Chrome engine of our own covers full pages. - A new crate,
rs-rich-embed(rich_embed) (default), for the pane, the web view and their backends, so the framework does not depend on a PTY, a VT emulator or a browser protocol. It depends on rs-rich-intuituive and rs-rich-record (forTerminaland the PTY), and, behind features, rs-rich-art (pixels). - Accessibility is cursor tracking, text mode, roles and names (decided), not the design note's side-channel sink, which nothing reads today; platform bridges (AT-SPI, UI Automation, macOS) wait for a user who needs one.
- A new crate,
rs-rich-web(rich_web) (default), for the server, the page and the PTY mode, so neither the framework nor the CLI's default build gains network code. The CLI'sservecommand is behind an off-by-defaultservefeature. This is a deliberate exception toAGENTS.md's default location, of the same kind as rs-rich-interact and rs-rich-record; the PR that creates it adds it to the dependency graph and the versioning table. - WebSocket and HTTP from
tungsteniteandtiny_http(default): synchronous, small, no async runtime pulled into users' builds. - xterm.js vendored with its licence (default), checked by hash at build time, never fetched from a CDN at run time.
- Workstreams 10 and 11 join this release (decided, 2026-10-08), from the comparison with other frameworks; its four other items wait for 0.0.19 (see Not in this release).
- termwiz's licences are accepted (decided, 2026-10-08): it brings in
terminfo(WTFPL) and two crates under Unicode-DFS-2016, both permissive, and only behind rs-rich-interact's off-by-defaulttermwizfeature. - The legacy Windows console is not special-cased (decided, 2026-10-08): the termwiz backend reads mouse positions as the one-based SGR reports of a VT-mode console; the old console host's zero-based positions are not converted.
- Version numbers (default): the next patch of each crate that changes; rs-rich-web starts at 0.0.1.
- If the release has to shrink (default), drop in this order: the
Browsh engine, the DOM renderer (7.3), drag-and-drop, termwiz, column
resizing, the Chrome engine,
spawn_asyncin Python. The terminal pane andProgramEnginestay.
Not in this release¶
- Platform accessibility bridges (AT-SPI, UI Automation, the macOS API).
- Authentication or TLS in the browser server; put it behind a proxy.
- A visual stylesheet editor or a CSS inspector beyond the existing inspector showing a node's matched rules.
- Sibling selectors,
!important, animations or transitions in the stylesheet. - Anything in core.
- For 0.0.19, from the comparison with other frameworks (decided, 2026-10-08):
- Flex alignment:
alignandjustify, maybewrap, in layout and in the stylesheet, as Ink and iocraft have. - A text-area widget: a multi-line editor with syntax highlighting, as Textual has.
- One process per browser session in rs-rich-web, as textual-serve isolates each session; an SSH front end later.
- A
Surfacedrawing handle in rs-rich-ext: intuiTUIve's screen and damage-based painter moved down into rs-rich-ext, with a smallsurface.draw(|s| …)API, so plain rich users get positioned rendering that repaints only what changed, as ratatui'sFramegives. Not namedFrame, which is already the finished render.
Package impact¶
| Package | Proposed | Reason |
|---|---|---|
rs-rich-intuituive |
0.0.3 | Workstreams 1, 3, 4, 6, 10 |
rs-rich-interact |
0.0.6 | Kitty keyboard protocol, termion and termwiz backends, synchronized output |
rs-rich-record |
0.0.6 | Mouse and more keys in tapes; PTY and emulator shared with rs-rich-web and rs-rich-embed |
rs-rich-web |
0.0.1 (new) | Serving to a browser |
rs-rich-embed |
0.0.1 (new) | The terminal pane, the web view and their backends; LocalPty::backpressure for rs-rich-web |
rs-rich-ext |
next patch, if the text mode needs it | Accessibility profile additions |
rs-rich-cli |
0.0.18 | rich serve behind the serve feature |
rs-rich (PyPI) |
0.0.6 | rs_rich.tui, with the terminal pane and the web view |
rs-rich-ratatui, rs-rich-micro, rs-rich-data |
next patch if a dependency's manifest moves | Manifest only |
rs-rich and the other crates |
unchanged | Nothing they need changes |
Delivered¶
| Workstream | Pull request | Merged as |
|---|---|---|
| Plan | #683 | 8158ab5 |
| 1. Rough spots | #684 | c7f7567 |
| Versions opened | #688 | c7f7567 |
| 2. The kitty keyboard protocol | #685 | cbc0ca8 |
| 3. The widget gaps | #686 | 574a1e3 |
| 4. Stylesheets | #687 | 39cd66f |
| 7. Browser serving, phase 1 | #689 | 59909f3 |
| 6. Accessibility | #690 | a5abce6 |
| 5. termion and termwiz backends | #691 | 0ecebba |
| 9. Embedding | #692 | 3c9bdfe |
| Plan: workstreams 10 and 11 | #693 | 141f2c1 |
| 11. Synchronized output | #694 | 066260f |
| 10. Accessibility states and linear mode | #695 | 5d671ab |
| 10. No synchronized output in linear mode | #696 | 0be02fc |
7. Browser serving, phases 2 and 3, and rich serve |
#697 | a14f2e2 |
| 8. Python bindings | #698 | 2f9f228 (held from the crates' publication) |
| Release test | the release PR | open |
Review focus¶
- Core is untouched, and the benchmark gate still passes.
- Legacy terminals behave as before. #681's key tests pass with the kitty protocol off, and with it on in a terminal that has it.
- The stylesheet cannot break an app. A sheet with a bad rule is reported and not applied (the last good one stays); precedence is the one written here; matching is cached.
- The browser server is safe by default: localhost, a token, the origin check, a session cap, and a test for each.
- The web view keeps Chrome's sandbox on and never fetches or ships a browser; each backend trait has a second implementation (a test host) proving it can be swapped.
- Accessible mode reads well in at least one real screen reader, recorded in the release notes, in both the cursor-tracking and the linear mode, following the checklist from workstream 10.
- Synchronized output is only sent to a terminal that has it, or
when
RICH_SYNC_OUTPUT=1asks for it, and never left open.