- Zig 100%
|
Mikael Säker
22a36a8e8f
docs: rename zsift -> sift in comments
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| src | docs: rename zsift -> sift in comments | |
| .gitignore | Ignore and untrack zig-pkg/ | |
| build.zig | Drop zmatch dep — matcher is plugged in by the caller | |
| build.zig.zon | Drop zmatch dep — matcher is plugged in by the caller | |
| README.md | Drop zmatch dep — matcher is plugged in by the caller | |
zpicker
A small UI-agnostic state machine for filtered list pickers — the kind used for command palettes, file pickers, buffer switchers, and similar "type to narrow, arrow to choose" widgets. Zero dependencies — the caller plugs in their own scoring function, so any matcher (fuzzy, prefix, substring, your own) works. Intended to be embedded in editors and tools that handle their own rendering.
The package owns:
- Query buffer with codepoint-safe
appendQuery/backspaceQuery/setQuery/clearQueryfor tail-only editing, plusinsertAt/deleteForward/moveCursor*for mid-string editing (command palettes, search bars). - Filtered result list with scores and per-item match positions (byte offsets, suitable for highlight rendering).
- Selection cursor and scroll offset with the usual
moveUp/moveDown/pageUp/pageDown/moveHome/moveEnd. setItemsto swap in a new borrowed item slice and re-filter.refilterWith(ctx, scoreFn)— caller-supplied scoring for cases where fuzzy alone isn't enough (e.g. alias-bonus scoring in a command palette where an exact alias should outrank a fuzzy match on the long command name).
The package deliberately does not own:
- Rendering — the caller draws the query, list rows, scroll bar, and any preview pane.
- Item sources — the caller passes a
[]const []const u8slice; the picker borrows it. - Actions — pressing Enter, mouse clicks, and focus handling are caller
concerns. Use
selectedEntry/selectedItemto read state.
Quick start
constzpicker=@import("zpicker");varpicker=tryzpicker.Picker.init(allocator,.{});deferpicker.deinit();// Plug in a scorer. The context is opaque to zpicker — typically a// pointer to your matcher or any state it needs to score items.picker.setScorer(&my_matcher,fuzzyScore);picker.setItems(items);// borrowed; outlives the picker or replace via setItems_=picker.appendQuery("foo");picker.moveDown(visible_rows);if(picker.selectedItem())|item|{// Enter: open `item`, etc.}fnfuzzyScore(ctx:?*anyopaque,_:usize,item:[]constu8,query:[]constu8)?u16{constmatcher:*MyMatcher=@ptrCast(@alignCast(ctx.?));returnmatcher.match(item,query);}Without a registered scorer, an empty query passes all items through in source order and a non-empty query produces an empty filtered list.
Mid-string editing
For command palettes and search inputs that need cursor movement and insertion at arbitrary positions:
_=picker.insertAt("text");// insert at cursor_pos, advance cursorpicker.moveCursorLeft();// step left by one codepointpicker.moveCursorRight();// step right by one codepointpicker.moveCursorHome();// Ctrl-Apicker.moveCursorEnd();// Ctrl-Epicker.backspaceQuery();// delete codepoint before cursorpicker.deleteForward();// delete codepoint at cursor (Del key)appendQuery keeps the cursor at the end; mix the two freely.
One-shot custom scoring
refilterWith is a comptime-typed variant for cases where you want
to score against something other than the picker's query (e.g. a
file-completion path with a prefix extracted from the full query):
constScorer=struct{fnscore(commands:[]constCommand,index:usize,item:[]constu8,// = commands[index].namequery:[]constu8,)?u16{constcmd=commands[index];for(cmd.aliases)|a|{if(std.mem.eql(u8,a,query))return10000;if(a.len>query.lenandstd.mem.eql(u8,a[0..query.len],query))return5000;}returnmy_matcher.match(cmd.name,query);}};picker.refilterWith(commands,Scorer.score);The context is anytype, so any struct, slice, or scalar works. The
registered default scorer (from setScorer) is not replaced; it
takes back over on the next state change.
Match positions
zpicker doesn't store match positions in FilteredEntry — at 100K
items the difference is 1 MB vs. 28 MB of memory. Recompute them at
render time for just the visible rows (typically ~50) by calling
your matcher's matchIndices (or equivalent) for each row about to
be drawn.
Config
zpicker.Picker.init(allocator,.{.max_query_bytes=128,// fixed-size query buffer.max_items=4096,// filtered list cap});License
MIT.