Skip to content

hue --gui — Feature Requirements (raylib GPU window)

Status: living inventory · Date: 2026-07-23 · Scope: the hue raylib backend — apps/hue/src/gui.d (window + interaction + paint), gui_preview.d (markdown model → wrapped-line layout), gui_ansi.d (```ansi fence decode), gui_text.d (pure metrics/search) — plus the shared libraries it drives: sparkles:raylib-text (fonts, glyph draw, procedural box-drawing), sparkles:ghostty (off-screen VT), sparkles:syntax (the markdown model), and sparkles:core-cli (table renderer).

Compiled into hue's default build (the GUI backend is included by default — BLD1); a raylib-/ghostty-free variant is the no-gui config (BLD2). The window opens automatically when a display is detected and can be forced or suppressed with --gui/--no-gui (general MOD6). Status scheme and ID conventions: see the overview. App-wide behaviour (source, engine, themes) is in feature-requirements.md; this doc covers only the GUI. The entry point is gui.runGui.

Design & scope (issue #121)

hue --gui is a third consumer of hue's identical (source, events, theme) triple — the same triple renderAnsi/renderHtml consume — folded into raylib draw calls instead of ANSI escapes or HTML markup. Nothing in hue's producer pipeline changes (file read, canonicalLanguage, highlightInjected, theme resolution, the plain-text fallback); only the sink is new. This is the "styled runs as data" GPU backend sparkles:syntax was designed around (StyledSpan/byStyledSpan/ResolvedTheme are its third-backend contract, and FontStyle stays backend-neutral), and building it forced the two seams the syntax spec reserved: toRgb(Color, palette, default) and byStyledLine.

It is a read-only, windowed, syntax-highlighted view with a live theme previewer and a render-markdown.nvim-style markdown preview. It is deliberately not a text editor, an incremental/LSP-backed surface, a terminal emulator, or the Vulkan engine (#47) — it is the smallest honest GPU consumer of the styled-run API, hosted in the app that already produces it. (The diff write wave later adds bounded editing surfaces — an editable diff pane, a comment composer — via the toolkit editor component (UIA9); the not-an-editor-app doctrine stands.)

NOTE

Most GUI areas below — SEL (selection), MDP (markdown constructs), NUM (line numbers), RND/VIW/WRP/NAV/SCB/FND/COD — are not GPU-specific; they apply best-effort to the other interactive backends. For a markdown file the MDP preview is the default rendering in the terminal and HTML backends too (general MOD8), not GPU-only — the same widget-tree views, a different canvas. The full terminal port and its GUI→TUI parity map are in tui.md; the HTML best-effort rows are HTM3HTM5 in the general spec. This document remains the source of truth for the raylib backend itself.

Window & lifecycle (WIN)

IDRequirementStatusTraces to
WIN1--gui must open a resizable raylib window titled hue — <file> and run a 60 fps event loop until closed.full (e6063309)runGui: InitWindow, FLAG_WINDOW_RESIZABLE, SetTargetFPS(60)
WIN2The window's initial size must follow --window-width/--window-height in cells, sized to the loaded cell metrics.full (c2b49e99)SetWindowSize(w*cellW, h*cellH)
WIN3Resizing must reflow content: any change in the available column count re-runs layout.full (2febf905)widthCols() != lastWidthColsrelayout
WIN4The window title must always show the current theme name and index (file — theme (i/n)).full (2febf905)applyThemeSetWindowTitle
WIN5The close button must exit; app keys must not be hijacked by raylib's default exit key.full (e6063309)SetExitKey(KEY_NULL); WindowShouldClose
WIN6With neither --gui nor --no-gui, the window must open by default when a display is available; --no-gui/--tui forces the terminal previewer instead (general MOD6/MOD7).full (cdc813f6)general MOD6/MOD7; displayAvailable/wantGui in app.main

Font (FNT)

Fonts are owned by sparkles:raylib-text (FontSet); hue configures and drives it.

IDRequirementStatusTraces to
FNT1--font must accept a path, a family, or a fontconfig preference list (first installed family wins); default leads with Nerd-Font coding families.full (c2b49e99)FontSet.tryLoad(fontName); defaultGuiFont
FNT2--font-size is in points; it must be converted to pixels at 96-DPI, matching the terminal.full (c2b49e99)fontSizePx = fontSize*96/72
FNT3Ctrl-= / Ctrl-- must grow/shrink the font at runtime (reload faces + re-measure the cell), with a floor.full (2febf905)fonts.reload(size ± 2)
FNT4Glyphs outside the base atlas (icons, higher-plane, CJK) must load on demand: draw requests a codepoint, atlas grows after EndDrawing.full (efa6cd7f)fonts.flushPending() after EndDrawing
FNT5Bold/italic/strike/underline attributes must map onto the shared TextStyle; real bold/italic faces are used when present.full (d1dd79d5)mapStyle/mapAttrs; sparkles:raylib-text
FNT6Monospace column width (v1) counts one column per codepoint — wide/CJK/tab characters count as 1 cell, so wide glyphs may overlap.partialgui_text.columnWidth (documented v1 limit)
FNT7Color-emoji / flag glyphs render as tofu (raylib/stb_truetype ignores CBDT/COLR); monochrome symbols render.partialraylib limitation (documented in raylib-text)
FNT8Preview decoration glyphs (heading/callout/link icons, checkboxes, box-drawing) are Nerd-Font codepoints; a non-Nerd --font degrades them to tofu.partialdefaultGuiFont doc; icon glyph sites
FNT9CJK renders only when the primary face covers it — on-demand loading asks only the primary, and hue does not expose the terminal's --font-codepoint-map, so CJK is tofu unless a CJK --font is chosen.partialFontSet.resolveFace; tryLoad (no codepoint map)
FNT10Underline is drawn as a single straight rule; the shaped TermStyle's curly/dotted/double/dashed styles and independent underline color are not rendered.partialmapStyle (underline bit only)
FNT11Per-style faces are explicitly configurable, ghostty-style: --font-bold, --font-italic, --font-bold-italic each take a family, fontconfig pattern, or font file; unset faces auto-detect from the --font family. hue never fakes an italic — a missing italic face renders upright (a synthetic slant/shift breaks grid alignment; tokens italic under one theme but not another appeared to move between themes). Bold without a bold face still double-strikes.fullFontSet.FaceOverrides; loadFaceOverride; pickStyledFace
FNT12Variable fonts: selecting a named instance / axis values (wght/slnt/ital) for each styled face. fontconfig can resolve named instances; per-axis control needs FreeType variation APIs the raylib loader does not expose.researchedFNT11; raylib LoadFontEx limitation

Render model (RND)

IDRequirementStatusTraces to
RND1Each frame must clear to the page background and paint only the viewport-visible rows (index-culled by topLine).full (a0b0f93a)the op cull + base clip in runGui
RND2Every view must be one laid-out widget tree painted by a single painter; content scrolls by visual row.full (b4b837f4)ViewerModel.rebuild; the one paint pass
RND3A styled run must draw per grid column via the shared per-run drawText (per-codepoint face routing).full (b55be7aa)sparkles.raylib_text.drawText
RND4Content must have a 1-cell background-filled left padding and a scrollbar gutter on the right; the left padding is page background.full (5ca88625)padX, rightPad, gutterPx in runGui
RND5Totality is the law: an unknown language, missing grammar, oversized file, or parse failure must degrade to plain uncolored text in the window — never a crash, never half-colored output.full (74d8f6a3)ENG4 fallback (general spec); drawText
RND6The grid is monospace with a fixed cell advance (v1) — keeps column/gutter math and hit-testing trivial and matches apps/terminal.full (b55be7aa)cellW/cellH; gui_text.columnWidth
RND7The render fold must go through the two sparkles:syntax seams this backend motivated: byStyledLine (spans clipped at \n, stable per-row y) and toRgb(Color, palette, default) (the unset/default_/palette/rgb sum type → RgbColor).full (b55be7aa)byStyledLine; toRgb (sparkles:syntax)

Views & toggle (VIW)

IDRequirementStatusTraces to
VIW1A raw highlighted-source view must render the file's styled runs (wrapped); it is the only view for non-markdown files.full (b55be7aa)viewCodeDocument
VIW2A markdown file must open in a rendered preview by default; Tab must toggle preview ↔ raw.full (9ec7f847)showPreview; KEY_TAB
VIW3The preview model must be built once at load from the markdown structural model + per-fence highlight/ANSI decode.full (9b0a4b50)buildPreviewModel

Wrapping & visual-line model (WRP)

IDRequirementStatusTraces to
WRP1Prose must soft-wrap (word-wrap) to the available width, with hang-indent on continuation lines.full (d871c47c)Layouter.emitFlow
WRP2Code and ```ansi fence lines must hard-wrap (character-level, styled runs split at the column) to the panel width; no horizontal overflow/clipping.supersededfence rows render unwrapped and clip within the panel (the canvas clip pair)
WRP3The raw (non-markdown) view must wrap long physical lines too (greedy word wrap; indentation survives as a no-break prefix; an overwide token overflows and clips).full (f0148ef8)viewCodeDocument TextWrap.greedy
WRP4All views must reflow on window/font resize (rebuild the wrapped-line list on width change).full (8a03bff1)ViewerModel.relayout on widthCols change

Line numbers (NUM)

IDRequirementStatusTraces to
NUM1A file line-number gutter must number by physical (source) line, shown only on the first visual row of a wrapped line (continuations blank).full (f0148ef8)DocRow source ranges → the gutter's per-line dedup in runGui
NUM2The file gutter is on by default; --line-numbers=false disables it and l toggles it at runtime (reflow follows).full (5ca88625)lineNumbers; KEY_L
NUM3The gutter width must be stable (from the source line count) so toggling never oscillates the wrap width.full (8a03bff1)gutterCols() from ViewerModel.srcTotal
NUM4Each code block must have an in-panel code-relative line-number gutter (1..N), first-wrapped-row only, dimmed.full (5b862346)codeGutterStr; codeLineNumbers
NUM5Code line numbers are on by default; --code-line-numbers=false disables and c toggles at runtime.full (5b862346)codeLineNumbers; KEY_C
IDRequirementStatusTraces to
NAV1Mouse wheel must scroll (3 lines/notch); ↑/↓ and j/k scroll one line; PageUp/PageDown a page; Home/End to ends.full (e813f99a)normal-mode key block in runGui
NAV2gl must enter goto-line mode; entering a number jumps to that source line's visual row. (g became a prefix when the keymap was unified — gg/Shift-G are the vim motions, lantern LMP3.)full (1e218180)Mode.gotoLine; visualOfSrc
NAV3Scroll position must be clamped to [0, maxTop] every frame.full (a0b0f93a)top clamp

Scrollbar (SCB)

IDRequirementStatusTraces to
SCB1A scrollbar must appear only when content exceeds the viewport; its thumb size/position reflect the visible fraction and scroll progress.full (e813f99a)thumbGeometry; maxTop > 0
SCB2On hover/drag the handle must expand to a font-proportional width (1.5 cells) from a thin proportional idle rail, eased over time.full (5ca88625)hoverW/idleW; sb.currentWidth easing
SCB3The reserved right gutter must equal the expanded handle width so the handle fills the gutter without overlapping text.full (5ca88625)scrollbarGutter() == rightPad
SCB4Dragging the thumb must track the cursor; clicking the track must center the viewport on the click.full (e813f99a)sb.isDragging; track-click branch
SCB5The hover track + thumb must use a distinct (link-tinted) color, so they read against the grayscale page/code bands.full (e463ac95)scrollbarTrack/scrollbarThumb

Live theme cycling (THG)

IDRequirementStatusTraces to
THG1←/→ must cycle the theme (wrapping), re-resolve colors, rebuild the derived palette, and repaint live.full (2febf905)applyTheme; KEY_LEFT/KEY_RIGHT
THG2Decoration colors (heading accents, quote-bar cycle, callout accents, scrollbar) must be theme-derived, not fixed.full (17b33d09)resolvePalette; quoteBarColors

Search & goto (FND)

IDRequirementStatusTraces to
FND1/ (raw view) must start an incremental search; a bottom input line shows the query + match count.full (1e218180)Mode.search; input-line paint
FND2All matches must be found in the source and overlaid as a translucent tint, the current match brighter.full (1e218180)findMatches; match overlay
FND3Enter jumps to the first match at/after the view; n/Shift-n cycle matches (centered).full (1e218180)jumpToMatch; KEY_N
FND4Because lines wrap, matches (source coords) must be remapped to visual rows via each line's srcLine+wrapColOffset.full (f0148ef8)ViewerModel.visualOfMatch; per-match rects from selectionRects

Markdown preview constructs (MDP)

gui_preview.d Layouter, over the sparkles:syntax structural model (extractMarkdown). Each block/inline construct is a requirement.

IDRequirementStatusTraces to
MDP1Headings must render a per-level Nerd icon leader + per-level accent color + a subtle full-width band.full (21da63cf)Layouter.heading; headingAccents
MDP2Paragraphs must soft-wrap; inline styling (bold/italic/strikethrough) must map to attributes.full (17b33d09)inlineRuns; emitFlow
MDP3Inline code spans must render with a distinct background.full (17b33d09)inlineRuns codeSpan; inlineCodeBg
MDP4Bullet lists must use ●○◆◇ glyphs cycled by nesting depth; ordered lists number.full (76405ab1)Layouter.list; listDepth
MDP5Task-list checkboxes must render Nerd glyphs (󰄱 unchecked, 󰱒 checked in green).full (76405ab1)Layouter.list checkbox
MDP6Block quotes must draw per-depth colored gutter bars.full (17b33d09)quoteBarColors; drawPreview bars
MDP7GitHub callouts (> [!NOTE/TIP/IMPORTANT/WARNING/CAUTION]) must render a titled, iconed, accent-barred block; the marker is stripped from the body. Detection is source-based (the marker parses as a shortcut-link).full (bb296176)blockQuote/detectCallout/renderCallout (fixed 3d3ad89f)
MDP8Links must prepend a per-destination icon (github/gitlab/mail/web/file), then the underlined label.full (39c16ce5)inlineRuns link; linkIcon
MDP9Images must render a monochrome Nerd glyph + alt text + destination (not the tofu-prone 🖼 emoji).full (39c16ce5)inlineRuns image
MDP10Tables must render box-drawing borders with per-column alignment via sparkles:core-cli's drawTableLines; the header row bold.full (6760c5b1)Layouter.table; renderTableLines
MDP11Thematic breaks must render a full-width rule; HTML blocks render their raw lines (muted italic).full (d871c47c)Layouter.rule/htmlBlock
MDP12```ansi fences must be decoded to styled lines by an off-screen libghostty-vt terminal (no PTY/window) and rendered with the live theme's default colors substituted for default-colored cells.full (ffd36f7c)gui_ansi.decodeAnsi; AnsiLine/AnsiSpan
MDP13Definition lists and footnotes are not rendered (the bundled grammar collapses them); they degrade to plain paragraphs / nothing.partialmd/model.d (documented grammar limits)
MDP14LaTeX math, wiki-links ([[…]]), and inline highlight (==text==) are not rendered — deferred from the render-markdown.nvim shortlist.not startedrender-markdown gap-analysis (deferred tier)
MDP15Custom checkbox states beyond [ ]/[x] (e.g. [-]) are intentionally not recognized (a [-] is a genuine markdown shortcut-link — ambiguous).non-goaldropped in 3d3ad89f
MDP16Footnote superscripts ([^1]) and bare-URL autolinks (GFM) are not rendered — not modeled by the bundled grammar / inline pass.not startedmd/model.d (not modeled)
MDP17YAML/TOML front-matter is not rendered as a distinct block.not startedrender-markdown parity gap
MDP18Only the 5 GitHub callout types are recognized; the wider Obsidian set (abstract/todo/question/failure/danger/bug/example/…) is not.not startedmatchCalloutType (5 types)
MDP19Table border presets (round/double/none) and explicit alignment-indicator glyphs are not exposed (box borders + alignment ship via core-cli).not startedthe markdown view's table panel (fixed preset)
MDP20Further render-markdown cosmetic parity — heading border glyphs / min-width / margins, code-block diff backgrounds & insets, inline-code icon affixes, per-level org indent, sign-column gutter — is intentionally not matched (hue's preview is lighter-weight).non-goaldesign (lighter-weight preview)
MDP21Preview-diff decorations: the preview widgets must accept per-block / per-cell change decorations (added/removed/changed tint slots) so a markdown diff renders in the decorated preview (DVN6 --diff-preview) — changed table cells tinted in the box-drawn table, word-level tinting inside paragraphs, struck/collapsed deleted blocks.not startedDVN6; viewMarkdown decoration channel
MDP22VitePress code groups (::: code-group:::) must be one block in the document model, with the fenced blocks as its children, so a renderer can show one at a time behind a tab strip (WGT23). A tab's title is the fence's [label], falling back to its language. A malformed container (unclosed, or holding no fences) degrades to the blocks it was made of rather than swallowing them.partial — the model folds groups (md/model.d foldCodeGroups); the tabbed rendering is pending the selection state

Code blocks (COD)

IDRequirementStatusTraces to
COD1A code block must render as a rounded box on all four sides (top ╭─ lang ─╮, side │…│, bottom ╰──╯), connected by procedural box-drawing.full (88c5df57)codeFence; box runs
COD2The top border must embed a devicon + language label in a left cutout, with 1-char horizontal padding.full (88c5df57)codeFence header; langIcon
COD3A copy-to-clipboard button must sit in a right-side cutout of the top border (a space on each side); clicking copies the block body and flips to a green checkmark for ~1.2s.full (f1d468ac)the fenceHitBase header hit; ViewerModel.markCopied
COD4The copy button must anchor to the border cutout column (not screenW) so it stays aligned across widths.full (88c5df57)the header band's copy glyph (widget view)
COD5Highlighted code must keep its per-token syntax colors inside the panel; the panel background is distinct from the page.full (f1a53bd3)codeFence highlighted branch

Mouse selection & clipboard (SEL)

IDRequirementStatusTraces to
SEL1A left-drag must select content; the selection is a half-open byte range into the original source file (const(char)[]).full (88c5df57)sourceOffsetAt (the identity channel); TextSpan.srcStart
SEL2Selection must include content only — gutters, line numbers, box borders, bullets, and other decorations are excluded (they have no srcStart). Tables are not "excluded" but handled by the separate TBL grid regime.full (88c5df57)srcStart == size_t.max skip; TBL
SEL3srcStart must survive wrapping and word-splitting (each split piece carries its byte offset).full (88c5df57)wrapSpans identity propagation
SEL4The selection must be highlighted with a translucent tint; Ctrl-C must copy source[a..b] to the clipboard.full (88c5df57)selection highlight pass; SetClipboardText
SEL5A click consumed by the copy button or over the scrollbar must not start a selection.full (88c5df57)copyClicked/overSb guards
SEL6A ```ansi fence body must be cell-granular file-offset selectable, like a terminal emulator: each decoded cell maps to its source byte (the off-screen VT is 1:1 with source lines and a span's text is the contiguous source between two escapes), so a drag selects character-precise and the /gutter/padding stay excluded (SEL2). Copying a range yields the raw source between the endpoints (escapes included).full (c018f39a)ansiColToSrc; span srcStart in codeFence decoded branch
SEL7A copy mode must govern how a text-regime selection is emitted: --ansi-copy=raw|strip (default raw), toggleable at runtime with a status-bar indicator. strip runs stripSgr over source[a..b] (drops SGR escapes); it also applies to the ANSI code-fence copy button (COD3).full (debfd92d)CliParams.ansiCopy (CLI10); stripSgr; copy pass
SEL8Selecting the whole document (drag from start to end) must reproduce the input source file on the clipboard: every selectable region maps to source offsets, so source[selMin..selMax] over the full range is the file (ANSI raw + table markdown included).full (c018f39a)contiguous source-offset discipline across SEL/TBL; text-regime table crossing
SEL9Diff panes are selection domains: in a split diff a drag is confined to the side it starts in — the interactive mirror of the HTML rule (HTM8 / DVL4); copying honors --diff-copy=text|patch (DVL8) — plain text of the selected side, or a valid unified sub-patch.not startedproposed diff selection domain; DVL8

Table grid selection (TBL)

Tables break the SEL file-offset rule (cells are reflowed/stripped and the core-cli renderer wraps cell content across lines, so there is no clean byte-range). Instead a markdown table gets spreadsheet-style 2D selection, a regime distinct from SEL.

IDRequirementStatusTraces to
TBL1A drag inside a table must select via smart drag: within one cell → a sub-cell character range; across cells → a rectangle of whole cells (rows×cols spanned); Shift snaps the rectangle to full rows, Alt to full columns.full (debfd92d)table_select.d; gui.d table-regime drag
TBL2The selection must be highlighted (per selected cell's on-screen rects; a sub-cell selection clipped to its character range). Ctrl-C must copy it: a rectangle per --table-copy=tsv|markdown (CLI11), a sub-cell → the cell substring.full (debfd92d)TableGridMap.cellSpans; table_select serializers
TBL3Screen↔cell mapping must be wrap- and alignment-aware — mapping a click (output line + x) to (gridRow, gridCol, charInCell) and back to a cell's screen rects — via a new core-cli table layout API.full (634dff4b)ui.components.table.render TableGridMap/drawTableMapped; wrapTable; keyedRects/MdCell
TBL4A drag that starts on a table cell uses the 2D grid regime (confined to that table); a text-regime drag that merely crosses a table selects it character-precise (each cell maps to its source byte via cellSrc[row][col] + charInCell; borders fall back to the block span), so whole-document selection reproduces the table markdown (SEL8). The regime is fixed at mouse-down.full (bc3ef6cf)gui.d mouse-down regime pick; TableView.cellSrc; TableGridMap.hit
TBL5The smart-drag→region rule and the TSV/markdown/sub-cell serialization must be presentation-free and testable (no raylib), consumed by the GUI painter/copy.full (4d1e2da2)table_select.d (pure); unit tests
TBL6A whole-table copy button must sit in a right-side cutout of the table's top border (the same idiom as the code button COD3, anchored at lineCols-3 — slightly left of the corner); clicking copies the table's raw markdown source (like the code button copies the raw body — the drag selection is what honors --table-copy) and flashes a checkmark. Rendered + click-copied in the GUI and the TUI (tui.md TSL).supersededtable copy buttons were removed with the widget view — cells carry srcStart, so selection-copy (TBL4/SEL8) is the one affordance

Fullscreen (FSC)

IDRequirementStatusTraces to
FSC1F11 must toggle borderless fullscreen and restore window decorations on the way back (managing the undecorated flag + geometry ourselves, not raylib's ToggleBorderlessWindowed).full (e463ac95)F11 handler in runGui (fixed 8a03bff1)
FSC2Fullscreen must target the window's current monitor on X11; on Wayland the app cannot set its position, so the window stays on its monitor rather than jumping to the primary.partialGetCurrentMonitor/SetWindowPosition (Wayland size may be wrong if monitors differ)

Debug / CI hooks (DBG)

Environment-variable hooks that make the GUI deterministically capturable headless.

IDRequirementStatusTraces to
DBG1HUE_GUI_SCREENSHOT=<path> must render a few warm-up frames then write a PNG and exit (headless golden capture).full (a0b0f93a)shotPath; TakeScreenshot
DBG2HUE_GUI_TOP, HUE_GUI_FONTSIZE, HUE_GUI_PREVIEW, HUE_GUI_SEARCH must pin initial scroll / font px / view mode / a preselected search for deterministic captures.full (1e218180)env reads in runGui

Box-drawing (BOX, shared library)

Implemented in sparkles:raylib-text (box.d), consumed by hue's tables, code boxes, and quote bars.

IDRequirementStatusTraces to
BOX1Box-drawing glyphs (─│┼╭╮╰╯ + heavy/header-rule forms) must render procedurally (arms drawn to the cell edges) so rules connect across cells instead of using gappy font glyphs.full (1ab4e71d)sparkles.raylib_text.box.drawBox/boxSpec
BOX2Uncovered forms (dashed/double/diagonal) must fall back to the font glyph.full (1ab4e71d)boxSpec returns valid == false

Semantic refinement (SEM)

IDRequirementStatusTraces to
SEM1The tree-sitter base highlighting may be refined with semantic kinds (member vs local, @safe call, …) from sparkles:dmd-lsp's identifierTypes — the semantic-tokens overlay the syntax spec reserves.researched/not-startedissue #121 M6; issue #120 §5 (bonus)

Milestones (issue #121)

The GUI backend shipped as milestones M0–M5; M6 is optional/future. (There is no M7+ in the hue-GUI track; the M0–M8 / D1–D3 ladder belongs to the twoslash / dmd-lsp design — see twoslash.md.)

MilestoneScopeStatusRequirements
M0gui config + --gui gate + version(HueGui) seamfull (e6063309)WIN1/5, general MOD1/2, NFR3
M1The render fold (toRgb/byStyledLine seams; draw the triple on the GPU)full (b55be7aa)RND1/3/6/7, VIW1
M2Viewport culling + line-number gutter + scrollbarfull (a0b0f93a)RND1, NUM1, SCB1
M3Font sizing + window resize + live theme cyclingfull (2febf905)FNT3, WIN3, THG1
M4Incremental search + goto-linefull (1e218180)FND*, NAV2
M5Extract sparkles:raylib-text; refactor terminal + hue onto itfull (d1dd79d5)FNT*, RND3, BOX*
M6(optional) Semantic refine via sparkles:dmd-lsp identifierTypesnot startedSEM1
Markdown preview (render-markdown.nvim parity) — a later effort on top of M5fullWRP*, MDP*, COD*, SEL*
GUI-by-default: gui/tui autodetection + no-gui config/packagepartial (cdc813f6, 29bf1a65)WIN6/MOD6/7/BLD1/BLD2 done; BLD3 hue-no-gui nix package pending

Module coverage (GUI spec)

SourceKey symbolsRequirements
apps/hue/src/gui.drunGui (window/fonts/input/paint), scrollbar (Scrollbar/thumbGeometry), hitAt, text/table selection regimes, copy pass (--ansi-copy/--table-copy + toggle keys), the explorer pane, F11, env hooksWIN*, FNT2/3, RND*, VIW2, NUM2/3/5, NAV*, SCB*, THG*, FND*, COD3/4, SEL*, TBL1/2/4, FSC*, DBG*
apps/hue/src/viewer_model.dViewerModel (the viewer's Whole: document, theme-resolved colors, pipeline artifacts, folds, scroll, search), MdFence/MdCell, attrsToTextStyleRND2, WRP3/4, NUM1, FND2/4, COD3, MDP12
apps/hue/src/gui_preview.dPreviewModel/CodeFence, buildPreviewModel, stripSgr, quoteBarColorsVIW1/3, MDP* (the model side)
apps/hue/src/table_select.dpure smart-drag→region + TSV/markdown/sub-cell serializersTBL1/2/5
apps/hue/src/gui_ansi.ddecodeAnsi, AnsiLine/AnsiSpan, AttrMDP12
apps/hue/src/gui_text.dcolumnWidth, lineCount, buildLineStarts, findMatches, MatchFNT6, WRP* (metrics), FND2, NUM3
sparkles:raylib-textFontSet, drawText, drawGrapheme/drawSolid, box.drawBoxFNT1/3/4/5, RND3, BOX*
sparkles:ghosttyoff-screen VT (ghostty_terminal_*)MDP12
sparkles:core-cliui.table.drawTableLines + drawTableMapped/TableGridMap (screen↔cell mapping), args, key_input, term_capsMDP10, TBL3, CLI* (general)

General requirements · Overview