Skip to content

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

Status: design · Date: 2026-08-07 · 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

Everything here is forward-looking design; no picker code exists on any branch. The lantern map already reserves the letters this claims, so landing it rearranges nothing a user has learnt. 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 --show-config CFG10.

Interactivity is a budget, not a promise (PIK5)

A grep over a large repository cannot block a frame, and hue's loops are synchronous. fff's answer is the right one and is already the shape hue's diff work needs (NFR8): every search takes a time budget, an abort flag and a resumable file offset, and returns partial results plus where to continue.

The fan-out runs on sparkles:event-horizon's cpuBound WorkStealingPool — the ring-less, plain-thread mode 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

WorkStealingPool.start can fail — the walker benchmark prints SKIP: io_uring unavailable — and its availability on Android and macOS is unverified. cpuBound skips the ring, but the picker must fall back to a synchronous budget-stepped walk rather than lose 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.not startedproposed apps/hue/src/picker.d
PIK2Its state must be a presentation-free value — the prompt line editor (LineEditState), the scored items, the selection and the scroll offset — testable with no canvas, like every other STM machine.not startedsparkles.ui.state.LineEditState/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.not startedproposed picker_view.d
PIK4A source must be a DbI seam: anything that can produce items incrementally is a source, and adding one must not touch the component.not startedproposed Finder seam
PIK5Every search must take a time budget, an abort flag and a resumable file offset, and return partial results with a continuation cursor — so a large repository never blocks a frame.not startedSearchOptions/SearchResult
PIK6Results must be written into a caller-owned sink, and paths held in one contiguous arena with a filename offset, so a query costs no allocation beyond the arena's growth.not startedSmallBuffer; fff's FileItem shape
PIK7Re-running a query must cancel in-flight work rather than racing it — a generation counter the workers check.not startedproposed generation counter
PIK8The picker must degrade to a synchronous budget-stepped walk when the work-stealing pool cannot start, rather than being unavailable.not startedWorkStealingPool.start failure path
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.not startedproposed sparkles.fuzzy.query
PKQ2Constraints must cover *.ext, a glob, a path segment, a file path suffix, and git:<status> (modified/staged/untracked/ignored).not startedgit_status.d supplies the status
PKQ3Any constraint must be negatable (!test/, !*.rs), with a minimum length on text exclusions so operators like != are not mistaken for one.not startedfff's rule
PKQ4A trailing :line[:col] must parse as a location, so pasting src/app.d:120 from a compiler diagnostic opens where it points.not startedfff's Location
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.not startedsparkles.fuzzy.score
PKQ6Match positions must be returned so the list can highlight what matched.not startedsparkles.fuzzy.score

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.not startedsparkles.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.not startedsparkles.fuzzy.frecency
PKR3A query-history combo boost must rank a file the same query previously opened.not startedsparkles.fuzzy.frecency
PKR4Every result must carry its score breakdown, and a debug toggle must show it — a ranking nobody can inspect is one nobody can fix.not startedproposed Score struct
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.not startedNFR1

Sources (PKS)

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

IDSourceKeyRequirementStatus
PKS1files<leader>ffThe .gitignore-aware walk, fanned out on the cpuBound pool, honouring the tree pane's include/exclude globs.not started
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).not startedsnacks.picker's presets
PKL2The preview must reuse DocumentPipeline.load and ViewerModel — the picker introduces no second rendering path.not starteddocument.d; viewer_model.d
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.not startedhueBindings picker scope
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.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

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.

IDRequirementStatusTraces to
PKM1The library must be 100% @safe pure nothrow @nogc, with SmallBuffer as its only dynamic container and Expected for errors — no exceptions.not startedlibs/fuzzy
PKM2Its unittests must carry those attributes explicitly, so an accidental allocation is a compile error rather than a review note.not startedthe repo's existing idiom
PKM3Every returned span must borrow from the caller's input; the library must own no string.not startedPKQ1
PKM4Scoring must be benchmarked (@benchmark) from the first commit, since a picker's whole value is that it answers within a frame.not startedlibs/fuzzy/bench
PKM5It must ship a docs/libs/fuzzy/ Diátaxis tree, as AGENTS.md requires of a new library.not starteddocs/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.not startedfff's index/bigram_filter.rs

Milestones

MilestoneScopeRequirements
F0sparkles:fuzzy — query parser, scoring, ranking, globPKM1PKM5, PKQ*, PKR1
P0The picker state machine, the Finder seam, and the files sourcePIK1PIK8, PKS1
P1The view, both backends, and the layoutsPIK3, PKL1
P2The preview panePKL2
P3Frecency + query-history persistence; the recent sourcePKR2PKR6, PKS3
P4The grep source: three modes, the definition classifierPKS2, PKL5, PKL6
P5The remaining sources, actions, and resumePKS4PKS10, PKL3, PIK9
P6Touch: tappable rows and the soft keyboardPKL4

Module coverage (proposed)

No code on any branch yet.

Source (proposed)Requirements
libs/fuzzy/src/sparkles/fuzzy/PKM*, PKQ*, PKR1PKR3
apps/hue/src/picker.dPIK1, PIK2, PIK5PIK9
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
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