Skip to content

hue as a pager — Feature Requirements (PAG/PIN/STR)

Status: design · Date: 2026-08-08 · Scope: hue's place in the shell — when it pages, what it pages with, how it renders input that arrives already formatted ($MANPAGER, git core.pager, a --help dump), and how it follows input that has not finished arriving (tail -f, less +F). Registered as CLI19/CLI25.

Why this exists

bat's pager story is a large part of what makes it a cat replacement people actually alias: bat on a long file behaves like less, on a short one like cat, and export MANPAGER="sh -c 'col -bx | bat -l man -p'" gives you highlighted man pages. bat gets there by shelling out to lesspager.rs and less.rs between them resolve $BAT_PAGER, $PAGER, sniff the less version to decide which flags are safe, and fall back to a bundled minus.

hue should not copy that. hue already has the thing bat is shelling out to acquire: a full-screen terminal viewer (tui.md) with scrolling, a scrollbar, mouse, search, selection and OSC 52 copy — strictly more than less offers, painted from the same widget views as the window. Spawning less to scroll a document hue can already scroll would be an odd thing for hue to do.

So the doctrine is: hue's own TUI is hue's pager, --pager=<cmd> is the escape hatch for people who want their own, and the genuinely new work is not paging at all — it is teaching hue to render input that is already formatted, so it can sit where less sits in $MANPAGER and git config core.pager.

Design & rationale

Paging is a sink choice, not a subprocess (PAG2)

hue already picks a backend once (MOD6pickBackend) and dispatches to one of four sinks. "Page this" is a fifth input to that decision, not a new stage bolted after the ANSI sink:

ConditionResult
stdout not a ttyANSI sink, whole document (MOD3)
tty, render fits the viewport, --paging=autoANSI sink, whole document — the cat feel
tty, render exceeds the viewport, --paging=autoTUI sink
--paging=alwaysTUI sink regardless of length
--paging=never / -PANSI sink regardless of length
--pager=<cmd> setANSI sink, piped to <cmd>

The one new fact this requires is how tall the render is, which the ViewerModel relayout already computes for the scrollbar. Nothing else in the dispatch changes, which is the point: hue gains bat's most-loved behaviour by routing an existing decision differently rather than by growing a pager.

--gui and paging are orthogonal

A display-backed launch opens the window, which is by construction paged. The paging decision only ever selects between hue's two terminal sinks; on a GUI-enabled autodetected launch it is not consulted. --paging=always does not imply --tui.

The real work: pre-formatted input (PIN)

To be $MANPAGER, core.pager or the sink of hue --help | hue, hue must render bytes that are already styled — SGR sequences from git diff, and man's ancient backspace overstrike (_\bt for underline, t\bt for bold). Today hue would highlight those bytes as source text, which is wrong twice over: the escapes show up as content, and the content shows up unstyled.

hue is unusually well placed here. ansi_model.d and the off-screen ghostty VT already decode SGR into hue's presentation cells — that is how ```ansi fences render in the markdown preview (MDP12). A pre-formatted content kind is therefore that decoder pointed at the whole input instead of at one fence, plus a small overstrike pass. That is a genuinely small feature that unlocks two of bat's headline integrations, which is exactly the trade this scope is meant to favour.

It also composes with the sanitizer: a document hue is deliberately interpreting as ANSI is precisely the region where TXT2 must not neutralize escapes, and the content kind is what tells it so.

Streaming is the same seam, not finished (STR)

tail -f logfile | hue and less +F are the same feature seen from two sides: input that has not ended. hue's frame loop is already event-driven (sparkles:event-horizon supplies pollAdd and Ticker), so following a growing fd costs a registration rather than a poll loop. The honest limitation is highlighting: tree-sitter is re-run over the accumulated text on append, so a partial construct at the tail may highlight oddly until the next chunk lands. Incremental reparse is a sparkles:syntax roadmap item and stays there.

Deliberate omissions

