diff --git a/kittens/choose_files/main.go b/kittens/choose_files/main.go index 720579868..f09479406 100644 --- a/kittens/choose_files/main.go +++ b/kittens/choose_files/main.go @@ -44,16 +44,16 @@ type Handler struct { } func (h *Handler) draw_screen() (err error) { - h.get_results() + matches, in_progress := h.get_results() h.lp.StartAtomicUpdate() defer h.lp.EndAtomicUpdate() h.lp.ClearScreen() - y := 0 - if dy, err := h.draw_search_bar(y); err != nil { - return err - } else { - y += dy - } + defer func() { // so that the cursor ends up in the right place + h.lp.MoveCursorTo(1, 1) + h.draw_search_bar(0) + }() + y := SEARCH_BAR_HEIGHT + y += h.draw_results(y, 4, matches, in_progress) return } diff --git a/kittens/choose_files/results.go b/kittens/choose_files/results.go new file mode 100644 index 000000000..8e6841f77 --- /dev/null +++ b/kittens/choose_files/results.go @@ -0,0 +1,172 @@ +package choose_files + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "unicode/utf8" + + "github.com/kovidgoyal/kitty/tools/tui/loop" + "github.com/kovidgoyal/kitty/tools/utils" + "github.com/kovidgoyal/kitty/tools/utils/style" + "github.com/kovidgoyal/kitty/tools/wcswidth" +) + +var _ = fmt.Print + +func (h *Handler) draw_results_title() { + text := filepath.Clean(h.state.BaseDir()) + home := filepath.Clean(utils.Expanduser("~")) + if strings.HasPrefix(text, home) { + text = "~" + text[len(home):] + } + available_width := h.screen_size.width - 9 + if available_width < 2 { + return + } + tt := wcswidth.TruncateToVisualLength(text, available_width) + if len(tt) < len(text) { + text = wcswidth.TruncateToVisualLength(text, available_width-1) + } + text = ` 📁 ` + text + ` ` + extra := available_width - wcswidth.Stringwidth(text) + x := 3 + if extra > 1 { + x += extra / 2 + } + h.lp.MoveCursorHorizontally(x) + h.lp.QueueWriteString(text) +} + +func (h *Handler) draw_no_matches_message(in_progress bool) { + text := "Scanning filesystem, please wait…" + if !in_progress { + text = utils.IfElse(h.state.SearchText() == "", "No files present in this folder", "No matches found") + } + for _, line := range style.WrapTextAsLines(text, h.screen_size.width-2, style.WrapOptions{}) { + h.lp.QueueWriteString("\r") + h.lp.MoveCursorHorizontally(1) + h.lp.QueueWriteString(line) + h.lp.MoveCursorVertically(1) + } + +} + +func (h *Handler) draw_matching_result(r ResultItem) { +} + +func (h *Handler) render_match_with_positions(text string, stop_at int, positions []int, scale int) { + prefix, suffix, _ := strings.Cut(h.lp.SprintStyled("fg=green", " "), " ") + write_chunk := func(text string, emphasize bool) { + if text == "" { + return + } + if emphasize { + h.lp.QueueWriteString(prefix) + defer func() { + h.lp.QueueWriteString(suffix) + }() + } + if scale > 1 { + h.lp.DrawSizedText(text, loop.SizedText{Scale: scale}) + } else { + h.lp.QueueWriteString(text) + } + } + at := 0 + limit := min(stop_at, len(text)) + for _, p := range positions { + if p > limit || at > limit { + break + } + write_chunk(text[at:p], false) + at = p + if r, sz := utf8.DecodeRuneInString(text[p:]); r != utf8.RuneError { + write_chunk(string(r), true) + at += sz + } + } + if at < len(text) { + write_chunk(text[at:], false) + } +} + +func icon_for(x os.DirEntry) string { + if x.IsDir() { + return `📁` + } + return "XX" +} + +func (h *Handler) draw_column_of_matches(matches []ResultItem, x, available_width int, has_extra_matches bool) { + for i, m := range matches { + h.lp.QueueWriteString("\r") + h.lp.MoveCursorHorizontally(x) + text := "" + if has_extra_matches && i == len(matches)-1 { + text = "…" + } else { + text = m.text + tlen := len(text) + if wcswidth.Stringwidth(text) > available_width-3 { + text = wcswidth.TruncateToVisualLength(text, available_width-4) + "…" + tlen = len(text) - 1 + } + h.lp.QueueWriteString(icon_for(m.dir_entry) + " ") + h.render_match_with_positions(text, tlen, m.positions, 1) + } + h.lp.MoveCursorVertically(1) + } +} + +func (h *Handler) draw_list_of_results(matches []ResultItem, y, height int) { + if len(matches) == 0 || height < 2 { + return + } + available_width := h.screen_size.width - 2 + col_width := available_width + num_cols := 1 + if len(matches) > height { + col_width = 40 + num_cols = available_width / col_width + for num_cols > 0 && height*(num_cols-1) >= len(matches) { + num_cols-- + } + col_width = available_width / num_cols + } + x := 1 + for i := range num_cols { + is_last := i == num_cols-1 + chunk := matches[:min(len(matches), height)] + matches = matches[len(chunk):] + h.lp.MoveCursorTo(x, y) + has_extra_matches := is_last && len(matches) > 0 + h.draw_column_of_matches(chunk, x, col_width-1, has_extra_matches) + x += col_width + } +} + +func (h *Handler) draw_results(y, bottom_margin int, matches []ResultItem, in_progress bool) (height int) { + height = h.screen_size.height - y - bottom_margin + h.lp.MoveCursorTo(1, 1+y) + h.draw_frame(h.screen_size.width, height) + h.lp.MoveCursorTo(1, 1+y) + h.draw_results_title() + y += 2 + h.lp.MoveCursorTo(1, y) + switch len(matches) { + case 0: + h.draw_no_matches_message(in_progress) + default: + switch h.state.SearchText() { + case "": + h.draw_list_of_results(matches, y, height-y) + default: + h.draw_matching_result(matches[0]) + y += 2 + h.draw_list_of_results(matches[1:], y, height-y) + } + } + return +} diff --git a/kittens/choose_files/scan.go b/kittens/choose_files/scan.go index d0d9a34f7..103534586 100644 --- a/kittens/choose_files/scan.go +++ b/kittens/choose_files/scan.go @@ -66,10 +66,13 @@ func (sc *ScanCache) fs_scan(root_dir, current_dir string, max_depth int) (ans [ 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)...) + ans = slices.Clone(ans) + // now recurse into directories + if max_depth > 0 { + for _, x := range ans { + if x.dir_entry.IsDir() { + ans = append(ans, sc.fs_scan(root_dir, x.abspath, max_depth-1)...) + } } } return @@ -79,7 +82,7 @@ func (sc *ScanCache) scan(root_dir, search_text string, max_depth int) (ans []Re if strings.HasPrefix(search_text, "/") { root_dir = "/" } - ans = slices.Clone(sc.fs_scan(root_dir, root_dir, max_depth)) + ans = 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() { @@ -105,7 +108,9 @@ func (sc *ScanCache) scan(root_dir, search_text string, max_depth int) (ans []Re for _, x := range ans { pm[x.text] = x } - matches := subseq.ScoreItems(search_text, utils.Keys(pm), subseq.Options{}) + matches := utils.Filter(subseq.ScoreItems(search_text, utils.Keys(pm), subseq.Options{}), func(x *subseq.Match) bool { + return x.Score > 0 + }) 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] @@ -116,7 +121,7 @@ func (sc *ScanCache) scan(root_dir, search_text string, max_depth int) (ans []Re return ans } -func (h *Handler) get_results() { +func (h *Handler) get_results() (ans []ResultItem, in_progress bool) { sc := &h.scan_cache sc.mutex.Lock() defer sc.mutex.Unlock() @@ -124,7 +129,7 @@ func (h *Handler) get_results() { sc.dir_entries = make(dir_cache, 512) } if sc.root_dir == h.state.CurrentDir() && sc.search_text == h.state.SearchText() { - return + return sc.matches, sc.in_progress } sc.in_progress = true sc.matches = nil @@ -142,4 +147,5 @@ func (h *Handler) get_results() { h.lp.WakeupMainThread() } }() + return sc.matches, sc.in_progress } diff --git a/kittens/choose_files/search-bar.go b/kittens/choose_files/search-bar.go index c5cdb19d9..ca25c7c5a 100644 --- a/kittens/choose_files/search-bar.go +++ b/kittens/choose_files/search-bar.go @@ -46,15 +46,15 @@ func (h *Handler) draw_search_text(available_width int) { h.lp.MoveCursorHorizontally(-2) } -func (h *Handler) draw_search_bar(y int) (height int, err error) { +const SEARCH_BAR_HEIGHT = 4 + +func (h *Handler) draw_search_bar(y int) { left_margin, right_margin := 5, 5 - height = 4 h.lp.MoveCursorTo(1+left_margin, 1+y) available_width := h.screen_size.width - left_margin - right_margin - h.draw_frame(available_width, height) + h.draw_frame(available_width, SEARCH_BAR_HEIGHT) h.lp.MoveCursorTo(1+left_margin+1, 2+y) h.draw_search_text(available_width - 2) - return }