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 areas — CAT, 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 (DEF25–DEF27). 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.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| CLI1 | hue [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 |
| CLI13 | The --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 (DVS1–DVS3); 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 started | diff-view.md DPR1/DPR7 |
| CLI26 | The --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 started | dsv-preview.md DSK*/DSD* |
| CLI27 | The --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 |
| CLI28 | The --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.
| ID | Requirement | Status | Traces 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 started | RNG |
| CLI16 | --style <components>, --decorations <when>, -p/--plain, -n/--number and -f/--force-colorization must select the document chrome. | not started | chrome.md STY1–STY5 |
| 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 started | chrome.md CHG4 |
| CLI18 | Multiple positional targets, a bare - for stdin, --file-name <name> and -E/--quiet-empty must give hue cat's concatenation semantics. | not started | CAT |
| CLI19 | --paging <when>, -P, --pager <cmd>, --preformatted and --set-terminal-title must configure paging and pre-formatted input. | not started | pager.md PAG1–PAG6, 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 started | TXT |
| 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 started | LNG |
| CLI22 | --theme must additionally accept auto, dark and light; --theme-dark <name>, --theme-light <name> and --list-themes must complete the pair. | not started | THM |
| CLI23 | --color <when> and --italic-text <when> must set the color policy, and $NO_COLOR must be honoured. | not started | CLR |
| CLI24 | --wrap <mode>, -S/--chop-long-lines and --terminal-width <width> (accepting +N/-N offsets) must set wrapping and the assumed width. | not started | WID |
| CLI25 | -u/--unbuffered must render input incrementally instead of waiting for EOF. | not started | pager.md STR1 |
Source acquisition (SRC)
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| SRC1 | With a path argument the whole file must be read into memory as the highlight input. | full (74d8f6a3) | readText(sourcePath) |
| SRC2 | With 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 |
| SRC3 | An 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 started | proposed URL fetch in app.main |
| SRC4 | A 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 |
| SRC5 | A 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 |
| SRC6 | That 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 |
| SRC7 | Diff-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 (DVS1–DVS3); DiffSides carries the per-side language |
| SRC8 | Forge 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 started | diff-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.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| CAT1 | Several 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 started | source_set.SourceSet; the ANSI/HTML sink loops |
| CAT2 | A 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. | partial | readStdinText (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 started | CliParams.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 started | CliParams.quietEmpty |
| CAT5 | A 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.
| ID | Requirement | Status | Traces 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 started | proposed line_range.d parseRange |
| RNG2 | The 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 started | document.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 started | line_range.d; theme highlight slot |
| RNG4 | A 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 started | target parsing in app.main; RNG2 |
| RNG5 | Where 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 started | chrome.md CHW6 |
| RNG6 | Line 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. | partial | HTM4 (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.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| TXT1 | Normalization 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 started | proposed 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 started | sparkles.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 started | sparkles.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. | partial | CliParams.listWhitespace; sparkles.base.text encoder |
| TXT5 | Binary 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 started | isBinary; the sink guard |
| TXT6 | A 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 started | sparkles.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 started | sparkles.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. | partial | CliParams.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.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| LNG1 | The grammar language must be derived from the file extension, canonicalized through sparkles:syntax aliases. | full (74d8f6a3) | canonicalLanguage(extension) |
| LNG2 | Language 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 started | proposed 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 started | CliParams.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 started | sparkles:build-primitives gitignore.d matcher |
| LNG5 | First-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 started | proposed 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 started | LNG2 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 started | GrammarRegistry; ENG2/DEG4 |
Highlight engine (ENG)
hue drives the sparkles:syntax precise pipeline; the engine internals are specified in docs/specs/syntax.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| ENG1 | Highlighting 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) |
| ENG2 | Grammars must be loaded from the nix bundle via $SPARKLES_TS_GRAMMAR_PATH; a GrammarRegistry/TsConfigCache is built once. | full (74d8f6a3) | GrammarRegistry.fromEnvironment; TsConfigCache.create |
| ENG3 | Highlighting must produce a HighlightEvent stream over the source, consumed identically by every rendering mode. | full (74d8f6a3) | SmallBuffer!HighlightEvent events |
| ENG4 | On any engine failure (no grammar, parse error) hue must fall back to a single plain-text span covering the whole source. | full (74d8f6a3) | res.hasError → HighlightEvent.sourceSpan |
Themes (THM)
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| THM1 | The named theme must be resolved from builtinThemes; an unknown name must warn and fall back to builtinDark. | full (74d8f6a3) | builtinThemes.get(themeName, …) |
| THM2 | The 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 |
| THM3 | A 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.
| ID | Requirement | Status | Traces 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 started | CliParams.theme/themeDark/themeLight; the cascade's level 1 |
| THM5 | Under --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 started | platform-ui-guidelines milestones P0/P1 |
| THM6 | A 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 started | sparkles:syntax Theme.variant; builtinThemes |
| THM7 | Every 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 started | the 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 started | builtinThemes; 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)
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| CLR1 | ANSI 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.
| ID | Requirement | Status | Traces 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 started | CliParams.color; AnsiOptions |
| CLR3 | A 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 started | CLR2 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 started | CliParams.italicText; renderAnsi options |
| CLR5 | True-color availability must be detected once in term_caps ($COLORTERM and the terminfo answer) and consumed by CLR1, rather than probed per sink. | partial | detectColorDepth(); 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.
| ID | Requirement | Status | Traces 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 |
| MOD3 | Non-interactive (stdout not a tty) must emit the whole file once (HTML if --html, else ANSI) and exit. | full (74d8f6a3) | !interactive branch |
| MOD4 | An interactive tty (no --html) must open the live terminal previewer. | full (74d8f6a3) | interactive branch → runLoop |
| MOD5 | If 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 null → emitAnsiWholeFile |
| MOD6 | With 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 (MOD3–MOD5). On a no-gui build it always falls through. | full (cdc813f6) | displayAvailable; wantGui in app.main |
| MOD7 | The 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 |
| MOD8 | A 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 |
| MOD9 | A 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) |
| MOD10 | A 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 started | dsv-preview.md DSK* |
ANSI terminal output (ANS)
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| ANS1 | Non-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) |
| ANS3 | For 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 |
| ANS4 | A 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).
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| BGM1 | no-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 |
| BGM2 | spans — 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 |
| BGM3 | full (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.
| ID | Requirement | Status | Traces 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 started | chrome.md STY1–STY5 |
| CHR2 | The 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 started | chrome.md CHW1–CHW7 |
| CHR3 | Chrome must never be selectable or copyable in any sink, generalizing the HTML-only rule HTM3 states today. | not started | chrome.md CHW3; HTM3 |
| CHR4 | The 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 started | chrome.md CHG1–CHG3 |
| 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 started | chrome.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.
| ID | Requirement | Status | Traces 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 started | CliParams.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 started | WID1; 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 started | CliParams.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.
| ID | Requirement | Status | Traces 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 started | pager.md PAG1/PAG2 |
| PGR2 | Paging 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 started | pager.md PAG3–PAG5 |
| PGR3 | hue 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 started | pager.md PAG7 |
| PGR4 | A 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 started | pager.md PIN1–PIN4; MOD9 |
| PGR5 | A 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 started | pager.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 started | pager.md STR1–STR3 |
| PGR7 | Highlighting 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 started | pager.md STR4 |
HTML output (HTM)
| ID | Requirement | Status | Traces 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) |
| HTM2 | The 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:
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| HTM3 | Best-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. | partial | plain <pre><code> copies to source today; the twoslash overlay realizes the decoration-excluded half on branch (TWH6–TWH8); a plain---html gutter awaits HTM4 |
| HTM4 | Best-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 |
| HTM5 | MDP 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 |
| HTM6 | A 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 |
| HTM7 | Each 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 |
| HTM8 | Gallery 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) |
| HTM9 | A 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 started | diff-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.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| PRV1 | The 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 |
| PRV3 | Enter 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 |
| PRV4 | Any other key / cancel must quit and print nothing. | full (74d8f6a3) | Key.cancel/Key.other |
| PRV5 | A 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 |
| PRV6 | The 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 |
| PRV7 | The 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 |
| PRV8 | The 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.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| WCH1 | The 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 open | workspace.d watch fiber; event-horizon watch.d |
| WCH2 | A 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 arm | WorkspaceTui.reloadCurrent |
| WCH3 | Incremental 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. | researched | ts_tree_edit; sparkles:diff byte diff |
| WCH4 | Degradation 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)
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| DEG1 | Only degradation warnings are logged (logger level = warning); normal operation is silent. | full (d87397b3) | initLogger(LogLevel.warning) |
| DEG2 | A missing grammar must warn (no grammar for '<lang>') and render plain text, not fail. | full (74d8f6a3) | warning(i"no grammar …") |
| DEG3 | An unknown --theme must warn and use the default dark theme. | full (74d8f6a3) | builtinThemes.get fallback |
| DEG4 | Without $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.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| BLD1 | The 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" |
| BLD2 | A 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" |
| BLD3 | The 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 |
| BLD4 | dub 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)
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| NFR1 | Startup (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 |
| NFR2 | Each 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 |
| NFR3 | A 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 |
| NFR4 | GUI/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-started | provisional target; no benchmark gate yet |
| NFR5 | GUI window resize must stay within the frame budget — the reflow (WIN3/WRP4 relayout on a width change) must not cause noticeable stutter. | not started | relayout; gui.md WIN3/WRP4 |
| NFR6 | Large 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. | partial | gui.md RND1 (culled scroll); load/relayout unbenchmarked |
| NFR7 | All 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) |
| NFR8 | Diff 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 started | diff-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.
| ID | Requirement | Status | Traces to |
|---|---|---|---|
| DEF1 | --twoslash <nodes.json> mode (ANSI default / --html / --gui) — D-native Twoslash rendering. | planned/branch-only | twoslash.md TWM1–TWM3 |
| 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 |
| DEF3 | The MdDoc → HTML emitter (sparkles:syntax md/render_html.d) that the --markdown/twoslash-docs paths need. | full | libs/syntax md/render_html.d; syntax spec J1 |
| DEF4 | Runtime 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-started | syntax spec D6 |
| DEF5 | CSS-variable multi-theme HTML output mode (one document, theme switched via :root[data-theme] / prefers-color-scheme). | researched/not-started | syntax spec F6; render/html.d |
| DEF6 | Content-based language detection (a Linguist-style cascade) — today the language is only the file extension / fence label. | not started | syntax spec §deferred (canonicalLanguage only) |
| DEF7 | A 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-started | gui.md FNT6 |
| DEF8 | Color-emoji rendering in the GUI (a separate rasterizer for CBDT/COLR) — raylib/stb_truetype cannot. | not started | gui.md FNT7 |
| DEF9 | Pluggable overlays — a backend-agnostic overlay seam generalized from twoslash, plus new overlay kinds (source map, coverage, tracing, tree-sitter inspector, code size). | researched/not-started | overlays.md OVL* |
| DEF10 | Notifier / 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-started | notifier.md NTF*/NSI*/NFI* |
| DEF11 | Full-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* |
| DEF12 | UI component library — sparkles: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. | partial | sparkles: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 started | web-integration.md PKG*/SHL*/WSM*/FWK* |
| DEF14 | Content 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 started | folding.md FLD*/FSR* |
| DEF15 | Tree / 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 started | tree-view.md TRV*/DAG*/TVU* |
| DEF16 | Navigation — 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 started | navigation.md LNK*/REF*/LNB* |
| DEF17 | Images & 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 started | media.md MDB*/IMG*/DGM*/MTH* |
| DEF18 | Tab 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 started | tab-view.md TAB*/TBU*/TBB* |
| DEF19 | Transformer 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-started | pipeline.md PIP*/PRS*/XFM*/CMP* |
| DEF20 | Gallery & 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* |
| DEF21 | Diff, 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 started | diff-view.md DVM*/DVS*/DVL*/DVN*/DVG*/DPR*/DST*/DCM*/CFV* |
| DEF22 | Lantern — 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* |
| DEF23 | Picker — 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 started | picker.md PIK*/PKQ*/PKR*/PKS*/PKL* |
| DEF24 | sparkles: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 started | picker.md PKM* |
| DEF25 | User 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 started | ENG2; DEF4; bat's bat cache --build as the reference shape |
| DEF26 | Input 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 started | bat's lessopen.rs; media.md MDB* |
| DEF27 | Introspection & 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 started | sparkles:core-cli args.d/help_formatting.d; config.md CFG* |
| DEF28 | DSV 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 started | dsv-preview.md DSK*–DSZ* |
| DEF29 | Format 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 open | format-preview.md FMV*/FPR*/RUL* |
Module coverage (general spec)
Every non-GUI source file maps to the requirements above:
| Source | Key symbols | Requirements |
|---|---|---|
apps/hue/src/app.d | CliParams, main, emitAnsiWholeFile, mode dispatch | CLI*, SRC*, LNG1, ENG*, THM*, CLR1, MOD*, ANS*, HTM*, DEG* |
apps/hue/src/source_set.d | SourceEntry, SourceSet, collectSources, twoslashTally, plainTally | SRC4–SRC6, gallery.md GAL1 |
apps/hue/src/gallery.d | relayoutGutter, pageShell, galleryIndex, writeGallery | HTM4, HTM6–HTM8, gallery.md GAL2/GAL4/GAL6/GAL7 |
apps/hue/src/previewer.d | deleted (D6, M9) — theme cycling lives in the TUI; BackgroundMode moved to ansi_model.d | PRV* (retired) |
apps/hue/dub.sdl | build configurations (application [GUI] / no-gui / unittest) | CLI4, MOD1/2, BLD1/2/4, NFR3 |
nix/packages/hue.nix | packages.hue, packages.hue-no-gui | BLD3 |
Proposed modules for the areas above that have no code yet:
| Source (proposed) | Key symbols | Requirements |
|---|---|---|
apps/hue/src/line_range.d | parseRange, LineSelection | RNG1–RNG4, RNG6, CHG4 |
apps/hue/src/style.d | StyleComponents, DecorationPolicy | CHR1–CHR3, chrome.md |
apps/hue/src/git_changes.d | LineChange, changeMap over sparkles:diff | CHR4, CHR5 |
apps/hue/src/pager.d | PagingMode, shouldPage, spawnPager | PGR1–PGR3 |
apps/hue/src/overstrike.d | decodeOverstrike | PGR4 |
apps/hue/src/stream.d | incremental append, follow state | PGR6, PGR7 |
libs/base/.../text/normalize.d | sanitize, stripSgr, expandTabs, squeeze, BOM decoding | TXT1–TXT8 |
libs/ui/.../components/chrome.d | GutterColumn, gutter, header, grid, rule, snip | CHR2, chrome.md CHW* |
libs/syntax/.../language.d | resolveLanguage (the cascade), firstLineLanguage | LNG2–LNG7 |
libs/base/.../term_caps.d | true-color tier and the detected terminal width | CLR5, WID3 |
sparkles:appearance (proposed) | the scheme cascade hue consumes — not hue's to build | THM5, THM7 |
→ GUI requirements · Document chrome · Pager & streaming · Overview