Skip to content

hue TUI — Feature Requirements (full-screen terminal viewer) ​

Note: requirements shared with the GPU window moved to viewer.md and are listed under Retired IDs; what remains here is terminal substrate — escape-sequence decoding, the alt-screen, capability probing and OSC 52.

Status: shipped (T1–T4); T5 outstanding · Date: 2026-07-29 · Scope: the interactive terminal mode — a full-screen TUI with scrolling, a scrollbar, mouse support, selection, wrapping, line numbers, and the markdown preview. It is the terminal port of the GUI viewer.

NOTE

The full-screen viewer is shipped (tui.d, on sparkles:tui) and is the interactive tty mode on POSIX. The older minimal theme-selection previewer (previewer.d, PRV1–PRV8) survives only as the non-POSIX fallback and is scheduled for removal, with theme selection folding into this viewer as a widget overlay — see ui-architecture.md UIA4. Status legend and ID conventions: see the overview.

Design & scope ​

The full TUI is the same interactive viewer as the GUI, painted into terminal cells instead of GPU quads. It consumes hue's identical (source, events, theme) triple and — crucially — reuses the GUI's raylib-free views unchanged: the shared widget views (viewMarkdown / viewCodeDocument / viewTwoslashDocument) and gui_text.d (pure metrics/search) are already terminal-independent and unit-tested. The TUI is therefore a second canvas over the same widget trees, not a parallel layouter — the GUI's RND2 ("one wrapped visual-line list, painted by a single painter") generalizes across backends.

Three things the GUI gets from raylib that the TUI must supply itself: input (SGR mouse + an expanded key vocabulary), clipboard (OSC 52, there being no windowing clipboard API), and the surface (the alt-screen cell grid, which the shipped previewer already manages). Two things get simpler in a terminal: the grid is natively monospace (RND6 is free), box-drawing glyphs render without gaps (the BOX GPU workaround is unnecessary), and ```ansi fences can be passed straight to the real terminal instead of decoded through an off-screen VT (MDP12 / sparkles:ghostty).

GUI → TUI parity map ​

How each GUI requirement area applies to the TUI. full = ports directly · best-effort = ports with a terminal caveat · n/a = not a terminal concern · future = backend-agnostic, deferred with the GUI's. All GUI areas are in gui.md; IDs below are bare references into it.

GUI areaAppliesTerminal note
RND render modelfullthe same widget trees, painted into cells (paintGrid); the terminal is natively monospace (RND6 free)
VIW views & togglefullraw + markdown preview, Tab toggles
WRP wrappingfullsame soft/hard wrap; reflow on SIGWINCH (TSF2)
NUM line numbersfullfile gutter + per-code-block gutter, in cells
NAV navigation & scrollfullwheel (SGR mouse), j/k, PageUp/Down, Home/End, goto-line
SCB scrollbarbest-effort → TSBcell-column bar, block-glyph thumb, mouse drag / track-click; no smooth width easing
THG live theme cyclingfull (partial now)<leader>ut cycles (←/→ before KBD1) — the shipped previewer already does this (PRV2)
FND search & gotofull, minus :Nincremental search, n/N jumps and match tinting, all on the shared matcher and the shared input bar. A cell carries no alpha, so the window's translucent gold/orange are pre-mixed against the page background at the same 70/255 and 130/255. :N goto-line is the one gap (D8)
MDP markdown constructsbest-effort → MDP-T (here)all decorations; Nerd-glyph dependence like FNT8; box-drawing is native; ```ansi fences pass through the real terminal
COD code blocksfull (best-effort)code gutter + highlighted body + border via native box glyphs; copy region + OSC 52 (TCL)
SEL selection & clipboardbest-effort → TSLapp-level drag-select → source offsets; clipboard via OSC 52; suppresses the terminal's native selection
FNT fontn/athe terminal owns the font/cell; bold/italic/underline → SGR attributes (the FNT5 analog)
WIN window & lifecyclen/ano window; the alt-screen is the surface; resize arrives as SIGWINCH
FSC fullscreenn/athe terminal emulator's concern, not hue's
BOX procedural box-drawingn/a (solved)box glyphs render natively without gaps — the GPU arms-to-edges workaround isn't needed
DBG debug/CI hooksbest-efforta headless frame-dump analog for golden capture (the previewer already assembles a frame buffer)
SEM semantic refinementfuturebackend-agnostic, deferred with the GUI's SEM1

Terminal input (TIN) ​

