Skip to content

hue lantern — Feature Requirements (the key guide, all interactive backends)

Status: partial — the binding table, the prefix machine, the panel and both backends shipped (c19bb9268ec10bee); the machinery and its KEY*/LTN* requirement rows have since moved to docs/specs/ui/keymap.md with the extraction into sparkles:ui · Date: 2026-08-18 · Scope: hue's key guide — now the policy this application feeds the framework's guide: the binding table (hueBindings) and the leader map it navigates.

NOTE

Inspired by folke/which-key.nvim. The name and the mechanism are hue's own; what is borrowed is the idea that a keymap should be able to explain itself, the delay-then-reveal interaction, and the multi-column panel layout. Status legend and ID conventions: see the overview.

Why this exists

hue binds about fifty keys and, until this landed, told you about none of them. Worse, it described them three times:

CopyDroveSaid g meantSaid y meant
keymap.commandForthe GUIgo-to-linetoggle ANSI-copy
tui.handleKeythe terminal viewertopcopy the selection
explorer.handleKeythe terminal explorertop

Three copies is not a documentation problem; it is a correctness problem, and it is the same defect UIA2 records for the two scrollbars whose thumb formulas scrolled the same document differently. A guide that describes one of the three would be lying about the other two.

So the guide's prerequisite is not a panel. It is one policy that can be read in both directions — which key does this, and which keys are available.

Design & rationale

The policy is a table, not a function (KEY)

commandFor answered "what does this key do here" and nothing else. Two consumers need the other direction:

  • the guide, which is an enumeration of the binding set;
  • configuration (CFG6), which says "configuration replaces the hardcoded table with a loaded one" — and there was no table to overlay.

So hueBindings is an immutable Binding[], commandFor is a lookup over it, and bindingsAt is the enumeration. One declaration, read both ways, with bindingsAtEnumeratesWhatWouldFire asserting the two agree at every level of the tree.

Ordering is the policy. Precedence used to live in the shape of an if/else chain — a Ctrl chord resolves before the plain letter, an open input mode claims Enter before anything else, a focused tree claims j before the viewer. That is now Scope_, whose declaration order is the resolution order.

IMPORTANT

The rewrite was proved, not asserted. The previous chain was kept as a version (unittest) differential oracle and swept against the whole input space — every context × every modifier combination × every key. It caught a real regression (Ctrl-Up must keep scrolling, because the old Ctrl block only guarded char_ keys) and four bindings added on main mid-rebase. It was retired in 8e4b0250, when the policy deliberately began to differ from the chain.

A prefix is a path, not a flag (LTN1LTN4)

hue had exactly one prefix, z, and it was a bool in KeyContext plus a 60-frame countdown in the GUI's frame loop. That shape works for one prefix, one level deep, on the one backend that has frames. It cannot express <leader>uy, it cannot tell the guide which prefix is pending, and its clock does not exist in a terminal.

LanternState holds the pending path instead — at most maxPathLength chords in a SmallBuffer — and step is a pure function over it.

The delay is measured in time, not frames. This is what lets the terminal use untilShown as its poll timeout: a terminal has no frames to count, and its loop blocks on input, so a panel that appears "after 200 ms" would in fact appear on the next keystroke — precisely when it is useless.

The delay is also the whole interaction. A prefix that opened its panel instantly would punish anyone who knows the keys; one that never opened would not be a guide. zc typed quickly folds and shows nothing; z held for a beat lights the panel. The same keystrokes teach or get out of the way depending only on how fast you are.

One panel, every backend (LTN5LTN8)

The panel is a sparkles:ui widget tree, so the GUI, the TUI and (for a static cheat sheet) HTML get one definition rather than three that drift — the UIA2 contract. Painting is where these backends have diverged before, so tui.lantern.panelPaintsIntoTheCellGrid reads the cells back rather than trusting the tree.

Layout is which-key's: pack the items into as many side-by-side columns as the width affords, filled top-to-bottom, one column at a time. Widening reflows to more, shorter columns; narrowing collapses toward one.

The map is the user-facing artefact (LMP)

A leader map is memorised, so rearranging it later is a real cost to a user who has learnt it. The letters the picker will claim are therefore reserved now and listed below as not started, rather than being assigned to whatever is convenient today and moved when the picker lands.

The binding table & the guide (KEY, LTN) — moved

