Skip to content

hue picker — Feature Requirements (fuzzy finding, all interactive backends)

Status: partial — F0 engine verified and P0 component/scheduler seams implemented · Date: 2026-08-17 · Scope: hue's picker — the fuzzy finder behind <leader>f, <leader>s, <leader>g and <leader>/ (lantern LMP7/LMP8) — its query language, its sources, its ranking, and the sparkles:fuzzy engine underneath.

NOTE

The bounded engine, files finder, presentation-free state, raw-pool scheduler, and shared widget tree exist; <leader>ff mounts the picker in both live hosts, and the preview panel hosts a real document pane over the same loader. Persistence, actions, and the later sources remain milestone work. Status legend and ID conventions: see the overview.

Design & rationale

Why hue needs one at all

hue can already open a file, walk a directory (TVU1) and search within a document (FND). What it cannot do is answer "where is the thing called roughly this" without you knowing where it lives — which is most of what using a code viewer consists of.

The reference points are ibhagwan/fzf-lua and folke/snacks.nvim's picker for the shape (finder → matcher → list → preview → actions, plus layouts and resume), and dmtrKovalenko/fff for the engine.

Why the engine is written, not linked (PKM)

fff is a resident file-search engine — it keeps an index and a content cache warm in one long-lived process, and on a 500k-file checkout that is the difference between seconds per rg spawn and single-digit milliseconds per query. It ships a stable C ABI, and hue already binds C libraries this way twice (sparkles:ghostty wraps a Zig library; sparkles:tree-sitter a C one).

Binding it was considered and rejected, for the same reason sparkles:diff was written rather than bound: hue's engines are things this repository owns, unit-tests, and benchmarks. A Rust cdylib plus LMDB in the closure would also have to cross-compile for both Android ABIs, where hue's build is nix-native and Gradle-free (android.md).

What is taken instead is fff's design, which is readable and portable:

BorrowedFrom
the composite ranking formulafff-core/src/score.rs
the query constraint languagefff-query-parser
frecency with exponential decayfff-core/src/dbs/frecency.rs
budget + abort + cursor pagingfff-core/src/grep/types.rs
three grep modes with fallbackfff-core/src/grep/
arena-chunked path storageFileItem — already sparkles' own doctrine

The query is a language, not a pattern (PKQ)

fzf's pattern mods ('exact, ^prefix, suffix$, !inverse) are a filter over strings. fff's query is a filter over files, which is what a code viewer actually has: git:modified src/**/*.rs !src/**/mod.rs user controller is one query, splitting into constraints plus a fuzzy remainder.

hue can satisfy those constraints today — it already has git status (git_status.d) and a .gitignore-aware walker (sparkles:build-primitives) — so the language costs a parser, not a subsystem.

Ranking is not matching (PKR)

A matcher answers "does this candidate contain the query"; a picker has to answer "which of these forty do you mean". fff's formula, portable as arithmetic:

total = base(fuzzy score)
      + frecency_boost      base·frecency/100
      + git_status_boost    base·15%  when modified
      + distance_penalty    relative to the current file's directory
      + filename_bonus      base·40% exact filename
                          | ≤30, quality-scaled, for a fuzzy filename match
                          | base·5%  special entry-point file (mod.rs / index.ts …)
      + current_file_penalty  −base/4
      + combo_match_boost   this query previously opened this file
      + path_alignment      suffix overlap, when the query contains "/"

Every term is returned as a breakdown, not just a total, so the ranking is inspectable rather than a black box that "feels wrong" — fff's :FFFDebug idea, and the same instinct as hue config show CFG10.

Interactivity is a budget, not a promise (PIK5)

A grep over a large repository cannot block a frame. Every picker generation therefore owns an immutable query arena, corpus snapshot, global top-K accumulator, and cursor. The hue adapter takes a real monotonic duration budget and advances the pure fuzzy engine one candidate-sized bounded call at a time. It returns globally ranked partial results plus a cursor bound to the generation, corpus snapshot, sink epoch, and accumulator revision.

