From c158fde7342094e94153c3472034480c41589312 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 20 May 2025 21:15:36 +0530 Subject: [PATCH] Code to scan filesystem for the file picker --- kittens/choose_files/main.go | 6 +- kittens/choose_files/scan.go | 145 +++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 kittens/choose_files/scan.go diff --git a/kittens/choose_files/main.go b/kittens/choose_files/main.go index 8b09061bf..720579868 100644 --- a/kittens/choose_files/main.go +++ b/kittens/choose_files/main.go @@ -39,10 +39,12 @@ type ScreenSize struct { type Handler struct { state State screen_size ScreenSize + scan_cache ScanCache lp *loop.Loop } func (h *Handler) draw_screen() (err error) { + h.get_results() h.lp.StartAtomicUpdate() defer h.lp.EndAtomicUpdate() h.lp.ClearScreen() @@ -52,7 +54,6 @@ func (h *Handler) draw_screen() (err error) { } else { y += dy } - return } @@ -81,7 +82,7 @@ func (h *Handler) OnKeyEvent(ev *loop.KeyEvent) (err error) { switch { case h.handle_edit_keys(ev): h.draw_screen() - case ev.MatchesPressOrRepeat("esc"): + case ev.MatchesPressOrRepeat("esc") || ev.MatchesPressOrRepeat("ctrl+c"): h.lp.Quit(1) } return @@ -118,6 +119,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) { } lp.OnKeyEvent = handler.OnKeyEvent lp.OnText = handler.OnText + lp.OnWakeup = func() error { return handler.draw_screen() } err = lp.Run() if err != nil { return 1, err diff --git a/kittens/choose_files/scan.go b/kittens/choose_files/scan.go new file mode 100644 index 000000000..d0d9a34f7 --- /dev/null +++ b/kittens/choose_files/scan.go @@ -0,0 +1,145 @@ +package choose_files + +import ( + "cmp" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "sync" + + "github.com/kovidgoyal/kitty/tools/tui/subseq" + "github.com/kovidgoyal/kitty/tools/utils" +) + +var _ = fmt.Print + +type ResultItem struct { + text, abspath string + dir_entry os.DirEntry + positions []int // may be nil +} + +func (r ResultItem) String() string { + return fmt.Sprintf("{text: %#v, abspath: %#v, is_dir: %v, positions: %#v}", r.text, r.abspath, r.dir_entry.IsDir(), r.positions) +} + +type dir_cache map[string][]ResultItem + +type ScanCache struct { + dir_entries dir_cache + mutex sync.Mutex + root_dir, search_text string + in_progress bool + matches []ResultItem +} + +func (sc *ScanCache) get_cached_entries(root_dir string) (ans []ResultItem, found bool) { + sc.mutex.Lock() + defer sc.mutex.Unlock() + ans, found = sc.dir_entries[root_dir] + return +} + +func (sc *ScanCache) set_cached_entries(root_dir string, e []ResultItem) { + sc.mutex.Lock() + defer sc.mutex.Unlock() + sc.dir_entries[root_dir] = e +} + +func scan_dir(path, root_dir string) []ResultItem { + if ans, err := os.ReadDir(path); err == nil { + if rel, err := filepath.Rel(root_dir, path); err == nil { + return utils.Map(func(x os.DirEntry) ResultItem { + path := filepath.Join(root_dir, x.Name()) + return ResultItem{dir_entry: x, abspath: path, text: filepath.Join(rel, x.Name())} + }, ans) + } + } + return []ResultItem{} +} + +func (sc *ScanCache) fs_scan(root_dir, current_dir string, max_depth int) (ans []ResultItem) { + var found bool + if ans, found = sc.get_cached_entries(current_dir); !found { + ans = scan_dir(current_dir, root_dir) + sc.set_cached_entries(current_dir, ans) + } + for _, x := range ans { + ans = append(ans, x) + if x.dir_entry.IsDir() && max_depth > 0 { + ans = append(ans, sc.fs_scan(root_dir, x.abspath, max_depth-1)...) + } + } + return +} + +func (sc *ScanCache) scan(root_dir, search_text string, max_depth int) (ans []ResultItem) { + if strings.HasPrefix(search_text, "/") { + root_dir = "/" + } + ans = slices.Clone(sc.fs_scan(root_dir, root_dir, max_depth)) + if search_text == "" { + slices.SortFunc(ans, func(a, b ResultItem) int { + switch a.dir_entry.IsDir() { + case true: + switch b.dir_entry.IsDir() { + case true: + return strings.Compare(strings.ToLower(a.text), strings.ToLower(b.text)) + case false: + return -1 + } + case false: + switch b.dir_entry.IsDir() { + case true: + return 1 + case false: + return strings.Compare(strings.ToLower(a.text), strings.ToLower(b.text)) + } + } + return 0 + }) + } else { + pm := make(map[string]ResultItem, len(ans)) + for _, x := range ans { + pm[x.text] = x + } + matches := subseq.ScoreItems(search_text, utils.Keys(pm), subseq.Options{}) + slices.SortFunc(matches, func(a, b *subseq.Match) int { return cmp.Compare(b.Score, a.Score) }) + ans = utils.Map(func(m *subseq.Match) ResultItem { + x := pm[m.Text] + x.positions = m.Positions + return x + }, matches) + } + return ans +} + +func (h *Handler) get_results() { + sc := &h.scan_cache + sc.mutex.Lock() + defer sc.mutex.Unlock() + if sc.dir_entries == nil { + sc.dir_entries = make(dir_cache, 512) + } + if sc.root_dir == h.state.CurrentDir() && sc.search_text == h.state.SearchText() { + return + } + sc.in_progress = true + sc.matches = nil + root_dir := h.state.CurrentDir() + search_text := h.state.SearchText() + sc.root_dir = root_dir + sc.search_text = search_text + go func() { + results := sc.scan(root_dir, search_text, h.state.MaxDepth()) + sc.mutex.Lock() + defer sc.mutex.Unlock() + if root_dir == sc.root_dir && search_text == sc.search_text { + sc.matches = results + sc.in_progress = false + h.lp.WakeupMainThread() + } + }() +}