Skip to content

Spec: sparkles:tui — a full-screen interactive TUI library

Status: living inventory · Date: 2026-07-12 · Scope: a new sparkles:tui sub-package (libs/tui/) layered on sparkles:core-cli and sparkles:base.

This is the single source of truth for the interactive-TUI feature set: what the existing sparkles stack already provides, what a full-screen interactive library additionally needs, per-item status, and the design questions still open. It is the forward-looking companion to the shipped core-cli TUI component suite, whose §F deliberately scoped out exactly this layer — "full-screen TUI loop (alt screen, general Event/Backend framework, an app-owned event loop)" and "a cell-grid diff compositor." Those deferrals are now the subject of this spec.

The motivating consumer is a full-screen, interactive terminal application — a live multi-pane operations dashboard: a header with status pills, a scrollable streaming log pane, a data table with a moving selection, an expand/collapse tree, animated spinners and per-item progress, mouse interaction, and live resize handling.

The evidence base is the in-repo TUI-libraries survey — the comparison synthesis (written explicitly as a design brief for a D TUI library), the tree-view and table-span case studies, and the per-library deep dives cited inline below. Every feature and design option in this spec is grounded in a surveyed library or in existing sparkles code; no external application is treated as the specification.

IMPORTANT

The core rendering architecture was decided by measurement, not taste. The two candidates (line-diff vs 2-D cell-grid — §3.1) were implemented as D PoCs and benchmarked under sparkles:test-runner --bench --perf; the render-cost benchmark chose the 2-D cell-grid with a compact packed cell (see the baseline). The rest of this spec inventories the requirements that are invariant to that choice.

Decision ledger

