Code to scan filesystem for the file picker

This commit is contained in:
Kovid Goyal
2025-05-20 21:15:36 +05:30
parent 56b26838ce
commit c158fde734
2 changed files with 149 additions and 2 deletions

View File

@@ -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

View File

@@ -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()
}
}()
}