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

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
CLI1hue [options] [target] is a synonym for hue view [options] [target], rendering target — a file path, an HTTP(S) / Forge URL (SRC3), or a directory (SRC4, a multi-document set); with no target when stdin is a terminal, hue opens the current directory (. / SRC2).fullcli.HueCli default subcommand dispatch; executeView in app.d
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, 799266ca).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 (badb5594)gui.md NAV5; parseScrollAnchor
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
CLI29--dark-theme <name> must name the dark half of a two-theme --html run (HTM10), making --theme the light half. Since a pair cannot live in a per-fragment <style> block, it must imply the shared stylesheet (HTM1).full (8044e739)CliParams.darkTheme; usesSharedStylesheet
CLI30--stylesheet <href> must link the given href from the emitted HTML instead of embedding a <style> block (HTM1).full (8044e739)CliParams.stylesheet; app.stylesheetLink
CLI31--emit-stylesheet <file> must write the stylesheet the run's pages need — both themes when --dark-theme is set — to <file>.full (8044e739)CliParams.emitStylesheet; assets.writeStylesheetFile
SRC1With a path argument the whole file must be read into memory as the highlight input.full (74d8f6a3)readText(sourcePath)
SRC2With no path and stdin connected to a terminal, hue opens the current directory (.) by default (CLI1). When stdin is piped or redirected, it highlights stdin or its bundled source.fullapp.d executeView
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. Forge file URLs (GitHub blob/raw, GitLab blob/raw) are parsed and fetched through the forge adapter with credentials discovery (forge.d / forge_github.d / forge_client.d). (This is the user's explicit request — unlike the opt-in auto-loaded remote media of media.md IMG3.)fullforge.parseForgeFileUrl; GitHubForge.fetchFile; forge_client.fetchUrl; DocumentPipeline.readSourceText
SRC4A directory target must open a multi-document session (SRC5) over the files it contains: interactively in TUI and GUI, the file-tree explorer opens focused with no file displayed (matching code <dir>); 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.fulltree-view.md TVU1; executeView / runWorkspace / runGui
SRC5A directory target must resolve to an ordered, filtered file list — sorted by path, restricted to renderable files: *.twoslash.json and .d sources under --twoslash (the latter batch-extracted via twoslash-extract --dub), 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 are included by base-name allow-list (LNG3) — the one place the filter must point the other way, since a bare name is as consistent with a stripped binary as with Makefile. One directory deep by default; the whole subtree under SRC9.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
CLI32--recursive (-r) must take a directory target's whole subtree rather than only its top level (SRC9); with --html the gallery then mirrors the directory structure (gallery.md GAL12).full (3f0a7105)CliParams.recursive; source_set.collectSources
CLI33--root <dir> must name the directory the mirrored gallery's paths are relative to, so a subtree's pages can be mounted where the surrounding site expects them; with --root omitted it must default to the directory target itself.full (3f0a7105)CliParams.root; source_set.mirrorPrefix
SRC9Recursive acquisition: with --recursive (CLI32) a directory target must resolve to the renderable files of its whole subtree, each entry carrying its root-relative path (--root, CLI33, names the root; the target itself by default) alongside its base name — the key the static gallery mirrors (gallery.md GAL12). The descent must reuse sparkles:build-primitives' globWalkGitRepository, so .gitignore(the root's, every nested one, and the enclosing repository's ancestor scopes) and.git/are excluded by the library that implements them rather than by a skip-list in hue.--include/--exclude (XPF2precedence: include wins) must filter the set, anincludeglob re-admitting a.gitignore`d file; an ignored directory is not descended, since the walker's scope stack owns entering.full (42bb1181)source_set.collectSources; glob_walk.GitGlobFilter/globWalkGitRepository; tree-view.md TVU1

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 a multi-document session.not startedchrome.md STY1; SRC6
CAT2Positional targets and stdin (-) may interleave arbitrarily (hue a.d - b.d). Interactively this creates a multi-document session whose - entry is sourced from stdin; in the continuous non-interactive stream, stdin renders where it was positioned. Stdin may be named at most once.not startedSourceSet construction in app.main
CAT3--file-name <name> (repeatable, mapped positionally) must override the display name and language-detection cue (LNG1) for stdin (-); with no --file-name, stdin renders with a synthetic stdin label.not startedSourceSet construction in app.main
CAT4-E/--quiet-empty must silently produce an empty document when stdin is empty, rather than printing the header.not startedapp.main
CAT5With no arguments and stdin not connected to a terminal, hue must read stdin (the standard UNIX pipeline default).full (74d8f6a3)isTerminal(stdin) check in app.main; CAT2

Line ranges & selection (RNG)

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
RNG4Location formats — path:line[:col], path:startLine-endLine, compiler diagnostics (path(line[,col])), Python traceback lines (File "...", line N), and forge file URIs (GitLab #L6-9, GitHub #L42-L55, #L42) — must be accepted as target spellings. In a non-interactive sink it highlights/renders the target; in an interactive sink (TUI/GUI) it opens scrolled to that line/range with the whole document reachable.fullsource_loc.d; executeView in app.d; runWorkspace / runGui
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
LNG8Every language the repository references — each markdown fence label under docs/** and each source-file extension hue can be pointed at — must reach a grammar in the ts-grammars bundle, whether directly or through an alias. Where no usable grammar exists anywhere, the label must degrade to plain text (DEG2), and the omission must be recorded with its reason rather than left to be rediscovered. Plain text is the floor, never the silent default. A grammar that exists but is not packaged by nixpkgs is not an excuse to alias: it must be pinned and built (fetched).partial (028444db)nix/packages/ts-grammar-languages.nix; canonicalLanguage; ts.coverage.auditBundle
LNG9The language must be derivable from a path, not only an extension: an extensionless file resolves through its base name, so Makefilemake and Dockerfiledockerfile reach a grammar through the same alias table (SRC5).full (c736beb5)canonicalLanguageOfPath; source_set.isRenderable/plainTally
LNG10An alias that is not a true synonym — a label pointed at the closest surface syntax because no grammar for it exists anywhere (eff/frankocaml) — must be commented as the approximation it is at the point of definition, and its unavailability must have been checked, not assumed. "Not in nixpkgs" does not qualify (LNG8).full (d37ae71b)canonicalLanguage's approximation block

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 — the default, and unchanged. A set of pages must be able to leave the rules to one shared stylesheet instead (--stylesheet <href> links it, --emit-stylesheet <file> writes it, a gallery self-hosts assets/hue.css), since ~2.6 KB of theme (plus ~11.6 KB of twoslash overlay) per page does not scale to hundreds of listings.full (74d8f6a3; shared sheet 8044e739)HTML branch; renderHtml(cssClasses); fragment.FragmentOptions; assets
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. A recursive set (SRC9) must mirror the source tree instead of flattening it — <out>/<rel-path>.html per document and an index.html per directory (gallery.md GAL12) — and a one-directory-deep set must keep the flat layout byte for byte. Single-file --html (stdout fragment) must be unchanged.full (b61f4701; mirrored 037e1e1a)page_shell.writeGallery; site_tree; gallery.md GAL1/GAL2/GAL12
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
HTM10--html must be able to serve light and dark from one rendering: --dark-theme <name> makes --theme the light half and emits the dark theme's rules under an html.dark scope in the same stylesheet, so the switch is a class on <html> (what VitePress toggles) and the markup itself is identical for both. A generated page must also be able to switch on its own: an appearance toggle in the header writes VitePress's own localStorage['vitepress-theme-appearance'] key — so a reader crossing between a docs page and a listing keeps one preference rather than flipping twice per round trip — and a <head> script applies it before first paint, since applying it later paints light and repaints dark. Toggle, script and their rules are emitted only for a two-theme run. The scoped rules must win by specificity ((0,2,1) vs (0,1,0)) — no !important — and no dark rule may escape its scope. DEF5 records why this is not the CSS-variable mode.full (8044e739)syntax StylesheetOptions.scopeSelector; assets.themeStylesheet

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 (ed907790) — 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 (ed907790) 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 (ed907790)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
DEG5A label that asks for no highlighting (text, ansi) must not warn: it got the output it requested, and DEG1 says normal operation is silent.full (c736beb5)isPlainTextLabel gate on the DEG2 warning

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"
BLD5A packages.hue-bundled (Linux and macOS — the font bundle is platforms.all) must wrap the GUI binary with the sparkles-fonts bundle as its default font source — an env-var default that the explicit --font-dir overrides, the SPARKLES_TS_GRAMMAR_PATH idiom — so font resolution is deterministic (no fontconfig/CoreText query, no host-font dependence) and matches the APK's (FNT13). packages.hue stays the minimal-closure default (host font database; fonts not in the closure). The bundle costs ~90 MB, ~84 MB of it the four CJK-bearing Maple Mono NF CN faces; a slimmer bundle tier (non-CN Maple via the derivation's existing enableCN knob) is the escape hatch for a consumer that wants determinism without CJK.not startednix/packages/fonts.nix; nix/packages/hue.nix

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*
libs/docs/…/source_set.d (sparkles:docs)SourceEntry, SourceSet, collectSources, isRenderable, twoslashTally, plainTallySRC4SRC6, LNG9, gallery.md GAL1
libs/docs/…/fragment.d (sparkles:docs)plainFragment, twoslashFragment, relayoutGutter, withLineNumbersHTM4, gallery.md GAL4
libs/docs/…/page_shell.d (sparkles:docs)pageShell, galleryIndex, writeGalleryHTM6HTM8, gallery.md GAL2/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