AreaDecision
PackageNew sparkles:tui (libs/tui/), depending on core-cli + base. Keeps core-cli's pure static-producer character intact and its dependency graph loop-free
Rendering coreDecided: 2-D cell-grid with a compact packed cell — the render benchmark (baseline) shows cell-grid best on CPU + bytes, and a packed-cell D renderer reaches C parity on the common workload (so the architecture is fast enough in D). Framework calibration (M3) is optional context
Loop ownershipOpen — a Ratatui-style library core (app owns the loop) with an optional MVU overlay, vs a Bubble-Tea-style framework-owned loop (§3.2)
Terminal controlReuse sparkles.base.term_control (hardcoded sequences, no terminfo — the survey's consensus); grow it with the alt-screen + mouse-mode lifecycle it lacks
ColorExtend beyond today's 16-color Style to truecolor + 256 + degradation (§2, Color row) — a prerequisite for every surveyed mid-level library's styling
Text substrateReuse sparkles.base.text (grapheme/width/wrap/align) unchanged — it is the single source of truth for cell widths and is stronger than most surveyed libraries'
ImagesIn scope as a requirement, not a non-goal (§2, Graphics row) — grounded in Notcurses and libvaxis

1. Substrate that already exists

The interactive layer does not start from zero. The following are shipped, tested, and reused as-is (see the component suite spec for detail):

LayerModule(s)What it gives the TUI library
Grapheme/width/wrap/alignbase.text.{grapheme,width,wrap,ansi}Kitty-TSP cell widths, style-safe wrapping, Align/alignField/truncateField, SGR tokenization — the cell-width authority every renderer needs
Control sequencesbase.term_controlCtlSeq (erase/cursor/alt-screen/sync-output), writeCursor*, DecMode set/reset — hardcoded, no terminfo
SGR stylingbase.term_style, theme (core-cli.ui.theme)Style (16-color SGR), border presets, StatusGlyphs, Semantic, makeTheme(TermCaps)
Capability detectioncore-cli.term_capsterminalSize(), isTerminal, TermCaps, detectTermCaps, SIGWINCH handler
Static producerscore-cli.ui.{box,table,tree,meter,header,tasklist,osc_link,layout}Span-capable table, tree, meter/bar, boxes, OSC-8 links, hjoin/kvList — candidate widget bodies / renderers
In-place repaintcore-cli.ui.live (LiveRegion)Log-update repaint (cursor-up + erase, DEC-2026 framing) — a full-repaint baseline and the current status quo the new renderer replaces
Minimal raw inputcore-cli.key_inputcbreak-mode enter/restore + a 4-key (up/down/enter/cancel) decoder — the seed the full input parser extends

The survey's own assessment: this substrate (grapheme-correct widths, style-safe wrapping, SGR-state tracking) is stronger than what most surveyed libraries sit on, so the gaps below are in the interactive/runtime layer, not the text engine.

2. The delta — features a full interactive TUI needs

Status legend: landed · partial (exists but incomplete or in the wrong layer) · open (net-new) · deferred.

#AreaFeatureStatusGrounding (surveyed libraries)
R1Render coreFrame buffer + diff — architecture decided: 2-D cell-grid with a compact packed cell (§3.1, baseline); build is openlandedRatatui/libvaxis/FTXUI/Notcurses (cell); Bubble Tea (line, rejected)
R2Render coreDouble-buffering + synchronized-output (DEC 2026) frame framing; minimal-write emissionlandedMosaic, libvaxis, Notcurses (CtlSeq.sync* exists in base)
B1BackendTerminal-lifecycle owner: raw mode, alt-screen enter/exit, mouse-mode enable/disable, cursor hide/show, panic/scope restore guardlandedlibvaxis (panic handler), Ratatui Backend trait, Notcurses
B2BackendSwappable backend seam incl. an in-memory/test backend for deterministic rendering testsopenRatatui (TestBackend), Cursive (DummyBackend)
I1InputFull key decoder: arrows/home/end/pgup·pgdn/insert/delete/F1–F12 + ctrl/alt/shift modifiers, into a structured Event sum typelandedlibvaxis, Bubble Tea (key_input decodes 4 keys today)
I2InputMouse events (X10 + SGR-1006), wheel, drag; positional hit-testing "zones"partialBubble Tea (bubblezone), libvaxis, tview
I3InputResize-as-event; bracketed paste; input read decoupled from the render looppartiallibvaxis, Textual (SIGWINCH handler exists in term_caps)
E1RuntimeEvent loop + command/async-effect model; optional MVU overlay (Model/update/view + Cmd/Msg) or app-owned loop (§3.2)partialBubble Tea (MVU), Ratatui (app owns loop), Brick
E2RuntimeFrame scheduler / tick source for animation (spinner/gradient/progress cadence), coalesced to a max frame rateopenBubble Tea (Tick), Mosaic, Textual
C1ColorTruecolor (24-bit) + 256-color + adaptive degradation to the terminal's real depthlandedlibvaxis, Notcurses (RGBA channels), Lip Gloss (via Bubble Tea)
C2ColorColor-depth probing in TermCaps (today colors is a bool); gradient/blend helperspartialNotcurses, libvaxis
S1StyleStructured cell-style value (fg/bg/modifiers) for the cell path, plus copy-on-write block styling (padding/margin/border/align/width)partialLip Gloss (via Bubble Tea), FTXUI decorators, Brick AttrMap (term_style/theme exist)
L1Layoutvjoin + place (positional alignment) — hjoin landedpartialLip Gloss Join*/Place, FTXUI (hjoin landed in ui.layout)
L2LayoutA layout engine: constraint splits and/or flexbox and/or combinators (hBox/vBox/hLimit/pad/center)openRatatui (constraints/Cassowary), FTXUI/Ink (flexbox), Brick (combinators)
W1Widget modelisWidget/isStatefulWidget DbI render contract; a focus model (tab order, focused-path event routing)openRatatui ((Stateful)Widget), libvaxis vxfw, Cursive/tview
W2WidgetsScrollable viewport + scrollbar (page/half-page/goto, wheel)openRatatui, Bubbles (via Bubble Tea), Textual
W3WidgetsInteractive (selectable/navigable) table + tree — static renderers exist in ui.table/ui.treepartialRatatui (Table/List + State), tview; tree-view case study
W4WidgetsStateful spinner + spinner catalog; toast/notification (timed, fading); key-map help barpartialBubbles/Bubble Tea, Textual (spinnerFrame/ProgressLine exist as pure producers)
W5WidgetsSingle-line input + multi-line text area; tabs; dialog/modal stackopenCursive, tview, Textual, Bubbles
G1GraphicsInline images — Kitty graphics / Sixel / iTerm protocols, cell-anchored, placement- and scroll-aware, capability-gated with a text fallbackopenNotcurses (Sixel/Kitty/iTerm pixel graphics + video), libvaxis (per-cell image)

The two hardest widgets already have accepted blueprints in the survey and should follow them rather than be redesigned:

  • Tree (W3) — the three-layer split (data / view-state / renderer), flat storage, flatten() as a pure free function, from the tree-view case study.
  • Table (W3) — span/selection over the HTML slot-grid model; the static span-capable core already landed per the table-span case study and table.md, so W3 is the interactivity overlay.

3. Open architectural questions

These are not deferrals — they are decisions that need evidence or a deliberate API choice before the library's modules are written. Each names what resolves it.

3.1 Rendering core: line-diff vs 2-D cell-grid

The single load-bearing choice. The candidates and their tradeoffs, from the survey's frame-diffing comparison:

  • Line-diff — a frame is a buffer of fully-styled ANSI byte-lines; the diff is bytes-equal per line; only changed lines are re-emitted with absolute cursor positioning (Bubble Tea lineage). Reuses today's string producers almost directly; damage tracking at whole-line resolution.
  • 2-D cell-grid — a frame is a flat Cell[] grid (grapheme + fg/bg/style per cell), double-buffered; the diff is per-cell; only changed cell runs are emitted (Ratatui/libvaxis/FTXUI/Notcurses lineage). More powerful (overlap, z-order, absolute placement, sub-line precision); the string producers need cell-grid adapters. The comparison recommends this as the best fit for D.

Resolution: the render-cost benchmark — both approaches implemented as D PoCs and benchmarked head-to-head across a suite of workload profiles (sparse update / full-screen churn / scrolling / resize), with cross-language reference implementations from the surveyed libraries as external calibration. The deciding axes are output bytes per frame, instructions per frame, and — likely dominant for a GC'd library — allocations and whether a zero-allocation steady state is achievable. core-cli's LiveRegion provides a naive full-repaint baseline.

Decision (render-bench-baseline): the 2-D cell-grid is best-or-tied on CPU at every change density and dominates on bytes on every profile; line-diff costs full-repaint CPU (it re-serializes every row to diff it), and the fix (cell-compare rows) only helps sparse workloads. Allocation is neutral — all approaches reach zero-alloc steady state. And the cross-language calibration shows a packed-cell D renderer reaches C parity on the common workload, so the architecture is fast enough in D. Chosen: the 2-D cell-grid, built on a compact packed cell (packed codepoint + flat style, long graphemes spilled out-of-line — the Notcurses/libvaxis inline-packing). A rough absolute-frontier comparison against the actual frameworks remains optional context.

3.2 Loop ownership and API shape

Whether the library owns the event loop (framework-style MVU, as Bubble Tea) or is a rendering library the application drives (as Ratatui, libvaxis). The comparison's recommendation is a library core (app owns the loop) with an optional MVU overlay built on it — update enforced pure, messages as SumType with exhaustive match!. This is an API decision (not a benchmark question); the render benchmark keeps it open by measuring the renderer independently of any loop.

3.3 Update strategy: immediate vs retained vs incremental

Immediate-mode (rebuild each frame) is the survey's recommended default for D (@nogc-friendly, stack-allocated widgets), with retained-mode and Nottui-style incremental reactivity as optional optimization paths — see comparison §8. Decided alongside 3.2 once the render core is fixed.

4. Non-goals

Grounded in the survey's consensus (see comparison):

  • terminfo — the survey's no-terminfo, query-first consensus; hardcoded sequences via base.term_control, with C-interop terminfo fallback only if a legacy-terminal consumer ever demands it.
  • Accessibility / screen-reader — no surveyed terminal library ships this; out of scope until a consumer needs it.
  • Terminal queries beyond capability probing (DA1/CPR/kitty-keyboard handshakes) — only as far as an in-scope interactive feature forces it.

(Inline image support, previously a natural non-goal, is promoted to a requirement — G1 above — grounded in Notcurses and libvaxis.)

5. Consumers / traceability

ConsumerItems exercised
Live operations-dashboard demoR1–R2, B1, I1–I3, E1–E2, W2–W4 — the full-screen driver, and the benchmark scene (§ PLAN)
releaseCould adopt E1/W3/W4 for an interactive stage/preflight view (today: line-based LiveRegion)
ciW2 (scrollback of example runs), I3 (resize)
test runnerAlready consumes the pure producers; a TestBackend (B2) would make widget tests golden
docs/examplesEvery landed widget ships a runnable example (ci --verify)

6. Execution

Milestones, dependencies, and the benchmark that resolves §3.1 live in the delivery plan. The benchmark harness under libs/tui/bench/ landed first and decided the core; the library proper is now being built on that decision. Landed in libs/tui/src/sparkles/tui/: the render core (cell.d + render.d — the 2-D cell-grid + per-cell diff, with scroll-region hardware scrolling (DECSTBM + SU/SD auto-detected between frames), the retained-mode compositing primitives Grid.fillRect / Grid.scrollRect, and color-depth folding (C1 — truecolor cells degrade to 256/16 for the detected terminal)), the terminal backend (terminal.d — raw mode / alt-screen / mouse / sync-framed diff flush + restore + depth detection), the input decoder (input.d — keys/mouse/resize → Event), and an app-owned event loop (app.d) with a runnable demo (examples/demo.d). First consumer: apps/hue's interactive TUI viewer renders through this stack. The style/layout (S1/L1–L2), widget model + widgets (W1–W5), and inline images (G1) build on top next.