Not doingWhy
less version detectionbat sniffs less --version to decide which flags are safe. hue passes -R when it spawns a pager named less with no flags of its own, and otherwise nothing.
A bundled pager (bat's minus)hue's TUI is the bundled pager.
$LESSOPEN / $LESSCLOSEInput preprocessing is a real feature but a separate one — deferred as DEF26.
Output-is-input cycle detectionbat's clircle guard against bat file > file. Worth revisiting; not load-bearing for the pager doctrine.

Paging (PAG)

IDRequirementStatusTraces to
PAG1--paging=auto|never|always must select whether a terminal launch pages, with -P an alias for never and -pp implying it (STY5). The default is auto.not startedCliParams.paging; pickBackend
PAG2Under auto, hue must page when stdout is a tty and the laid-out render is taller than the viewport, and emit the whole document to ANSI otherwise — so short files feel like cat and long ones like less. The height comes from the existing ViewerModel relayout, not a second measurement.not startedviewer_model layout height; pickBackend
PAG3Paging must mean hue's own TUI (tui.md) over the same document, not a spawned process: no bundled pager, no less-version detection.not startedrunTuiSink reached from the paging decision
PAG4--pager=<cmd>, $HUE_PAGER or $PAGER (in that precedence) must instead pipe the ANSI emit to <cmd>. When the command is bare less, hue must add -R; otherwise the user's arguments are passed through untouched. A pager that fails to spawn must warn and fall through to PAG3.not startedproposed pager.d spawnPager
PAG5Under a pager (internal or external) hue must behave as if --color=always --decorations=always, since stdout is a pipe but the eventual consumer is a terminal.not startedCLR2/STY4 policy resolution
PAG6--set-terminal-title must set the terminal title to the document's title for the lifetime of a paged session and restore it on exit.not startedsparkles:tui lifecycle; OSC 2
PAG7hue must be documented and shaped as a drop-in for $PAGER, $MANPAGER and git config core.pager — which requires PIN1, CAT2 (stdin), STY5 (-p) and PAG5, and nothing else. The README must carry the three recipes.not startedREADME.md; PIN1; CAT2

Pre-formatted input (PIN)

IDRequirementStatusTraces to
PIN1hue must recognize a pre-formatted content kind — input that is already styled — and render it by decoding rather than highlighting it, in every sink. It is a ContentKind beside code / markdown / twoslash / diff, produced by the pipeline like any other.not starteddocument.ContentKind.preformatted; MOD9 doctrine
PIN2SGR-styled input must be decoded through the existing ansi_model.d path (the off-screen ghostty VT that ```ansi fences already use), not a second parser.not startedansi_model.d; MDP12
PIN3man-style backspace overstrike (x\bx = bold, _\bx = underline) must be decoded to the equivalent styling, so $MANPAGER works without col -bx.not startedproposed overstrike.d (pure, @nogc)
PIN4Detection must be automatic for non-tty stdin that contains SGR or overstrike sequences, and forceable with --preformatted; --language/-l must still win, so -l man or -l diff overrides the sniff.not startedlooksPreformatted; LNG2 cascade
PIN5A pre-formatted document must be exempt from ANSI stripping and sanitizing (TXT2/TXT3) — hue is interpreting those bytes on purpose — while every other content kind stays subject to them.not startedTXT2; ContentKind gating
PIN6Selection and copy over a pre-formatted document must yield the decoded text by default, with the escape-preserving variant reachable through the existing --ansi-copy=raw|strip toggle (CLI10), which already names exactly this choice.not startedCliParams.ansiCopy; SEL7

Streaming & follow (STR)

IDRequirementStatusTraces to
STR1-u/--unbuffered must render input incrementally as it arrives rather than waiting for EOF, so tail -f logfile | hue -u shows lines as they are written.not startedproposed stream.d; DocumentPipeline.append
STR2The interactive sinks must offer a follow mode (F, after less +F): the viewport pins to the tail as content arrives, and any navigation key releases it. Following must be entered automatically under -u on a tty and reported in the status line.not startedtui.d; keymap.d
STR3Appended input must be registered with the event loop (sparkles:event-horizon pollAdd), never polled on a timer, so a quiet stream costs no wakeups.not startedsparkles:event-horizon; the TUI frame loop
STR4Highlighting under streaming must be re-run over the accumulated text on append and may be imperfect at the tail while a construct is incomplete; line numbers, wrapping and search must stay correct regardless. Incremental reparse is out of scope here (sparkles:syntax).not startedsparkles:syntax incremental roadmap
STR5A growing file target should be followed as well as a growing pipe, so hue -u ./build.log matches tail -f ./build.log | hue -u.not startedstream.d file watch

Module coverage

Source (proposed)Key symbolsRequirements
apps/hue/src/pager.dPagingMode, shouldPage, spawnPagerPAG1PAG6
apps/hue/src/overstrike.ddecodeOverstrike (pure, @nogc)PIN3
apps/hue/src/document.dContentKind.preformatted, looksPreformattedPIN1, PIN4PIN5
apps/hue/src/ansi_model.dthe existing SGR → cell decoder, reused wholePIN2
apps/hue/src/stream.dincremental append, follow state, the fd registrationSTR1STR5

Feature requirements · Document chrome · TUI · Overview