The fan-out runs on sparkles:event-horizon's fixed-capacity raw CPU-job pool — the persistent, closure-free companion to the measured cpuBoundWorkStealingPool that beats rayon on polyglot-walks (1.16× on a real 325k-entry tree, 4.26× on dense ones, 55 futex calls against std.parallelism's 10 632). Reusing it means the picker inherits a walker that has already been measured against the best in the field.

WARNING

Pool start or job submission can fail explicitly. Queue saturation runs the same bounded step synchronously; an unavailable platform uses a fully synchronous budget-stepped walk rather than losing the feature.

The component (PIK)

IDRequirementStatusTraces to
PIK1A picker must present a prompt, a ranked result list, and an optional preview, over any source — one component, many sources, in the shape fzf-lua and snacks.picker share.fullpicker_host.d + picker_preview.d, mounted in both hosts
PIK2Its state must be a presentation-free value — a fixed-capacity prompt editor, the scored items, selection and scroll offset — testable with no canvas, like every other STM machine.full (working tree)PickerPrompt, PickerState, ScrollState
PIK3The view must be a sparkles:ui widget tree, painted by GUI and TUI from one definition (UIA2) — the contract lantern LTN5 is already held to.fullpicker_view.d, painted by both hosts; grid readback in workspace.leaderFfMountsThePicker
PIK4A source must be a DbI seam: anything that can produce items incrementally is a source, and adding one must not touch the component.full (working tree)isFinder, finderSnapshot, FilesFinder
PIK5Every search must take a real monotonic duration budget and return partial globally-ranked results plus a cursor bound to generation, corpus snapshot, sink epoch, and accumulator revision.full (working tree)searchChunk; PickerScheduler
PIK6Query/corpus arenas and result accumulators are caller-owned, fixed-capacity, immutable while borrowed, and pinned until all generation jobs complete; the search-scheduling path for a keystroke performs no allocation.full (working tree)fixed query/glob arena, generation slots, TopK
PIK7Re-running a query must cancel in-flight work with a monotonic atomic generation (release publication/acquire read); stale jobs cannot append to a new sink epoch.full (working tree)PickerScheduler + stale-generation tests
PIK8The picker must degrade to a synchronous budget-stepped walk when the raw CPU pool cannot start or accept a job, rather than being unavailable.full (working tree)RawCpuPool submission + synchronous fallback
PIK9resume must reopen the last picker with its query and selection intact.not startedproposed session store

The query language (PKQ)

IDRequirementStatusTraces to
PKQ1A query must split into constraints plus a fuzzy remainder, in one pass, with every span borrowed from the input so parsing allocates nothing.full (working tree)sparkles.fuzzy.query
PKQ2Constraints must cover *.ext, a glob, a path segment, a file path suffix, and git:<status> (modified/staged/untracked/ignored).full (working tree)compiled constraint evaluator
PKQ3Any actual constraint must be negatable (!seg:test, !*.rs); ordinary fuzzy text, !=, and !!foo remain literal.full (working tree)deterministic dispatch + tests
PKQ4A trailing :line[:col] must parse as a location, so pasting src/app.d:120 from a compiler diagnostic opens where it points.full (working tree)Location; Windows-drive guard
PKQ5The matcher must be typo-resistant, not merely subsequence-based — a transposition or a dropped character must still rank, which is the difference from fzf.full (working tree)exact bounded-deletion witness
PKQ6Match positions must be returned so the list can highlight what matched.full (working tree)canonical merged byte ranges

Ranking (PKR)

