Skip to content

hue — Feature Requirements (all rendering modes)

Status: living inventory · Date: 2026-08-08 · Scope: apps/hue features common to every rendering mode — invocation, source acquisition, concatenation, line selection, text normalization, language detection, the highlight engine, themes, color policy, output-mode dispatch, document chrome, paging, the ANSI / HTML / terminal-previewer sinks, degradation, and non-functional requirements. The raylib GUI is specified separately in gui.md; document chrome in chrome.md and paging in pager.md.

NOTE

The cat-citizen areasCAT, RNG, TXT, CHR, WID, PGR and the LNG2+/THM4+/CLR2+ extensions — came from a feature-by-feature reading of bat (78951393). hue is well ahead of bat on interactivity and well behind it on being an ordinary, pipe-friendly cat replacement, and those areas close that half.

They are scoped by the Pareto rule, not by parity: full coverage only where it is cheap, and otherwise the 80% that carries the use cases. Each area states what was deliberately dropped, and three whole clusters are deferred outright (DEF25DEF27). Where bat's design is good it is taken as a model (--style's composable component set, the range grammar, the auto/never/always triad); where hue's own abstractions can do better the requirement says so and names the library — chrome as sparkles:ui widgets rather than ANSI strings, the change column as sparkles:diff rather than a second engine, language resolution in sparkles:syntax rather than in the app — and, for light/dark, hue specifying no detection at all, because the platform-UI-guidelines research already owns that question.

See the overview for the status scheme (not started / researched / partial / full (<sha>)), the ID scheme, and the rendering-mode map. Requirement statements use lowercase must / should with the repo's usual force. "Traces to" names the implementing source and, for full rows, the primary commit is in the Status cell.

Invocation & CLI (CLI)

Parsed by sparkles.core_cli.args.parseCliArgs!CliParams in app.d.