IDRequirementStatusTraces to
TIN1The input layer must decode an expanded key vocabulary — arrows, PageUp/Down, Home/End, Tab, Enter, Ctrl-C, and printable characters (for search / goto) — beyond the shipped minimal Key{up,down,enter,cancel,other}.full (dd70e1b4)core-cli key_input.d (must grow / be superseded)
TIN2SGR mouse tracking (mode 1006 + 1000/1002) must be enabled on entry and disabled on exit; press / release / drag / wheel events with button + modifiers must decode to (row, col) cell coordinates.full (b8809549)proposed mouse decoder (core-cli)
TIN3Wheel events must scroll (NAV); left press/drag/release must drive selection (TSL); clicks must hit-test the scrollbar (TSB), the code-block copy region (COD), and notifier popup items (NTF6).partial (b8809549)handleWheel/handlePointer (tui.d); the notifier items wait on NTF6
TIN4Mouse tracking must be restored (disabled) on exit, signal, and crash, so the terminal is never left in mouse mode.full (b8809549)scope(exit) / signal handler (proposed)

Keyboard policy (TKB) ​

IDRequirementStatusTraces to
TKB1The terminal backends must resolve keys through the one binding table (lantern KEY1), not a switch of their own. Both the viewer and the explorer carried one, and both had drifted from the window's.full (e71a9417)tui.handleKey; explorer.handleCommand
TKB2The divergences that routing exposed are resolved vim-correct, not by picking a side: gg top, Shift-G bottom, gl go-to-line (both previous meanings survive because vim has both); y copies the selection everywhere and the ANSI-copy mode moves to <leader>uy; q quits in both backends.full (8e4b0250)lantern LMP3–LMP5
TKB4The key guide must paint into the cell grid from the same widget tree the window paints (lantern LTN5), and the event loop must take the guide's remaining delay as a second deadline — a terminal has no frames, so a panel that waits for the next keystroke is a panel nobody sees.full (8ec10bee)tui.paintLantern; workspace.d loop

Terminal clipboard (TCL) ​

IDRequirementStatusTraces to
TCL1Copy must write to the system clipboard via OSC 52 (base64) — the only portable in-band terminal clipboard; no read is needed.full (8b1d2aac)proposed OSC 52 writer
TCL2A terminal without OSC 52 support must degrade — show a status message instead; copy must never block or corrupt the screen.not startedTCP1 capability probe

Surface & frame (TSF) ​

Extends the shipped previewer's frame discipline.

IDRequirementStatusTraces to
TSF1The TUI must render into the alt-screen (hide cursor), one synchronized-output frame per repaint, assembled into a single buffer and flushed with one write.partial (844680a3/0657c94a)PRV7/PRV8; NFR2 (previewer already does this)
TSF4The per-frame paint core should stay @nogc nothrow (NFR1); load-time layout may allocate, as the GUI's does.not startednone: the @nogc core was the retired previewer (NFR1); the TUI paints through GridCanvas into Screen, and nothing checks that path's attributes

NOTE