IDRequirementStatusTraces to
PKR1Results must be ranked by the composite formula above — a fuzzy base plus frecency, git status, path distance, filename quality and path alignment — not by match score alone.full (working tree)sparkles.fuzzy.rank
PKR2Frecency must decay exponentially (a 10-day half-life over a 30-day window, capped at 128 timestamps per file), so recently and repeatedly opened files rank above cold ones.full in memory (working tree)fixed Q16 FrecencyTable
PKR3A query-history combo boost must rank a file the same query previously opened.full in memory (working tree)bounded ComboTable
PKR4Every result must carry its score breakdown, and a debug toggle must show it — a ranking nobody can inspect is one nobody can fix.partialbreakdown + shared debug view exist; host mount pending
PKR5Frecency and query history must persist through the configuration layer's state directory, not a second storage mechanism, and must never make hue fail to start.not startedsparkles:wired; common_dirs
PKR6The persistence read/write is the one place @nogc is not required (it is startup/shutdown I/O, the NFR1 carve-out); the in-memory table and every scoring path must be @nogc.partialin-memory path ships; persistence I/O pending

Sources (PKS)

Each row is one <leader> binding. The map reserves them all today.

IDSourceKeyRequirementStatus
PKS1files<leader>ffThe .gitignore-aware walk, searched on the raw CPU pool, honouring the tree pane's include/exclude globs.partial — bound and mounted in both hosts; corpus walk is synchronous
PKS2grep<leader>/Content search in three modes — plain, regex, fuzzy — auto-detected, falling back to fuzzy on zero hits.not started
PKS3recent<leader>frFrecency-ordered previously opened documents (PKR2).not started
PKS4open documents<leader>,The current SourceSet (SRC6) — the substrate the tab view shares.not started
PKS5git status<leader>gsChanged files, from the existing cache rather than a new git invocation.not started
PKS6git commits<leader>gcCommits, opening the revision as a diff session.not started
PKS7themes<leader>stThe built-in theme list (THM2), applying live as the selection moves.not started
PKS8lines<leader>slLines of the current document — the in-document search, as a picker.not started
PKS9keymaps<leader>skhueBindings itself. Free once the table exists (KEY3), and the honest test of PIK4.not started
PKS10git files<leader>fgTracked files only, skipping the walk where a repository can answer faster.not started

Layout & actions (PKL)

IDRequirementStatusTraces to
PKL1Layouts must be selectable: default (list + preview side by side), vscode (a centred dropdown, no preview), select (small, for a short list).partialall presets exist in shared tree; final sizing/host mount pending
PKL2The preview must reuse DocumentPipeline.load and ViewerModel — the picker introduces no second rendering path.fullpicker_preview.d: a PreviewTui over the host's own loader
PKL3Actions must be bindings in the one table (KEY1), so the guide lists what a picker's keys do exactly as it lists everything else.fullhueBindings picker* scopes; keymap.pickerScopesAreModalAndFocusRouted
PKL4Rows must be tappable, and the prompt must accept the soft keyboard, so the picker is usable on Android where the leader menu is the only command surface.not startedandroid.md
PKL5<S-Tab> must cycle the grep mode, with the active mode shown; a single-mode configuration must hide the indicator. (Today Shift-Tab reverses the pane focus, PKL7; a grep source claims it back with a context-gated row when P4 lands.)not startedfff.nvim's affordance
PKL6A grep result must classify definition lines (struct/fn/class/def/impl), so a definition can be ranked and marked above a mention.not startedfff's classifier
PKL7The picker's panes must have a focus model (the snacks.picker shape, framework-owned — FOC2/FOC4): prompt/list/preview cycled by Tab, per-pane key routing from the one table, focus-dependent chrome (highlight border, accent title, caret only in the prompt, selection bar dims under a focused preview), a printable always a query edit, and a focused preview forwarding unbound keys to the real document pane.fullPickerHost.focus (ScopeFocus); picker_view chrome; picker.host.focusCyclesAndRoutesKeys; workspace.leaderFfMountsThePicker

sparkles:fuzzy (PKM)

