Skip to content

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

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, PRV1PRV8) 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)←/→ cycle — the shipped previewer already does this (PRV2)
FND search & gotofullincremental search; matches via reverse-video / tint
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)previewer.d input dispatch (proposed)
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 LMP3LMP5
TKB3/ and PgUp/PgDn are pane-scoped: a focused tree navigates its rows as every tree does. This changed the window, not the terminal — the terminal explorer already behaved this way. Theme cycling keeps the arrows in the viewer and gains <leader>ut from anywhere.full (e71a9417)lantern LMP6
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)
TSF2The laid-out view must reflow on terminal resize (SIGWINCH) — rebuilt at the new column width (the WRP4 analog).full (dd70e1b4)proposed SIGWINCH handler
TSF3The TUI must reuse the shared raylib-free views (viewMarkdown/viewCodeDocument/viewTwoslashDocument) unchanged — one view, two canvases.full (8172e070)PreviewTui.rebuildMd
TSF4The per-frame paint core should stay @nogc nothrow (NFR1); load-time layout may allocate, as the GUI's does.partialpreviewer.d @nogc core (NFR1)

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.

Scrollbar (TSB, best-effort → gui.md SCB)

IDRequirementStatusTraces to
TSB1A scrollbar must occupy the right gutter column and appear only when content exceeds the viewport; the thumb size/position reflect the visible fraction and scroll progress (SCB1).full (b8809549)proposed cell scrollbar
TSB2The thumb must be draggable and the track click-to-page/center via SGR mouse (SCB4); the wheel scrolls (NAV).full (b8809549)TIN2/TIN3 dispatch
TSB3The bar must render with block glyphs (▏▎▍▐█ / half-blocks); hover-expansion is approximated (no smooth easing — SCB2 degrades).full (b8809549)proposed block-glyph thumb

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

IDRequirementStatusTraces to
TSL1Left-drag must select content only as a half-open byte range into the original source (SEL1/SEL2); gutters, line numbers, borders, and decorations are excluded, and the range survives wrapping (SEL3).partial (8b1d2aac)reuse PreviewRun.srcStart (gui_preview.d)
TSL2The selection must be tinted (reverse-video / SGR); a copy key must write source[a..b] to the clipboard via OSC 52 (TCL1) — the terminal analog of SEL4.full (8b1d2aac)selection pass; TCL1
TSL3App-level mouse selection requires SGR mouse tracking, which suppresses the terminal emulator's native selection — hue must provide its own.full (8b1d2aac)TIN2
TSL4```ansi blocks should gain cell-granular file-offset selection at TUI parity with the GUI's SEL6 (the shared identity channel — TextSpan.srcStart / sourceOffsetAt); the GUI-first table grid selection (TBL) and copy modes (SEL7/CLI10/CLI11) are a later TUI parity item.not startedthe identity channel (sourceOffsetAt); table_select.d (GUI-first)
TSL5The code-fence and whole-table copy buttons (COD3/TBL6) must render in the top-border cutout and, on click, copy the raw fence body / raw markdown table source via OSC 52 (TCL1), with a ✔ flash until the next event.supersededtable copy buttons were removed with the widget view (TBL6); the fence copy is the header-band hit (copyFenceAt)

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 constructionMDP-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-T1Every GUI markdown construct must render in cells, painted from the same widget view: headings (MDP1), inline styles (MDP2), inline code (MDP3), bullets / ordered lists (MDP4), task checkboxes (MDP5), quote bars (MDP6), callouts (MDP7), links (MDP8), images (MDP9), tables (MDP10), rules / HTML blocks (MDP11). Nerd-glyph fidelity depends on the terminal font, like FNT8 / TCP3.full (dd70e1b4)paintGrid paints the viewMarkdown ops (TSF3)
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
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
MDP-T4The GUI's markdown gaps and non-goals inherit unchanged — the TUI paints the same model, so it renders neither more nor less: definition lists / footnotes fall back to plain paragraphs (MDP13); LaTeX math, wiki-links, and ==highlight== are not rendered (MDP14); footnote superscripts / bare-URL autolinks (MDP16) and YAML/TOML front-matter (MDP17) are not; only the 5 GitHub callout types are recognized (MDP18); table cells are flattened to plain text (MDP10). "Parity" is bounded to what the shared model renders.not startedinherited from viewMarkdown

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-T2Below the split-width threshold the TUI degrades to the stacked single-rail unified variant (a context-aware line-number column: old numbers on -, new on +/context) rather than two cramped rails (DVL3; the diffs.nvim precedent).not startedresearch: diffs.nvim
DIF-T3Staging, hunk/conflict navigation, and the LINE/RANGE/HUNK selection modes (DST2/DST3, CFV3) operate through the keymap and the existing TSL selection machinery; copying honors --diff-copy=text|patch (DVL8) via OSC 52 (TCL1).not startedkeymap.d; TSL*; TCL1
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)
TCP3Nerd-glyph decorations depend on the terminal font (the FNT8 doctrine); a non-Nerd terminal font shows tofu — acceptable and documented.not starteddecoration glyph sites

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*
apps/hue/src/gui_preview.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