The KEY1KEY13 and LTN1LTN17 requirement rows live in docs/specs/ui/keymap.md, beside the machinery they specify (sparkles.ui.keymap, sparkles.ui.lantern, sparkles.ui.components.lantern_view). Their statuses and shipping history are preserved there; hue remains the first consumer, and the hue-side obligations trace back to this application's modules: hueBindings is the one table (KEY1), its rows are spelled in normalised form (KEY8, keymap.tableIsSpelledInNormalisedForm), and every backend dispatch is a final switch over Command (KEY11).

The map (LMP)

The reviewable source of truth for what hue binds. Non-leader prefixes: z +fold, g +goto, [/] prev/next.

IDRequirementStatusTraces to
LMP1The leader is <space>, because that is the muscle memory the LazyVim-shaped map this is growing into is built on — and the easiest key to hit on a touch keyboard (LTN13).full (cbec2dbb)keymap.leader
LMP2<leader>v +view · <leader>u +ui · <leader>d +diff · <leader>e explorer · <leader>? all bindings.full (cbec2dbb)hueBindings leader rows
LMP3Motions are vim-correct: gg top, Shift-G bottom, gl go-to-line, per pane.full (8e4b0250)keymap; tui.keys.resolvedDivergences…
LMP4y copies the selection in every backend; the ANSI-copy mode it displaced is a setting and lives at <leader>uy.full (8e4b0250)hueBindings
LMP5q quits in every backend.full (8e4b0250)hueBindings
LMP6/ and PgUp/PgDn are pane-scoped: a focused tree navigates its rows, the viewer cycles themes and pages. Theme cycling stays reachable from anywhere via <leader>ut.full (e71a9417)hueBindings; sharedBindingsWork…
LMP7<leader>f +file/find and <leader>/ grep are reserved for the picker and must not be assigned to anything else in the meantime.not startedpicker.md PIK
LMP8<leader>s +search and <leader>g +git are likewise reserved for the picker's search and git sources.not startedpicker.md PKS
LMP9<leader>o +overlay is reserved for the overlay kinds (OVL4).not startedoverlays.md
LMP10<leader>vf toggles the format preview and <leader>vF cycles its formatter; </> nudge the ruler while the preview is active (context-gated — [/] stay set/diff navigation).full (ca124bea5)format-preview.md

Milestones

MilestoneScopeStatusRequirements
LT0The binding table; commandFor derived from it; the enumerationdone (c19bb926)KEY1KEY9
LT1The prefix state machine; z as a path; the leader treedone (cbec2dbb)LTN1LTN3, LTN9LTN12, LMP1/LMP2
LT2The panel as a widget tree; the GUIdone (91054510)LTN5LTN8
LT3The TUI and explorer routed; the panel in cells; the loop's deadlinedone (8ec10bee)KEY10KEY11, LTN4, LMP3LMP6
LT4Tappable rows; the Android command menunot startedLTN13
LT5Configuration: placement, delay, leader, and the rebindable tablenot startedKEY12, LTN14, LTN15
LT6Icons; the static HTML cheat sheetnot startedLTN16, LTN17

Module coverage (lantern)

The machinery's rows (sparkles.ui.keymap/lantern/components.lantern_view) are covered in ../ui/keymap.md; this table keeps hue's own.

SourceKey symbolsRequirements
apps/hue/src/keymap.dCommand, Scope_, KeyContext (the hooks), hueBindings, bound wrappersKEY1, KEY8, LMP*
apps/hue/src/lantern.dstep bound to hueBindings; hue's policy tests through the machineKEY1
apps/hue/src/gui.dthe final switch dispatch; the panel paint; HUE_GUI_LANTERNKEY11, LTN5
apps/hue/src/tui.dhandleKey, keyContext, paintLantern, untilLanternShownKEY11, LTN4, LTN5
apps/hue/src/explorer.dhandleKey, handleCommand, collapseOrUpKEY11, LMP6
apps/hue/src/workspace.dthe loop's second deadline; tickLanternLTN4

Relationship to existing specs

PieceRole
../ui/keymap.mdthe machinery and its KEY*/LTN* rows, extracted from here
config.md CFG6the rebindable keymap this table finally makes possible
picker.mdwhat <leader>f / <leader>/ / <leader>s / <leader>g open
ui-architecture.md UIA2the one-definition-per-visual contract the panel is held to
android.mdwhy LTN13 is not optional — no keyboard exists there
folding.md FLD5the z family the prefix machine replaced a frame counter for
sparkles:inputthe Key/Mods/KeyEvent vocabulary a chord is written in

Picker requirements · Configuration · Overview