IDRequirementStatusTraces to
CLI1hue [options] [target] must render target — a file path, an HTTP(S) URL (SRC3), or a directory (SRC4, a multi-document set); with no target it highlights hue's own bundled source.partial (74d8f6a3)app.main; sourcePath/source; directory via SRC4 (URL pending)
CLI2--html must select HTML output instead of ANSI.full (74d8f6a3)CliParams.html
CLI3--theme <name> must select a built-in theme (default catppuccin-mocha).full (74d8f6a3)CliParams.theme
CLI4--gui must force the raylib window and --no-gui (alias --tui) must force terminal output; with neither, hue must autodetect (see MOD6). On a no-gui build, --gui must error out cleanly.full (cdc813f6)CliParams.gui/noGui/tui; wantGui
CLI5--font, --font-size, --window-width, --window-height must configure the GUI window (see gui.md).full (c2b49e99)CliParams.font*/window*
CLI6--line-numbers / --code-line-numbers (both default on, disableable with =false) must configure the GUI gutters.full (5b862346)CliParams.lineNumbers/codeLineNumbers
CLI7--help must print a usage/description header for the program and every option.full (d87397b3)HelpInfo in app.main
CLI8--background <no-background|spans|full> must select the terminal background mode (BGM); the default is full.full (d404dc8c)CliParams.background; parseBackgroundMode
CLI9--raw must force the highlighted-source view instead of the markdown preview, in every sink (GUI, TUI, ANSI, HTML). Without it a markdown file renders the decorated preview by default (MOD8); non-markdown files are always source.full (af6ebe95)CliParams.raw; preview-vs-raw branch in app.main
CLI10--ansi-copy=raw|strip (default raw) must set how a GUI text-regime selection over a ```ansi block copies — verbatim source (escapes) or stripSgr-cleaned (gui.md SEL7); runtime-toggleable.full (debfd92d)CliParams.ansiCopy
CLI11--table-copy=tsv|markdown (default tsv) must set how a GUI table grid selection copies — tab-separated cells or re-emitted | … | markdown (gui.md TBL2); runtime-toggleable. The source mode re-emits a DSV document's own dialect and auto (the new default) picks it for DSV documents (DSC2, 090f05c6).full (debfd92d)CliParams.tableCopy
CLI12--out <dir> must select the output directory for the static HTML gallery (HTM6) a directory target renders into; with --out omitted it must default to <target>/html.full (b61f4701)CliParams.outDir; gallery.md GAL2
CLI13The --diff family must select and configure the diff content kind: --diff [<a> <b> | <rev>[..<rev>]], --patch (force stdin-patch), --diff-layout unified|split, --diff-structural=on|off, --diff-preview, the whitespace/noise toggles, and --diff-copy=text|patch — registered here, specified in diff-view.md.partial (a2b634e8)--diff/--patch/--staged ship (DVS1DVS3); the layout, noise and copy flags await their milestones
CLI14--pr <number|url> must open a pull/merge-request session through the forge seam.not starteddiff-view.md DPR1/DPR7
CLI26The --dsv family must select and configure the DSV content kind: --dsv (force detection, like --markdown), --dsv-delimiter <char>, --dsv-quote <char>, --dsv-header auto|yes|no — registered here, specified in dsv-preview.md (DSK2/DSD4).not starteddsv-preview.md DSK*/DSD*
CLI27The --format-preview / --format-width COL / --formatter NAME family must start view in the format-preview mode (ruler at COL, else .editorconfig discovery) and select the formatter (a miss lists candidates) — registered here, specified in format-preview.md.full (36b601ae5)format-preview.md FMV8
CLI28The --scroll-anchor segment|line option must select what a re-layout keeps at the top of the pane (gui.md NAV5); an unknown spelling warns and falls back to segment.full (0485a4c3)gui.md NAV5; parseScrollAnchor

The cat-citizen options

hue is meant to be aliasable to cat and usable as $PAGER / $MANPAGER / git core.pager, which is a surface bat already worked out well. The rows below register the flags; each names the section that specifies the behaviour. Short aliases are spelled the way sparkles.core_cli.args already supports (@CliOption("r|line-range", …)), and match bat's letters wherever bat has one, so muscle memory transfers.

IDRequirementStatusTraces to
CLI15-r/--line-range <spec> (repeatable) and -H/--highlight-line <spec> (repeatable) must restrict and emphasize the visible lines; a path:line[:col] target must be accepted as a third spelling.not startedRNG
CLI16--style <components>, --decorations <when>, -p/--plain, -n/--number and -f/--force-colorization must select the document chrome.not startedchrome.md STY1STY5
CLI17--changes-only[=<N>] must restrict the view to git-changed lines plus N lines of context. (bat spells this -d/--diff; --diff is already hue's two-file diff, so the letter is not reused.)not startedchrome.md CHG4
CLI18Multiple positional targets, a bare - for stdin, --file-name <name> and -E/--quiet-empty must give hue cat's concatenation semantics.not startedCAT
CLI19--paging <when>, -P, --pager <cmd>, --preformatted and --set-terminal-title must configure paging and pre-formatted input.not startedpager.md PAG1PAG6, PIN4
CLI20--sanitize <when>, --strip-ansi <when>, -A/--show-all, --nonprintable-notation <notation>, --binary <behavior>, -s/--squeeze-blank, --squeeze-limit <N> and --tabs <T> must configure input normalization. hue's --tab-width and --list-whitespace fold in.not startedTXT
CLI21-l/--language <lang>, --fallback-syntax <lang>, -m/--map-syntax <glob:lang> (repeatable), --ignored-suffix <suffix> (repeatable) and -L/--list-languages must drive language resolution.not startedLNG
CLI22--theme must additionally accept auto, dark and light; --theme-dark <name>, --theme-light <name> and --list-themes must complete the pair.not startedTHM
CLI23--color <when> and --italic-text <when> must set the color policy, and $NO_COLOR must be honoured.not startedCLR
CLI24--wrap <mode>, -S/--chop-long-lines and --terminal-width <width> (accepting +N/-N offsets) must set wrapping and the assumed width.not startedWID
CLI25-u/--unbuffered must render input incrementally instead of waiting for EOF.not startedpager.md STR1

Source acquisition (SRC)

IDRequirementStatusTraces to
SRC1With a path argument the whole file must be read into memory as the highlight input.full (74d8f6a3)readText(sourcePath)
SRC2With no path, hue must highlight its own app.d, embedded at compile time via import() so it works from any install.full (1c7398a0)import("app.d"); stringImportPaths
SRC3An http(s):// URL target must be fetched over the network and rendered like a file; the language is derived from the URL path extension, falling back to the response Content-Type. A fetch failure must report an error, not crash. (This is the user's explicit request — unlike the opt-in auto-loaded remote media of media.md IMG3.)not startedproposed URL fetch in app.main
SRC4A directory target must open a multi-document session (SRC5) over the files it contains: interactively, the gallery index (gallery.md GAL5) — superseded by the richer file-tree explorer (tree-view.md TVU1) when that lands; with --html, the static gallery (HTM6). A directory in a piped non---html mode must degrade to a static listing or a reported error, not a crash.full (6d86054e)gallery.md GAL1; tree-view.md TVU1; dir detection in app.main
SRC5A directory target must resolve to an ordered, filtered file list — sorted by path, restricted to renderable files: *.twoslash.json under --twoslash, else any file with an extension that is not obviously binary. The filter must be a deny-list, since hue renders unknown text as plain text (DEG2) and canonicalLanguage normalizes a label without deciding membership — an allow-list would drop the .toml/.txt files a directory legitimately holds. Extensionless files, recursive descent and .gitignore awareness are deferred to tree-view.md TVU1.full (77a476f3)source_set.collectSources/isRenderable
SRC6That file list must be the one multi-document session every rendering mode consumes — the HTML gallery iterates it, and the interactive backends navigate it (GAL3) — so a document set is acquired once and rendered many ways. It is the substrate the tab view (TBU1) and navigation (LNK3) later build on, not a parallel mechanism.full (6d86054e)source_set.SourceSet — the gallery iterates it, the GUI/TUI navigate it
SRC7Diff-pair acquisition: a diff target acquires pairs of sources through this same layer — two files (DVS1), a stdin patch plus worktree re-reads (DVS2), git show <rev>:<path> old sides (DVS3) — so source acquisition stays one subsystem. A rename pairs sources whose paths (and so LNG1 languages) differ; the language is resolved per side.full (a2b634e8)all three acquisition shapes ship (DVS1DVS3); DiffSides carries the per-side language
SRC8Forge acquisition: a PR/MR is an acquirable session source — a network fetch through the forge seam (DPR7) sharing SRC3's URL-fetch machinery once that lands, producing a SRC6-style multi-document session (DVS4).not starteddiff-view.md DPR1/DPR7; SRC3

Concatenation & stdin (CAT)

cat's own semantics, which hue does not have today: many inputs are one stream, and - is one of them. The multi-document machinery this needs already exists — SourceSet (SRC5/SRC6) — so concatenation is a rendering policy over an existing session, not a second acquisition path.

IDRequirementStatusTraces to
CAT1Several positional file targets must form one SourceSet (SRC6) rendered as a single continuous stream in the non-interactive sinks — a header per document (header-filename) and a rule between them (chrome.md STY1) — while the interactive sinks open that same set as a navigable session.not startedsource_set.SourceSet; the ANSI/HTML sink loops
CAT2A bare - target must mean stdin, mixable with paths (hue a.d - b.d), and must be readable exactly once. With no target and a non-tty stdin, hue must read stdin — which it already does; - makes that explicit and composable.partialreadStdinText (bare stdin ships; - does not)
CAT3--file-name <name> must name a stdin or URL input for the header (CHW4) and as the language-detection input (LNG2), so cmd | hue --file-name x.py highlights as Python.not startedCliParams.fileName; LNG2 cascade
CAT4-E/--quiet-empty must produce no output at all for empty input, so git diff | hue -E is silent when there is nothing to show.not startedCliParams.quietEmpty
CAT5A directory target must keep its SRC4 meaning (explorer / HTML gallery) rather than being concatenated; concatenation is a property of listing files explicitly.full (6d86054e)dir detection in app.main (already distinct)

Line ranges & selection (RNG)

Which lines of a document are visible, and which are emphasized. hue can show only whole files today, which is what keeps it out of fzf/ripgrep preview panes and out of compiler-error workflows.

The design decision worth stating: a range is one LineSelection value on the Document, applied once in DocumentPipeline and honoured by every sink — not a truncation each sink performs. That is what lets --changes-only (chrome.md CHG4) be a derived selection rather than a mode.

IDRequirementStatusTraces to
RNG1--line-range must accept N, N:M, :M, N:, -N: (the last N lines) and N:+K, plus a trailing context field (N::C, N:M:C) that widens the range by C lines on each side. Repeating the option must union the ranges. The parser must be pure and independently tested.not startedproposed line_range.d parseRange
RNG2The resolved ranges must become one LineSelection carried on the Document and applied once by DocumentPipeline, so every sink — ANSI, HTML, TUI, GUI — shows the same lines without its own truncation logic.not starteddocument.d; viewer_model.d
RNG3--highlight-line must take the same grammar and paint a highlight background over those lines, from a theme slot, in every sink. It must compose with --line-range rather than imply it.not startedline_range.d; theme highlight slot
RNG4A path:line[:col] target must be accepted as a target spelling — hue app.d:42, the shape rg, grep -n and compiler diagnostics emit. In a non-interactive sink it must behave as --line-range 42::<context>; in an interactive sink it must open scrolled to that line with the whole document still reachable, not truncated.not startedtarget parsing in app.main; RNG2
RNG5Where a selection elides lines, the sinks must draw a snip separator stating how many lines were skipped (chrome.md CHW6), so a partial view never reads as a whole file.not startedchrome.md CHW6
RNG6Line numbers must always be physical source line numbers, unaffected by the selection — the invariant HTM4 already states for the HTML gutter, generalized to every sink.partialHTM4 (HTML only today)

Text normalization & safety (TXT)

What happens to the bytes between reading them and highlighting them. hue does almost none of this today, and one row here is a genuine hazard rather than a missing convenience: hue renders ANSI escapes on purpose (```ansi fences, ANS2), so it currently hands control sequences from an arbitrary file straight to the terminal. Any viewer that might be pointed at untrusted content — a file-manager preview pane, a downloaded diff, a fetched URL (SRC3) — needs a way not to.

