aboutsummaryrefslogtreecommitdiff
path: root/gui/internal/model/filter.go
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 00:51:15 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-09-17 00:51:15 +0200
commit5418ba6bc5c653b6a99572c6ece6abbf57836c43 (patch)
treeae0a430c468890770d20f531c3a026937c9bb948 /gui/internal/model/filter.go
parentca0b703526151149d060d6d38f307e9e38c3dcd2 (diff)
downloadkrino-5418ba6bc5c653b6a99572c6ece6abbf57836c43.tar.gz
krino-5418ba6bc5c653b6a99572c6ece6abbf57836c43.zip
gui: coloured actions with headers, a filter over the plan, bulk trash or delete
Diffstat (limited to 'gui/internal/model/filter.go')
-rw-r--r--gui/internal/model/filter.go107
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
+}