A new library — libs/fuzzy — because the matcher is a self-contained, testable, benchmarkable engine with no dependency on hue, exactly as sparkles:diff is. Its contract-level design now lives in its own spec — docs/specs/fuzzy/SPEC.md, milestoned in PLAN.md — grounded in the fuzzy-matching research catalog; the rows below are hue's requirements on it, traced there.

IDRequirementStatusTraces to
PKM1Every shipped fuzzy entry point and built-in analyzer must be @safe pure nothrow @nogc; storage is caller-owned and fixed-capacity, and errors are explicit values.full (working tree)attributed public API + fixed workspaces
PKM2Unittests must carry those attributes explicitly and instrument allocator calls, because @nogc alone does not prove allocation freedom.full (working tree)attributed tests + calibrated libc-wrap/GC audit of the complete keystroke path
PKM3Every returned span must borrow from the caller's input; the library must own no string.full (working tree)DIP1000-safe slice bridges
PKM4Scoring must be benchmarked (@benchmark) from the first commit, since a picker's whole value is that it answers within a frame.full (working tree)reused-workspace @benchmark
PKM5It must ship a docs/libs/fuzzy/ Diátaxis tree from the package scaffold onward, as AGENTS.md requires of a new library.full (working tree)docs/libs/fuzzy/
PKM6A bigram prefilter should narrow candidates before content scoring, once the grep source's scale justifies it. Deferred, and recorded so it is not re-derived.researchedPLAN § deferred — the catalog records it is a content index, not a path prefilter

Milestones

MilestoneScopeStatusRequirements
F0sparkles:fuzzy — analyzers, query/glob, exact matching, ranking, bounded history, and chunked search (PLAN M0–M7)implemented and verifiedPKM1PKM5, PKQ*, PKR1PKR3, library PKR4
P0Picker state/Finder/files source, generation scheduler, synchronous fallback, and visible ranking debug togglefull — <leader>ff opens it in both hosts; Ctrl-S shows the breakdownPIK1PIK8, PKS1, UI PKR4
P1The view, both backends, and the layoutspartial — mounted in both backends; preset switching pendingPIK3, PKL1
P2The preview panefull — a real document pane; the live-types oracle starts after a 2 s dwellPKL2
P3Versioned bounded frecency/query-history persistence and the recent sourcenot startedPKR5, PKR6, PKS3
P4The grep source: three modes, the definition classifiernot startedPKS2, PKL5, PKL6
P5The remaining sources, actions, and resumenot startedPKS4PKS10, PIK9
P6Touch: tappable rows and the soft keyboardnot startedPKL4

Module coverage (proposed)

The rows name the intended ownership; implementation status is tracked by the milestone tables above.

Source (proposed)Requirements
libs/fuzzy/src/sparkles/fuzzy/PKM*, PKQ*, PKR1PKR3
apps/hue/src/picker.dPIK1, PIK2, PIK5PIK9
apps/hue/src/picker_host.dthe host glue both backends share (PIK1)
apps/hue/src/picker_preview.dthe preview document pane (PKL2)
apps/hue/src/picker_sources.dPIK4, PKS*
apps/hue/src/picker_view.dPIK3, PKL1, PKL4
apps/hue/src/keymap.dPKL3 (the picker's bindings)

Relationship to existing specs

PieceRole
sparkles:fuzzy SPEC + PLANthe engine's own contract-level spec and milestones (F0)
fuzzy-matching researchthe prior-art evidence base behind both specs
lantern.md LMP7/LMP8the reserved keys this opens, and the table its actions join
tree-view.md TVU1the explorer this complements — browse there, find here
feature-requirements.md SRC6the document set PKS4 picks from
diff-view.mdwhat PKS6 opens, and the NFR8 budget this shares
config.mdwhere the frecency store and the picker's defaults live
sparkles:event-horizonthe measured work-stealing walker the file and grep sources fan out on
sparkles:build-primitivesthe .gitignore-aware walk PKS1 reuses

Lantern requirements · Tree / DAG view · Overview