Repository navigation
feat(browse): fuzzy-find the listing with / and make q always quit - #29
Merged
Merged
Conversation
The browser pane had no working exit once a file was open: q was gated on no viewer being loaded, so the only way out was Tab into the viewer and quit from there. The gate is gone - q quits from either pane. New src/browser/find.rs ranks the listing against a live query: subsequence match, bonuses for word boundaries and consecutive runs, gap penalties, shorter-name tie-break, and fzf smart case. It is hand-rolled (~80 lines of matcher) rather than a crate, so nothing is added to Cargo.toml. Match positions are char indices, which is what lets ui.rs highlight them without byte arithmetic. Anchored greedy retried from every occurrence of the first query char, because a single greedy pass mis-scores the tightest run. BrowserApp::cursor now indexes matches instead of entries and is resolved through selected_entry()/selected_index(). The prompt owns the keyboard while it is open, checked right after the download prompt and for the same reason: q, d and T have to arrive as query text. Covered by 44 new unit tests and qa.sh cases X10-X16. X5 also stops asserting on a ctrl-h binding that never existed - tab is the focus toggle - and now checks the shortcut bar, which is what actually reflects focus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md testing guidance and the qa.sh launch() comment both justified respawning the pane with a rule that no longer holds. The justification still stands, it just has a different cause now: prompts that accept typed text - search, filter, export, browse find - take q as a character rather than as a quit. Also lists / and q on the browser events.rs module line, and drops a line-count claim from the changelog entry that the file does not bear out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The browse pane had two gaps:
qdid not quit it, and there was no way to find a file in a long listing. This closes both.qalways quitsThe binding was gated on
app.viewer.is_none(), so the moment a file was open the browser pane had no exit at all — the only way out wasTabinto the viewer and quit from there. The gate is gone:qquits from either pane, with or without a file open, and the shortcut bar now offers it in both focus states./fuzzy-finds the listingAn fzf-style live filter.
/opens a prompt that owns the keyboard; the listing narrows as you type and the matched characters are highlighted.EnterEsc↑/↓,ctrl-n/ctrl-pctrl-uBackspaceThe query matches anywhere in a name, in order, so
ordcsvfindsorders.csv. Ranking favours hits on word boundaries (_,-,.,/, space, camelCase humps) and runs of consecutive characters; gaps cost; the shorter name wins a tie. Case follows the fzf rule — a lowercase query is case-insensitive, one uppercase character makes the whole query case-sensitive.Why the matcher is hand-rolled
No new dependency —
Cargo.tomlis untouched. The alternative wasnucleo-matcher, which is MPL-2.0; that alone was not disqualifying (option-ext, also MPL-2.0, is already in the tree viadirs), but the whole matcher is about a hundred lines and a crate is not worth the upgrade surface for that.Three details worth a reviewer's attention in
src/browser/find.rs:abagainsta_xb_ab— it locks onto the firstaand never finds the tight run at the end.positionsare char indices, not byte offsets. That is what letsui.rshighlight without byte arithmetic on non-ASCII names.c.to_lowercase().next()).flat_map(char::to_lowercase)would expandßtossand desynchronise the match positions from the name being highlighted.Architecture notes
BrowserApp::cursornow indexesmatches, notentries. Resolve it throughselected_entry()/selected_index(), neverentries[cursor]. With no query activematchesis still every entry, so the two only diverge while filtering.run_browser_appis download prompt → find prompt → theme picker → single-key bindings. The find prompt has to consume keys before the browse bindings soq,dandTarrive as query text.close_find()is the single exit for bothEnterandEsc: it restores the full listing and leaves the cursor on whatever was highlighted, so opening it afterwards is the ordinary unfiltered path.Tests
the_tightest_run_wins_over_the_earliest_one), smart case in both directions,positions_are_char_indices_not_byte_offsets, the prompt swallowing the browse bindings, andq_quits_with_a_file_already_open— which pins the reported bug.qa.shcases X10–X16, per the CLAUDE.md rule that keybinding changes are covered before the PR:/narrows,Escrestores,Enteropens the top match, the prompt swallowsq/d/T,ctrl-nwalks the matches, andqquits with a viewer loaded. Full suite: 192 passed, 0 failed.ctrl-hbinding that never existed (Tabis the focus toggle) and now checks the shortcut bar, which actually reflects focus.cargo test,cargo clippy -- -D warnings,cargo fmt --check— all clean.Docs
CHANGELOG (
Addedfor/,Fixedforq), README (feature bullet, browse keybindings, a paragraph on ranking and smart case), CLAUDE.md (find.rsin the module list, two gotchas, keybinding table). The CLAUDE.md testing guidance and theqa.shlaunch()comment both justified respawning the pane with "qis ignored in browse mode" — the rule this PR deletes — so both were rewritten around the cause that still holds: prompts that accept typed text takeqas a character.Not included
.github/assets/demo.gifwas not regenerated, so the browse phase of the demo still does not show/. It needsvhs+ttyd.🤖 Generated with Claude Code