diff options
Diffstat (limited to 'gui/internal/model/filter.go')
| -rw-r--r-- | gui/internal/model/filter.go | 107 |
1 files changed, 107 insertions, 0 deletions
diff --git a/gui/internal/model/filter.go b/gui/internal/model/filter.go new file mode 100644 index 0000000..af617c3 --- /dev/null +++ b/gui/internal/model/filter.go @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package model + +import ( + "strings" + + "krino/internal/norm" +) + +// FuzzyMatch reports whether every character of pattern appears in text in +// order - the way fzf matches - and how good the match is. Capitals and +// accents are ignored, using krino's own folding, so "zazolc" finds +// "zażółć" exactly as a rule with (fold yes) would (his request, +// 2026-09-17). +// +// The score rewards characters that follow one another and those at the +// start of a word, so "lec" ranks "lectio-2026.pdf" above +// "old-latex-certificate.pdf". +func FuzzyMatch(pattern, text string) (score int, ok bool) { + p := []rune(norm.Fold(strings.ToLower(pattern))) + t := []rune(norm.Fold(strings.ToLower(text))) + if len(p) == 0 { + return 0, true + } + if len(p) > len(t) { + return 0, false + } + pi := 0 + run := 0 + for ti := 0; ti < len(t) && pi < len(p); ti++ { + if t[ti] != p[pi] { + run = 0 + continue + } + score += 1 + run // a run of matching characters is worth more + if ti == 0 || isWordBreak(t[ti-1]) { + score += 4 // the start of a word counts for more still + } + run++ + pi++ + } + if pi < len(p) { + return 0, false + } + // A short name matching is a better match than a long one. + if len(t) > 0 { + score += 20 * len(p) / len(t) + } + return score, true +} + +// isWordBreak reports whether a character separates words in a file name. +func isWordBreak(r rune) bool { + switch r { + case ' ', '.', '-', '_', '/', ',', '(', ')', '[', ']': + return true + } + return false +} + +// Matching is the rows a filter leaves, in the plan's own order - which is +// what a list of files is read in - not in score order. An empty pattern +// matches everything. +func (t *PlanTab) Matching(pattern string) []int { + var out []int + for i, r := range t.Rows { + if _, ok := FuzzyMatch(pattern, r.Rel); ok { + out = append(out, i) + continue + } + // The rule a file took is worth searching too: "to-sort" finds + // everything that rule claimed. + if _, ok := FuzzyMatch(pattern, r.Rule); ok { + out = append(out, i) + } + } + return out +} + +// SelectOnly checks exactly the rows given - a filtered "Select all" - and +// clears the rest, so what is applied is what was on screen. +func (t *PlanTab) SelectOnly(rows []int) { + want := map[int]bool{} + for _, i := range rows { + want[i] = true + } + for i := range t.Rows { + t.Rows[i].Selected = want[i] && t.Rows[i].Actable + } +} + +// HiddenSelected is how many checked files a filter is hiding, so the +// window can say so rather than apply something out of sight. +func (t *PlanTab) HiddenSelected(shown []int) int { + visible := map[int]bool{} + for _, i := range shown { + visible[i] = true + } + n := 0 + for i, r := range t.Rows { + if r.Selected && !visible[i] { + n++ + } + } + return n +} |