The layering follows the answer taken for this cluster: the transforms are pure primitives in sparkles:base.text, and their order is owned by one DocumentPipeline stage. Primitives so ci, terminal and future consumers get them; one stage so the order is written down exactly once, which is where this kind of pipeline usually rots.

IDRequirementStatusTraces to
TXT1Normalization must be one ordered stage in DocumentPipeline, ahead of highlighting, over @safe pure nothrow @nogc primitives in sparkles:base.text. The order is fixed and stated once: decode → sanitize/strip → non-printable → tab expansion → squeeze. No sink may normalize on its own.not startedproposed sparkles.base.text.normalize; document.DocumentPipeline
TXT2--sanitize=auto|always|never must substitute U+FFFD for terminal-active control bytes (cursor motion, charset switches, OSC, bell) and for Unicode bidi-override and zero-width formatting characters, leaving tab, LF, FF and CRLF intact. auto is the default and sanitizes everywhere except regions hue is deliberately interpreting as ANSI```ansi fences (ANS2) and a pre-formatted document (pager.md PIN5). (bat defaults to never; hue can afford auto because it knows which regions are intentional, and should, because it is a viewer for content it did not produce.)not startedsparkles.base.text.sanitize; ContentKind gating
TXT3--strip-ansi=auto|always|never must remove SGR sequences without the rest of TXT2's substitution — the weaker sibling, for input that is merely pre-colored rather than hostile. auto strips outside the same intentional-ANSI regions.not startedsparkles.base.text.stripSgr (exists, to be generalized)
TXT4-A/--show-all with --nonprintable-notation=unicode|caret must render non-printable characters visibly (/^J, · for space, a ruled tab). hue's existing --list-whitespace must become the vim-flavoured preset of the same mechanism rather than a parallel one.partialCliParams.listWhitespace; sparkles.base.text encoder
TXT5Binary content must be detected (a NUL in the leading block) and refused with a one-line notice naming the file and its size, rather than emitted; --binary=as-text must override. A hex dump view is explicitly out of scope.not startedisBinary; the sink guard
TXT6A UTF-16 BOM must be transcoded to UTF-8 and a UTF-8 BOM dropped; invalid UTF-8 must be replaced with U+FFFD rather than throwing or truncating the document. Charset guessing for un-BOM'd input is out of scope.not startedsparkles.base.text decoder
TXT7-s/--squeeze-blank must collapse consecutive blank lines to one, and --squeeze-limit <N> must set the kept count. Squeezed lines must not disturb physical line numbering (RNG6).not startedsparkles.base.text.squeeze; RNG6
TXT8--tabs <T> must set tab stops with 0 meaning "pass through unexpanded", and expansion must be escape-aware — an SGR sequence occupies no columns, so a colored line's tab stops must not drift. hue's --tab-width becomes an alias.partialCliParams.tabWidth; expandTabs

Language detection (LNG)

LNG1 is the whole of hue's language detection today: one lookup from the file extension. The rows below turn it into an ordered resolution cascade, which belongs in sparkles:syntax rather than in hue — twoslash-extract and the web integration want the same answer to "what language is this?", and there should be one of them.

IDRequirementStatusTraces to
LNG1The grammar language must be derived from the file extension, canonicalized through sparkles:syntax aliases.full (74d8f6a3)canonicalLanguage(extension)
LNG2Language resolution must be an ordered cascade, evaluated in sparkles:syntax and shared by every consumer: explicit --language--map-syntax glob → full file name → extension (LNG1) → first-line sniff (LNG5) → --fallback-syntax → plain text (DEG2). The first step that answers wins.not startedproposed sparkles:syntax resolveLanguage
LNG3-l/--language must accept either a language name or an extension (-l cpp and -l C++ alike), resolved through the same alias table as LNG1.not startedCliParams.language; canonicalLanguage
LNG4-m/--map-syntax <glob>:<lang> (repeatable) must map a glob to a language, matched against both the full path and the base name, with later rules losing to earlier ones. The matcher must be sparkles:build-primitives' existing glob matcher, not a new one.not startedsparkles:build-primitives gitignore.d matcher
LNG5First-line detection must cover the cases that pay for themselves — a #! shebang (including env-dispatched interpreters), a vim or emacs modeline, and an XML/HTML declaration — and nothing more. A full content-classification cascade stays deferred (DEF6).not startedproposed firstLineLanguage; DEF6
LNG6--ignored-suffix <suffix> (repeatable) must be stripped before extension lookup, so config.json.dev highlights as JSON. A small built-in list (.in, .orig, .bak, .dist, .tmpl) must apply by default.not startedLNG2 cascade; CliParams.ignoredSuffix
LNG7-L/--list-languages must print the resolvable languages with their extensions and note which have a grammar available in the current environment (ENG2) — so a missing $SPARKLES_TS_GRAMMAR_PATH is diagnosable rather than mysterious.not startedGrammarRegistry; ENG2/DEG4

Highlight engine (ENG)

hue drives the sparkles:syntax precise pipeline; the engine internals are specified in docs/specs/syntax.

IDRequirementStatusTraces to
ENG1Highlighting must use the injection-aware path so markdown (and other languages with injections.scm) get fenced/inline content highlighted by nested grammars.full (9b0a4b50)highlightInjected(cache, lang, source, events)
ENG2Grammars must be loaded from the nix bundle via $SPARKLES_TS_GRAMMAR_PATH; a GrammarRegistry/TsConfigCache is built once.full (74d8f6a3)GrammarRegistry.fromEnvironment; TsConfigCache.create
ENG3Highlighting must produce a HighlightEvent stream over the source, consumed identically by every rendering mode.full (74d8f6a3)SmallBuffer!HighlightEvent events
ENG4On any engine failure (no grammar, parse error) hue must fall back to a single plain-text span covering the whole source.full (74d8f6a3)res.hasErrorHighlightEvent.sourceSpan

Themes (THM)

IDRequirementStatusTraces to
THM1The named theme must be resolved from builtinThemes; an unknown name must warn and fall back to builtinDark.full (74d8f6a3)builtinThemes.get(themeName, …)
THM2The full sorted built-in theme set must be materialized once (names + parallel Theme values) for the live previewer and GUI.full (74d8f6a3)names/themes in app.main
THM3A theme must be resolved against the standard LabelSet before rendering (ResolvedTheme).full (74d8f6a3)resolveTheme(theme, labels)

hue ships 36 built-in themes and picks one fixed default, which means a user on a light terminal gets a dark theme painted over it. The rows below add the light/dark axis to hue's surface — the flags, the pair, the listing.

They deliberately specify no detection of their own. Answering "what does the user want?" is the subject of the platform-UI-guidelines research, whose proposal is a leaf library, sparkles:appearance, with a compile-time source cascade (explicit config → SPARKLES_APPEARANCE → platform source → terminal source → compiled default) and a deriveTheme step. That work is strictly better-informed than anything this area should invent — it uses mode 2031 with an OSC 11 fallback rather than OSC 11 alone, it routes the reply through sparkles:input instead of reading stdin behind the event loop's back, and it argues correctly that a D-Bus client has no place in sparkles:base. hue is therefore a consumer of that library, and these rows say only what hue does with the answer.

IDRequirementStatusTraces to
THM4--theme must additionally accept auto (the new default), dark and light; --theme-dark <name> and --theme-light <name> must name the pair auto chooses between. An explicitly named theme keeps overriding everything — which is level 1 of the appearance cascade, not a hue-local rule.not startedCliParams.theme/themeDark/themeLight; the cascade's level 1
THM5Under --theme auto, the scheme must come from sparkles:appearance and hue must add no detection of its own — no OSC query issued from hue, no $COLORFGBG read in app.d. Until that library exists, auto must resolve to the compiled default, so the flag can ship ahead of the detection without ever growing a throwaway probe.not startedplatform-ui-guidelines milestones P0/P1
THM6A Theme must declare its variant (dark or light) in sparkles:syntax, so the built-in set partitions itself and --theme-dark/--theme-light can validate their arguments and default sensibly rather than by a hardcoded name list. This is theme metadata, distinct from and complementary to deriveTheme, which computes a theme rather than labels one.not startedsparkles:syntax Theme.variant; builtinThemes
THM7Every sink must take its scheme from the same cascade, so a terminal launch follows the terminal and a window launch follows the desktop without hue branching on the backend. SPARKLES_APPEARANCE must override both, so golden captures stay deterministic exactly as the HUE_GUI_* hooks already do (CFG2).not startedthe cascade's levels 2–4; config.md CFG2
THM8--list-themes must print the built-in themes grouped by variant (THM6), each with a short highlighted sample rendered in that theme, so the list is choosable rather than merely enumerable.not startedbuiltinThemes; the ANSI sink

NOTE

$HUE_THEME, $HUE_THEME_DARK and $HUE_THEME_LIGHT are not specified here. Environment overrides are one layer of the general configuration precedence (config.md CFG2), not a per-setting mechanism — which is the one place hue should not copy bat, whose theme, style, pager, width and config path each grew their own BAT_* variable.

Color depth & policy (CLR)

IDRequirementStatusTraces to
CLR1ANSI output (non-interactive and previewer) must adapt to the terminal's detected color depth.full (74d8f6a3)detectColorDepth()

Whether to colorize at all is currently implied by the sink. These rows make it a policy, which is what a program in a pipeline needs.

IDRequirementStatusTraces to
CLR2--color=auto|never|always must gate colorization independently of the sink: auto (default) colors iff stdout is a tty, always survives a pipe (the hue | less -R case), never emits plain text with decorations intact.not startedCliParams.color; AnsiOptions
CLR3A non-empty $NO_COLOR must select --color=never, and an explicit --color must override it. It must suppress color only — decorations (STY4) are a separate axis, so NO_COLOR still leaves line numbers and headers.not startedCLR2 resolution
CLR4--italic-text=auto|always|never must gate ANSI italics, which the whole-file emit hardcodes on today (ANS1) and many terminals render as inverse video.not startedCliParams.italicText; renderAnsi options
CLR5True-color availability must be detected once in term_caps ($COLORTERM and the terminfo answer) and consumed by CLR1, rather than probed per sink.partialdetectColorDepth(); term_caps

Output-mode dispatch (MOD)

Exactly one mode runs per invocation; see the mode map.

NOTE

These requirements are scheduled to be reshaped, not merely re-implemented. hue is moving to a single behavior with three backend flavors: the target (file · directory · URL) selects a content kind, the flags select a backend, and content kinds compose — a markdown document may embed a twoslash block, whose documentation popups render through the same markdown view. --markdown, --raw and --twoslash therefore stop selecting code paths and become content kinds and overlays (OVL4), and a directory target opens the file explorer (TVU1) rather than a bespoke index view. The dispatch collapse shipped (a75f1fc9): document.d owns one Document value with a content-detected kind (code / markdown / twoslash — a *.twoslash.json target needs no flag), one DocumentPipeline.load replaces the four load-pipeline copies, and app.main picks a backend once (pickBackend) then dispatches to one of four sinks, each a final switch over the kind. --twoslash/--markdown/--raw are now detection inputs (--overlay twoslash=… is the OVL4 spelling). Content-kind composition (a markdown document embedding a twoslash block) is the remaining piece — see ui-architecture.mdUIA6 and pipeline.md XFM3.

IDRequirementStatusTraces to
MOD1--gui (explicit or autodetected) on a GUI-enabled build must dispatch to the raylib window and return its exit code.full (e6063309)if (cli.gui) version(HueGui)
MOD2--gui on a no-gui build must print a rebuild hint to stderr and exit non-zero.full (e6063309)else stderr.writeln(…); return 1
MOD3Non-interactive (stdout not a tty) must emit the whole file once (HTML if --html, else ANSI) and exit.full (74d8f6a3)!interactive branch
MOD4An interactive tty (no --html) must open the live terminal previewer.full (74d8f6a3)interactive branch → runLoop
MOD5If a raw-key session can't be acquired in an otherwise-interactive tty, hue must degrade to emitting the whole file as ANSI.full (74d8f6a3)sessFactory is nullemitAnsiWholeFile
MOD6With no explicit mode flag (--gui/--no-gui/--html) on a GUI-enabled build, hue must default to the GUI window when a graphical display is available (Linux/BSD: $DISPLAY or $WAYLAND_DISPLAY set; macOS/Windows: a local, non-SSH session) and stdout is a tty, and otherwise fall through to the terminal dispatch (MOD3MOD5). On a no-gui build it always falls through.full (cdc813f6)displayAvailable; wantGui in app.main
MOD7The mode flags must override autodetection: --no-gui/--tui forces the terminal path even with a display present; --gui forces the window even with no display detected (raylib surfaces any failure).full (cdc813f6)wantGui flag precedence in app.main
MOD8A markdown file must render the render-markdown decorated preview by default in every sink — GUI (MDP), interactive TUI (MDP-T), non-interactive ANSI (ANS3), and HTML (HTM5) — reusing the shared MdDoc model and the one viewMarkdown widget view. --raw (CLI9) forces the highlighted-source view.full (241e8052)the ContentKind dispatch across sinks; buildPreviewModel + viewMarkdown
MOD9A diff or PR session is a content kind like code/markdown/twoslash (the dispatch-collapse doctrine): produced once by the pipeline, dispatched through the same backend pick, and rendered by every sink (DVL4) — no new modes.full (8b4f9797)ContentKind.diff dispatched by every sink (ANSI/HTML arms; TUI/GUI via the Document transport + ViewerModel)
MOD10A DSV document (CSV/TSV/…) is a content kind like the others: detected once (extension or content sniff), dispatched through the same backend pick, and rendered as the grid preview by default in every sink (DSK1/DSK3); --raw (CLI9) forces highlighted source.not starteddsv-preview.md DSK*

ANSI terminal output (ANS)

IDRequirementStatusTraces to
ANS1Non-interactive/piped output must render the whole file to ANSI with italics and background emission enabled.full (74d8f6a3)emitAnsiWholeFile; renderAnsi
ANS2```ansi fenced blocks embedded in a doc are passed through as literal SGR (the tty renders them) in ANSI mode.full (74d8f6a3)(renderer pass-through)
ANS3For a markdown file the non-interactive ANSI emit must render the decorated preview by default — the shared viewMarkdown widget view painted to SGR cells via CellGrid.writeAnsi — not highlighted source; box-drawing / quote / rule glyphs emit natively and ```ansi fences pass through (ANS2). --raw (CLI9) reverts to ANS1.full (c3a5b2af)the ANSI sink's widget path in app.d
ANS4A diff session in non-interactive ANSI must emit the unified layout whole — the pager use-case (git diff | hue) — composing with the background modes (BGM*) like any theme background.full (31fdab59)runAnsiSink case diff:; verified via git diff | hue

Background mode (BGM)

How the theme background is applied in terminal rendering — the whole-file ANSI emit and the interactive previewer — selected by --background (CLI8). The three modes; full is the default (the prior fixed behaviour was spans).

IDRequirementStatusTraces to
BGM1no-background — emit foreground colors only; the terminal's own background shows through (the theme background is ignored). Useful for piped output over a themed terminal.full (d404dc8c)AnsiOptions.emitBackground: false; previewer chrome bg + backdrop gated
BGM2spans — emit a background only where the theme sets a span background (today's fixed whole-file behaviour, AnsiOptions(emitBackground: true)); selectable via the flag.full (d404dc8c)emitAnsiWholeFile; backgroundOptions
BGM3full (the new default) — fill every line with the theme's default background edge-to-edge, matching the previewer's back-color-erase look (PRV7). Fills each line via sparkles:syntax renderAnsi's fillLine.full (d404dc8c)renderAnsi fillLine (3e3d9cb4); backgroundOptions; previewer PRV7

Document chrome (CHR)

The decorations drawn around the content — header, grid, rule, snip, the line-number gutter and the git change column — selected by one composable --style set. Specified in chrome.md; the rows here are the app-wide contract.

The design decision behind the sub-spec: hue paints a line-number gutter three times today (GUI NUM, TUI TSL, HTML HTM4), and adding a second column would mean writing it three more times. bat's Decoration trait — ordered columns, each a width plus a per-line producer — is the right model in the wrong medium, since it can only ever emit ANSI. hue takes the model and implements it as sparkles:ui chrome widgets, so one --style set renders in all four sinks.

IDRequirementStatusTraces to
CHR1--style must select from changes, numbers, grid, rule, snip, header-filename, header-filesize plus the default/full/plain/auto presets, with +/- prefixes modifying the set rather than replacing it, and --decorations gating the whole set independently of the sink.not startedchrome.md STY1STY5
CHR2The components must be sparkles:ui widgets, not per-sink painters, so the same set renders in ANSI, HTML, TUI and GUI — and each backend's private gutter is deleted as it migrates onto them.not startedchrome.md CHW1CHW7
CHR3Chrome must never be selectable or copyable in any sink, generalizing the HTML-only rule HTM3 states today.not startedchrome.md CHW3; HTM3
CHR4The changes component must mark lines added / modified / removed against the git index, computed by sparkles:diff through the existing git show acquisition (DVS3) — hue must not grow a second diff engine — off the first-paint path, degrading to an empty column.not startedchrome.md CHG1CHG3
CHR5--changes-only[=N] must express itself as a derived LineSelection (RNG2) rather than as a mode, so it composes with --line-range and shares its snip separators.not startedchrome.md CHG4; RNG2

Width & wrapping (WID)

The wrapping policy; the wrapped-line render models themselves are WRP in the window and TSF in the terminal.

IDRequirementStatusTraces to
WID1--wrap=auto|never|character|word must select the wrapping mode in every sink. auto (default) is word wrapping when a width is known, and no wrapping when it is not.not startedCliParams.wrap; gui_text.d wrap model
WID2-S/--chop-long-lines must alias --wrap=never. In the non-interactive sinks that truncates at the width; in the interactive sinks it must instead enable horizontal scrolling, since a viewer that can scroll should not discard the text.not startedWID1; viewer_model.d
WID3--terminal-width <width> must set the assumed width, accepting +N/-N as offsets from the detected width. With no tty and no flag the width must be 80. Detection stays term_caps' answer (ENG-adjacent, one place).not startedCliParams.terminalWidth; term_caps

Paging, pre-formatted input & streaming (PGR)

Specified in pager.md; the rows here are the app-wide contract.

The design decision behind the sub-spec: bat shells out to less, sniffing its version to decide which flags are safe. hue already has what bat is shelling out to acquire — a full-screen terminal viewer with scrolling, search, mouse and copy — so paging is a fifth input to the existing backend pick (MOD6), not a subprocess. The work that is genuinely new is not paging but rendering input that arrives already formatted, which is what $MANPAGER and git core.pager require, and which ansi_model.d already knows how to decode.

IDRequirementStatusTraces to
PGR1--paging=auto|never|always (-P = never) must decide whether a terminal launch pages; under auto hue pages iff stdout is a tty and the laid-out render exceeds the viewport — short files feel like cat, long ones like less.not startedpager.md PAG1/PAG2
PGR2Paging must mean hue's own TUI, not a spawned process. --pager=<cmd> / $HUE_PAGER / $PAGER must remain available as an escape hatch, and under any pager hue must behave as --color=always --decorations=always.not startedpager.md PAG3PAG5
PGR3hue must be usable as $PAGER, $MANPAGER and git config core.pager, with the three recipes in the README — which requires only PGR4, CAT2, STY5 and PGR2.not startedpager.md PAG7
PGR4A pre-formatted content kind must exist beside code / markdown / twoslash / diff — input that is already styled, rendered by decoding rather than highlighting, through the existing ansi_model.d VT plus a man-style backspace-overstrike pass. Sniffed, forced by --preformatted, overridden by -l.not startedpager.md PIN1PIN4; MOD9
PGR5A pre-formatted document must be exempt from TXT2/TXT3 — those bytes are being interpreted on purpose — while every other content kind stays subject to them.not startedpager.md PIN5; TXT2
PGR6-u/--unbuffered must render incrementally instead of waiting for EOF, and the interactive sinks must offer a less +F-style follow mode released by any navigation key. Appended input rides the event loop's pollAdd, never a timer.not startedpager.md STR1STR3
PGR7Highlighting under streaming may be imperfect at the tail while a construct is incomplete; line numbers, wrapping and search must stay correct regardless. Incremental reparse remains a sparkles:syntax roadmap item, not a hue one.not startedpager.md STR4

HTML output (HTM)

IDRequirementStatusTraces to
HTM1--html must emit a self-contained <style> + <pre class="syn-root"><code> document with CSS-class highlighting.full (74d8f6a3)HTML branch; renderHtml(cssClasses)
HTM2The emitted stylesheet must carry the theme's default fg/bg on .syn-root (no duplicate pre{} color rule).full (74d8f6a3)writeThemeStylesheet

The GUI's interactive features apply best-effort to HTML output too — HTML selection is the browser's, so parity means emitting markup that copies cleanly and (optionally) a preview/gutter:

IDRequirementStatusTraces to
HTM3Best-effort SEL parity: the markup must be selectable so a browser copy yields the original source; any gutter / line-number / decoration content must be non-selectable (user-select: none) so it is excluded.partialplain <pre><code> copies to source today; the twoslash overlay realizes the decoration-excluded half on branch (TWH6TWH8); a plain---html gutter awaits HTM4
HTM4Best-effort NUM parity: HTML output must render an optional line-number gutter numbering physical (source) lines — each line wrapped in a .ln span whose number is a CSS ::before counter, so it is never selected or copied (HTM3); below-line overlay annotations must carry no number and must not advance the counter, and blank lines must keep their height and survive a copied selection.full (b61f4701)gallery.relayoutGutter; gallery.md GAL4
HTM5MDP parity: for a markdown file --html must render a rich HTML preview by default via the shared sparkles:syntax MdDoc → HTML emitter (renderMarkdownHtml) — with theme-derived CSS (writeThemeStylesheet), syntax-highlighted fences (a fenceRenderer hook reusing renderHtml), GitHub callouts, and column-aligned tables — not highlighted source. --raw (CLI9) emits highlighted source (HTM1).full (af6ebe95)renderMarkdownHtml (exists; enrichment + default wiring pending); app.main HTML markdown branch
HTM6A directory target with --html must render a static gallery into --out (CLI12): one standalone page per file in the set (SRC5) plus an index.html linking every page with its name and summary. Single-file --html (stdout fragment) must be unchanged.full (b61f4701)gallery.writeGallery; gallery.md GAL1/GAL2
HTM7Each gallery page must wrap hue's content fragment in a page shell: a header (name · summary · prev/next/index nav), a full-height single scroll container (exactly one scrollbar, no nested body+pre scrollbars), and a page background matched to the theme's .syn-root background.full (b61f4701)gallery.pageShell; gallery.md GAL2/GAL3/GAL6
HTM8Gallery pages must implement selection domains: a drag must be confined to the domain it starts in — the code, or one overlay annotation — so a copy never mixes the two. This layers on the pure-CSS code-only default (TWH6) and is hue's own output, not a consumer's responsibility.full (b61f4701)gallery.pageShell (selection CSS/JS); gallery.md GAL7; supersedes the JS harness (TWD3)
HTM9A diff/PR session with --html must render the static review site — the gallery analog: an index of changed files with per-file ± stats, one diff page per file with prev/next navigation, the dual old/new gutter (HTM4 generalized), and split-pane selection domains (HTM8).not starteddiff-view.md DVL4; gallery.md GAL10

Interactive terminal previewer (PRV)

IMPORTANT

Retired (D6, M9): previewer.d is deleted. Everything it did is subsumed by the full TUI (tui.md) — live theme cycling is ←/→ there — and the non-Posix tty path degrades to the whole-file ANSI emit (sparkles:tui's input reader is Posix-only). The rows below are kept as the historical record of what the TUI absorbed; none traces to live code.

previewer.d — the live theme browser (the baseline the full TUI (tui.md) extends). Its render/output core was @nogc nothrow.

IDRequirementStatusTraces to
PRV1The previewer must repaint a full frame, flush, read one key, and repeat until quit/select.full (74d8f6a3)runLoop
PRV2↑/↓ must cycle the selected theme (wrapping) and repaint live.full (74d8f6a3)runLoop Key.up/Key.down
PRV3Enter must select the theme: leave the alt screen and print the whole file highlighted in that theme onto the primary screen.full (d1c4e159)LoopResult.selected; renderFull
PRV4Any other key / cancel must quit and print nothing.full (74d8f6a3)Key.cancel/Key.other
PRV5A frame must show a header (title · theme name · index), a hint line, separators, the highlighted viewport slice, and a scrolling theme-list window around the selection.full (844680a3)Previewer.buildFrame
PRV6The viewport must show only the top maxCode lines that fit (height-derived), not the whole file, keeping the fold O(visible).full (fd22c112)firstLines; buildFrame maxCode
PRV7The theme backdrop must fill the viewport via back-color-erase (open theme bg, then erase), with begin/end synchronized-output markers.full (844680a3)CtlSeq.syncBegin/eraseDisplay/syncEnd
PRV8The alt screen must be entered on start and restored (show cursor, exit alt screen) on exit; its contents are discarded.full (74d8f6a3)enterAltScreen/exitAltScreen

File monitoring (WCH)

hue is a viewer, so the one file worth following is the open document — the explorer's refresh stays manual by decision (XPF4). Substrate: sparkles:event-horizon's async inotify watcher (watch.d, its PLAN M7), whose intended consumer this is.

IDRequirementStatusTraces to
WCH1The open document must be watched for on-disk changes: the watch arms on the document's directory (editors save by rename — the file's own inode dies) and filters by name; write-close and rename-in trigger. One watcher fiber parked on the ring, woken through the loop's channel.partial (de29178e) — the TUI's async arm; the GUI loop and the kqueue mapping (event-horizon M10) are openworkspace.d watch fiber; event-horizon watch.d
WCH2A triggered reload re-reads in place: the same loader, the viewport preserved, highlighting and the retained parse rebuilt, the inspector refreshed — its extent tint clears (an old byte extent is meaningless against a changed file; the next hover re-syncs).full (de29178e) for the watched armWorkspaceTui.reloadCurrent
WCH3Incremental re-parse: watching yields whole-file snapshots, not edit deltas — synthesize ts_tree_edit edits by diffing old/new bytes (sparkles:diff) and re-parse with the old tree, so a large document's reload costs its change, not its size.researchedts_tree_edit; sparkles:diff byte diff
WCH4Degradation is structural, never an error: the blocking loop arm (no ring), non-Linux (until M10), and an embedded path-less document simply never arm a watch; navigation still re-reads on open.full (de29178e)version (linux) gates; the async-arm-only wiring

Degradation & diagnostics (DEG)

IDRequirementStatusTraces to
DEG1Only degradation warnings are logged (logger level = warning); normal operation is silent.full (d87397b3)initLogger(LogLevel.warning)
DEG2A missing grammar must warn (no grammar for '<lang>') and render plain text, not fail.full (74d8f6a3)warning(i"no grammar …")
DEG3An unknown --theme must warn and use the default dark theme.full (74d8f6a3)builtinThemes.get fallback
DEG4Without $SPARKLES_TS_GRAMMAR_PATH, hue must still run — degrading to plain text for grammar-requiring languages.full (74d8f6a3)GrammarRegistry.fromEnvironment

Build and packaging (BLD)

How hue is compiled and shipped. The GUI backend is included in the default build; a raylib-/ghostty-free variant stays available for terminal-only use and minimal dependency closures.

IDRequirementStatusTraces to
BLD1The default dub configuration (application) must compile the GUI backend in (pulling sparkles:raylib-text + sparkles:ghostty), so a stock dub build :hue and the installed binary have --gui and the markdown preview with no special build.full (29bf1a65)apps/hue/dub.sdl configuration "application"
BLD2A no-gui dub configuration must build the raylib-/ghostty-free variant (the pre-flip application behaviour) for terminal-only installs and minimal closures.full (29bf1a65)apps/hue/dub.sdl configuration "no-gui"
BLD3The nix packages.hue must build the GUI-enabled default; a packages.hue-no-gui must build the headless variant with no raylib/GL in its closure. (GUI-enabled packages.hue done; the separate hue-no-gui package pending.)partial (29bf1a65)nix/packages/hue.nix
BLD4dub test :hue must stay headless — linking no window/GL — regardless of the default flip; gui.d stays excluded from the unittest config.full (e6063309)apps/hue/dub.sdl configuration "unittest"

Non-functional (NFR)

IDRequirementStatusTraces to
NFR1Startup (file read, parse, theme-list build) may allocate on the GC; the previewer's per-frame render/output core must be @nogc nothrow (a theme switch triggers no GC pause).full (0657c94a)Previewer @nogc methods; TermOut
NFR2Each previewer repaint must assemble into one reused SmallBuffer and flush with a single write; the resolved theme table is reused and only rebuilt on change.full (0657c94a)Previewer.frame/styleBuf; themeView
NFR3A raylib-/ghostty-free build of hue must remain available (the no-gui config, BLD2); the GUI + preview modules compile into the default application build and the unittest build (minus gui.d), and are excluded by no-gui.full (29bf1a65)apps/hue/dub.sdl no-gui excludedSourceFiles
NFR4GUI/TUI startup (process launch → first paint) should be fast: a provisional target of ≤ 100 ms (optimal ≤ 50 ms). TODO: calibrate against the window-system-integration research and empirical measurement.researched/not-startedprovisional target; no benchmark gate yet
NFR5GUI window resize must stay within the frame budget — the reflow (WIN3/WRP4 relayout on a width change) must not cause noticeable stutter.not startedrelayout; gui.md WIN3/WRP4
NFR6Large files must scroll instantly — a provisional target of up to 1 MB / 20k lines with no per-frame cost growth. Viewport culling (RND1) already makes scroll O(visible); load/relayout cost is the open question. TODO: calibrate against third-party editors + benchmarks.partialgui.md RND1 (culled scroll); load/relayout unbenchmarked
NFR7All logging must route through sparkles.base.logger; raylib's TraceLog output must be encapsulated (a SetTraceLogCallback bridge) and emitted at trace level, so it is silent under hue's default warning level (DEG1).full (c497d99d)gui.raylibTraceLog installed via SetTraceLogCallback before InitWindow; hue logging via sparkles.base.logger (DEG1/DEG2)
NFR8Diff interactivity targets: live re-diff under inline editing (DST5) must fit the debounce budget (provisional ≤ 50 ms for typical files); large-PR sessions (hundreds of files) must stay viewport-culled (DVG5) with lazy per-file diff computation — a file's diff/refinement is computed when it first scrolls into view or is opened, not up front.not starteddiff-view.md DVG5/DST5/DVM6

Deferred, researched & branch-only (DEF)

Roadmap items — planned/researched features, and modes that exist on another branch. Library-engine roadmap (TextMate second engine, locals, injection combined, incremental editor loop, UTF-16 sources) lives in the sparkles:syntax spec; the rows below are the ones that surface as hue capabilities.

IDRequirementStatusTraces to
DEF1--twoslash <nodes.json> mode (ANSI default / --html / --gui) — D-native Twoslash rendering.planned/branch-onlytwoslash.md TWM1TWM3
DEF2--markdown <file.md> mode — treat the input as Markdown and render the preview. Shipped as a standalone HTML emit on main (runMarkdownMode); being folded into the unified preview-by-default HTML path (HTM5) with theme CSS + highlighted fences.full (af6ebe95)app.runMarkdownMode; HTM5
DEF3The MdDoc → HTML emitter (sparkles:syntax md/render_html.d) that the --markdown/twoslash-docs paths need.fulllibs/syntax md/render_html.d; syntax spec J1
DEF4Runtime theme-file parsing (load user themes: native JSON, TextMate/VSCode JSON, Helix TOML) so --theme can name a file, not just a built-in.researched/not-startedsyntax spec D6
DEF5CSS-variable multi-theme HTML output mode (one document, theme switched via :root[data-theme] / prefers-color-scheme).researched/not-startedsyntax spec F6; render/html.d
DEF6Content-based language detection (a Linguist-style cascade) — today the language is only the file extension / fence label.not startedsyntax spec §deferred (canonicalLanguage only)
DEF7A grapheme/east-asian width table so wide/CJK/combining/tab characters occupy their true cell count (replacing the v1 one-column-per-codepoint metric).researched/not-startedgui.md FNT6
DEF8Color-emoji rendering in the GUI (a separate rasterizer for CBDT/COLR) — raylib/stb_truetype cannot.not startedgui.md FNT7
DEF9Pluggable overlays — a backend-agnostic overlay seam generalized from twoslash, plus new overlay kinds (source map, coverage, tracing, tree-sitter inspector, code size).researched/not-startedoverlays.md OVL*
DEF10Notifier / interactive popups — a cross-backend (GUI/TUI/HTML) snacks.nvim-style popup component (collapse-to-icon, expand, buttons, expandable items) with startup-info and file-info popups.researched/not-startednotifier.md NTF*/NSI*/NFI*
DEF11Full-screen TUI — grow the minimal theme-selection previewer into a terminal port of the GUI viewer: scrolling, a cell scrollbar, SGR mouse, selection → source offsets + OSC 52 copy, wrapping, line numbers, and the markdown preview (reusing the raylib-free widget views).full (8b1d2aac)tui.md TIN*/TSF*/TSB*/TSL*/MDP-T*/TCP*
DEF12UI component librarysparkles:ui ships: canvas-first, no native widgets, three levels, and immediate/retained/SSG interpreters. Its requirements live in docs/specs/ui; hue's port onto it is UIA4.partialsparkles:ui; ui-architecture.md UIA*
DEF13@sparkles/hue web integration — a JS/TS npm package that swaps Shiki for sparkles:syntax in web frameworks (VitePress/Next/Solid Start) across SSG/SSR (process shell-out first) and a future wasm client-side backend.not startedweb-integration.md PKG*/SHL*/WSM*/FWK*
DEF14Content folding — expand/collapse of code structures, markdown sections/lists, and any tree-sitter CST node (a cross-backend fold-range model + presentation-free fold-state machine, elided from the wrapped-line render).not startedfolding.md FLD*/FSR*
DEF15Tree / DAG view — an interactive tree + DAG component (snacks.nvim-explorer-style) across GUI/TUI/HTML: file explorer, tree-sitter inspector, file outline, git graph, dependency graph; a sparkles:ui widget.not startedtree-view.md TRV*/DAG*/TVU*
DEF16Navigation — link following & go-to: markdown anchors + local-file links, module/import & relative paths, doc-comment ($(REF …)/@see) references, and LSP go-to-definition; intra- and inter-document, cross-backend.not startednavigation.md LNK*/REF*/LNB*
DEF17Images & diagrams — render raster images (![](…)), diagram fences (mermaid/graphviz), and LaTeX math via one media-block mechanism; GUI texture · terminal graphics protocol · HTML <img>/<svg>.not startedmedia.md MDB*/IMG*/DGM*/MTH*
DEF18Tab view — a tab-bar + active-tab-state-machine component across GUI/TUI/HTML; initial use cases: open files as tabs, and VitePress-style code groups.not startedtab-view.md TAB*/TBU*/TBB*
DEF19Transformer pipeline (architecture) — a pluggable parse→transform→compile pipeline (à la unified.js/markdown-it/babel) where highlighting/overlays/folding/navigation/media are transform plugins and the renderers are compilers.researched/not-startedpipeline.md PIP*/PRS*/XFM*/CMP*
DEF20Gallery & multi-document navigation — a directory of files rendered as a static HTML gallery (index + per-file pages with a prev/next header, a physical-line gutter, and selection domains) and navigated interactively in the GUI/TUI. Replaces the branch-only JS preview harness.full (6d86054e)gallery.md GAL*
DEF21Diff, PR & review — render diffs (two files, piped unified patch, git revisions) and pull requests (behind a DbI forge seam: GitHub first; GitLab/Gitea/Forgejo/Codeberg as adapters) across all four sinks: unified + side-by-side layouts over a new sparkles:diff engine, layered formatting-noise handling (word-level refinement, formatting-only hunk classification, structural tree-sitter diff incl. commutative-container equivalence, rendered markdown-preview diff), then a write surface — hunk/line staging, inline editing, content-anchored comments with two-target suggestions, and 3-way conflict viewing/resolution.not starteddiff-view.md DVM*/DVS*/DVL*/DVN*/DVG*/DPR*/DST*/DCM*/CFV*
DEF22Lantern — a which-key-inspired key guide: press a prefix, wait a beat, and a panel lists every key that can follow it. Built on hue's one binding table, which replaced three divergent copies of the keyboard policy and is what makes the keymap enumerable at all (and so is CFG6's prerequisite). Includes the <space> leader map.partial (c19bb926)lantern.md KEY*/LTN*/LMP*
DEF23Picker — a fuzzy finder behind <leader>f / <leader>s / <leader>g / <leader>/: a query constraint language (git:modified src/**/*.rs !mod.rs name), frecency-aware composite ranking, budgeted cancellable searches over the sparkles:event-horizon work-stealing pool, and sources for files, grep, recent, open documents, git, themes, lines and the keymap itself.not startedpicker.md PIK*/PKQ*/PKR*/PKS*/PKL*
DEF24sparkles:fuzzy — the matcher the picker drives, as its own library: a typo-resistant scorer with match positions, the query/constraint parser, the composite ranking formula, exponential-decay frecency, and a glob matcher. 100% @safe pure nothrow @nogc, benchmarked from the first commit.not startedpicker.md PKM*
DEF25User assets — user-supplied grammars and themes discovered from a config directory, with whatever build/cache step tree-sitter grammars need, plus --no-custom-assets. Today grammars come only from the nix bundle (ENG2) and themes only from builtinThemes, which makes a non-nix install of hue unextendable. Subsumes DEF4 (theme-file parsing) as its theme half. Deliberately deferred from the bat-parity scope.not startedENG2; DEF4; bat's bat cache --build as the reference shape
DEF26Input preprocessors — a decoder seam for inputs that are not text: $LESSOPEN/$LESSCLOSE compatibility so lesspipe handles .gz/archives/PDFs out of the box, plus native handlers where hue can do better. Composes with media.md (MDB*), which is the same question asked about embedded content rather than whole inputs. Deliberately deferred.not startedbat's lessopen.rs; media.md MDB*
DEF27Introspection & completions — shell completion generation (which belongs in sparkles:core-cli, so every app in the monorepo gets it from its @CliOption table rather than a hand-written script), a --diagnostic dump for bug reports, and --generate-config-file / --no-config once config.md lands. --list-languages (LNG7), --list-themes (THM8) and --list-overlays are specified in their own areas and are not deferred. Deliberately deferred.not startedsparkles:core-cli args.d/help_formatting.d; config.md CFG*
DEF28DSV preview & data browser — CSV/TSV/PSV rendered as a decorated data grid in every sink over a new sparkles:dsv engine (dialect sniffing, an RFC 4180 identity-channel parser, typed columns), then the interactive browser tier: multi-key sort, a PKQ-style filter bar + header menus with a sparkles:fuzzy full-text remainder, column hide/reorder, DSV-dialect copy, and a 100 MB / 1M-row scale target. Phase 1 ships through the existing md table path; the plan re-orients at a checkpoint (CHK) once the table-rendering unification merges.not starteddsv-preview.md DSK*DSZ*
DEF29Format preview — a toggle-able in-memory reformat of the open file through pluggable formatters (in-process sparkles:dmd-fmt for D, opt-in external shell-outs), driven by a draggable column ruler that sets the soft max line length and reformats live off-thread; read-only, backend-neutral.partial (3247d25e8) — v1 shipped; CFG20 + diff presentation openformat-preview.md FMV*/FPR*/RUL*

Module coverage (general spec)

Every non-GUI source file maps to the requirements above:

SourceKey symbolsRequirements
apps/hue/src/app.dCliParams, main, emitAnsiWholeFile, mode dispatchCLI*, SRC*, LNG1, ENG*, THM*, CLR1, MOD*, ANS*, HTM*, DEG*
apps/hue/src/source_set.dSourceEntry, SourceSet, collectSources, twoslashTally, plainTallySRC4SRC6, gallery.md GAL1
apps/hue/src/gallery.drelayoutGutter, pageShell, galleryIndex, writeGalleryHTM4, HTM6HTM8, gallery.md GAL2/GAL4/GAL6/GAL7
apps/hue/src/previewer.ddeleted (D6, M9) — theme cycling lives in the TUI; BackgroundMode moved to ansi_model.dPRV* (retired)
apps/hue/dub.sdlbuild configurations (application [GUI] / no-gui / unittest)CLI4, MOD1/2, BLD1/2/4, NFR3
nix/packages/hue.nixpackages.hue, packages.hue-no-guiBLD3

Proposed modules for the areas above that have no code yet:

Source (proposed)Key symbolsRequirements
apps/hue/src/line_range.dparseRange, LineSelectionRNG1RNG4, RNG6, CHG4
apps/hue/src/style.dStyleComponents, DecorationPolicyCHR1CHR3, chrome.md
apps/hue/src/git_changes.dLineChange, changeMap over sparkles:diffCHR4, CHR5
apps/hue/src/pager.dPagingMode, shouldPage, spawnPagerPGR1PGR3
apps/hue/src/overstrike.ddecodeOverstrikePGR4
apps/hue/src/stream.dincremental append, follow statePGR6, PGR7
libs/base/.../text/normalize.dsanitize, stripSgr, expandTabs, squeeze, BOM decodingTXT1TXT8
libs/ui/.../components/chrome.dGutterColumn, gutter, header, grid, rule, snipCHR2, chrome.md CHW*
libs/syntax/.../language.dresolveLanguage (the cascade), firstLineLanguageLNG2LNG7
libs/base/.../term_caps.dtrue-color tier and the detected terminal widthCLR5, WID3
sparkles:appearance (proposed)the scheme cascade hue consumes — not hue's to buildTHM5, THM7

GUI requirements · Document chrome · Pager & streaming · Overview