TSF3 pulls gui_preview.d (and, for ```ansi fences, potentially gui_ansi.d/sparkles:ghostty) into the default terminal build. Reconcile with NFR3 / the no-gui build (BLD2) — the preview layout is raylib-free, but the off-screen VT is not needed in a real terminal (fences pass through), so the TUI can take the layout without the VT dependency.

Selection & clipboard (TSL, best-effort → gui.md SEL) ​

IDRequirementStatusTraces to
TSL3App-level mouse selection requires SGR mouse tracking, which suppresses the terminal emulator's native selection — hue must provide its own.full (8b1d2aac)TIN2

Markdown preview in the terminal (MDP-T, best-effort → gui.md MDP) ​

The TUI is a second canvas over the shared widget views (TSF3), so the GUI's markdown constructs reach the terminal by construction — MDP-T1 binds each to its gui.md source, and MDP-T4 records the gaps that inherit unchanged. Three deltas are terminal-specific (MDP-T2/MDP-T3):

IDRequirementStatusTraces to
MDP-T2Box-drawing (tables, code-block borders, quote rules) must use the native terminal glyphs — no procedural drawBox; a real terminal renders ─│┼╭╮╰╯ without gaps.full (dd70e1b4)native SGR box glyphs
MDP26In the terminal, link text must be wrapped in OSC 8 so the terminal itself makes it clickable. A hyperlink is opened and closed inside each redrawn run, never across the diff's cursor moves. The GUI ignores the channel — there the hand cursor is the affordance.fullTextSpan.linkId → Visual → CellT → Screen.render; MdLinkTable
MDP-T3```ansi fences render through the shared fence renderer: in the default (ghostty) build they are decoded by the off-screen VT and re-emitted as resolved-color spans (full fidelity, the code-panel box preserved); in the raylib-/ghostty-free no-gui build the SGR is stripped to plain text (no VT). Raw byte pass-through into the bordered panel was rejected — it would corrupt the box.partial (241e8052)hueFenceRenderer; ViewerModel's decode hook; the strip fallback

Diff view in the terminal (DIF-T, best-effort → diff-view.md) ​

The diff/PR/review feature reaches the terminal by construction (the shared widget views, TSF3); these rows bind the terminal-specific deltas.

IDRequirementStatusTraces to
DIF-T1Both diff layouts render in cells from the shared widget view — dual gutters, filler rows, and hunk separators via native terminal glyphs (the MDP-T2 doctrine), diff row backgrounds as SGR over the syntax colors (DVL1/DVL2).not startedpaintGrid over the diff widget view
DIF-T4The write surface reaches the terminal: inline editing (DST5) and the comment composer (DCM3) through the UIA9 editor component's terminal input; degraded terminals fall back per TCP1 (no mouse → keyboard-only staging; no OSC 52 → copy disabled).not startedUIA9; TCP1

Capabilities & degradation (TCP) ​

IDRequirementStatusTraces to
TCP1On entry the TUI must probe tty status, color depth (CLR1), mouse, and OSC 52 support, and adapt (no mouse → keyboard-only; no OSC 52 → copy disabled).not startedcore-cli term_caps.d
TCP2A non-tty invocation must fall through to the non-interactive ANSI whole-file emit (MOD3/MOD5) — the full TUI is only for interactive ttys.full (74d8f6a3)interactive gate (general spec)

Retired IDs → viewer.md ​

Seventeen T* rows specified capabilities that gui.md already specified, which is how the two backends were able to drift apart. They are absorbed into the tier-scoped rows in viewer.md, which carry the old ids in their Supersedes column. This table exists so a citation of a retired id still lands somewhere useful.

RetiredAbsorbed intoNote
TSF2NAV5, NAV6, WRP4Only "SIGWINCH is the resize source" was terminal-specific; that clause stays in TSF1's section
TSF3RND2tui.md's own prose already said RND2 "generalizes across backends"
TSB1SCB1, SCB3
TSB2SCB4Divergence D3: said click-to-page; SCB4 says centre. Centre wins
TSB3SCB2The cell degradation is now SCB2's precise → interactive tier
TSL1SEL1, SEL2, SEL3Cited them directly
TSL2SEL4Divergence D4: y copies everywhere per TKB2; SEL4 had never been updated
TSL4SEL6, SEL7, TBL1–TBL8A deferral note, not a requirement
TSL5COD3, TBL6Divergence D1: anchored the fence button to the header band; the border cutout wins
TSL6SEL10The "request ticks only while capture is live" clause is terminal substrate and stays
TSL7TBL7, TBL8Both already said "in both hosts"
TSL8SEL4Divergence D2: holdMs: 1600 versus COD3's ~1.2 s. 1.2 s wins
TCP3FNT8Same doctrine, two statuses — FNT8 partial, TCP3 not started
TKB3THG1Divergence D5: silently narrowed THG1 to pane scope. THG1 now says so
MDP-T1MDP1–MDP11A citation list
MDP-T4MDP13–MDP18A citation list
DIF-T3SEL9

Milestones ​

MilestoneScopeStatusRequirements
T0Minimal theme-selection previewer (viewport slice + ↑/↓ theme cycle)full (74d8f6a3)PRV* (baseline)
T1Viewport scrolling over the reused GUI wrapped-line layoutfull (dd70e1b4)TSF3, RND/VIW/WRP/NUM
T2SGR mouse + wheel + the cell scrollbarfull (b8809549)TIN, TSB, NAV
T3Selection → source offsets + OSC 52 copyfull (8b1d2aac)TSL, TCL, SEL parity
T4Markdown-preview parity + code blocks + incremental searchpartial (30e8e133)MDP-T*, COD, FND
T5Notifier popups in the terminalnot startednotifier.md NTF6

Module coverage (TUI) ​

SourceRequirements
apps/hue/src/previewer.d (extended to a full viewer)TSF*, TSB*, TSL*, MDP-T*, TCP*, PRV*
libs/doc-view/src/sparkles/doc_view/preview_model.d / gui_text.d (reused as-is)TSF3 (shared layout); RND/WRP/NUM/SEL model
sparkles:core-cli key_input.d (expanded), term_caps.dTIN*, TCP1
OSC 52 writer (proposed)TCL*

→ GUI requirements · General requirements · Notifier · Overview