Use readline for the choose files search input

This commit is contained in:
Kovid Goyal
2026-07-05 15:16:11 +05:30
parent 31eb21f9f5
commit 011ef4d319
5 changed files with 184 additions and 25 deletions

View File

@@ -216,6 +216,7 @@ type Handler struct {
result_manager *ResultManager
lp *loop.Loop
rl *readline.Readline
search_rl *readline.Readline
err_chan chan error
shortcut_tracker config.ShortcutTracker
msg_printer *message.Printer
@@ -355,6 +356,7 @@ func (h *Handler) init_sizes(new_size loop.ScreenSize) {
h.screen_size.width_px = int(new_size.WidthPx)
h.screen_size.height_px = int(new_size.HeightPx)
h.rl.ClearCachedScreenSize()
h.search_rl.ClearCachedScreenSize()
}
func (h *Handler) OnInitialize() (ans string, err error) {
@@ -373,6 +375,7 @@ func (h *Handler) OnInitialize() (ans string, err error) {
if (s.IsDir() && h.state.mode != SELECT_SAVE_FILE) || (!s.IsDir() && h.state.mode == SELECT_SAVE_FILE) {
h.state.SetCurrentDir(filepath.Dir(h.state.suggested_save_file_path))
h.state.SetSearchText(filepath.Base(h.state.suggested_save_file_name))
h.search_rl.SetText(h.state.SearchText())
}
}
}
@@ -426,6 +429,7 @@ func (h *Handler) toggle_selection() bool {
func (h *Handler) change_current_dir(dir string) {
if dir != h.state.CurrentDir() {
h.state.SetCurrentDir(dir)
h.search_rl.ResetText()
h.result_manager.set_root_dir()
h.state.last_render = render_state{}
}
@@ -664,15 +668,20 @@ func (h *Handler) dispatch_action(name, args string) (err error) {
func (h *Handler) OnKeyEvent(ev *loop.KeyEvent) (err error) {
switch h.state.screen {
case NORMAL:
if h.handle_edit_keys(ev) {
ev.Handled = true
h.draw_screen()
}
ac := h.shortcut_tracker.Match(ev, h.state.keyboard_shortcuts)
if ac != nil {
ev.Handled = true
if rlac, is_edit_action := edit_actions[ac.Name]; is_edit_action {
if err = h.perform_edit_action(rlac); err == nil {
err = h.draw_screen()
}
return
}
return h.dispatch_action(ac.Name, ac.Args)
}
if err = h.forward_key_event_to_search_rl(ev); err == nil {
err = h.draw_screen()
}
case SAVE_FILE:
err = h.save_file_name_handle_key(ev)
}
@@ -693,8 +702,10 @@ func (h *Handler) OnMouseEvent(event *loop.MouseEvent) (err error) {
func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (err error) {
switch h.state.screen {
case NORMAL:
h.set_query(h.state.SearchText() + text)
return h.draw_screen()
if err = h.search_rl.OnText(text, from_key_event, in_bracketed_paste); err == nil {
h.set_query(h.search_rl.AllText())
err = h.draw_screen()
}
case SAVE_FILE:
if err = h.rl.OnText(text, from_key_event, in_bracketed_paste); err == nil {
err = h.draw_screen()
@@ -951,6 +962,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
handler.rl = readline.New(lp, readline.RlInit{
Prompt: "> ", ContinuationPrompt: ". ", Completer: FilePromptCompleter(getcwd),
})
handler.search_rl = readline.New(lp, readline.RlInit{DontMarkPrompts: true})
if err = handler.set_state_from_config(conf, opts); err != nil {
return 1, err
}

View File

@@ -6,12 +6,37 @@ import (
"strings"
"github.com/kovidgoyal/kitty/tools/tui/loop"
"github.com/kovidgoyal/kitty/tools/tui/readline"
"github.com/kovidgoyal/kitty/tools/utils"
"github.com/kovidgoyal/kitty/tools/wcswidth"
)
var _ = fmt.Print
// Actions that, when matched by a keyboard shortcut, are forwarded to the
// readline instance powering the search/filter text box instead of being
// dispatched as a normal choose_files action. This lets users who prefer
// editing text with the arrow keys, Home, End, etc. rebind those keys away
// from their default job of navigating the list of matches, towards editing
// the search text instead. See the kitten's documentation for details.
var edit_actions = map[string]readline.Action{
"edit_cursor_left": readline.ActionCursorLeft,
"edit_cursor_right": readline.ActionCursorRight,
"edit_start_of_line": readline.ActionMoveToStartOfLine,
"edit_end_of_line": readline.ActionMoveToEndOfLine,
"edit_start_of_document": readline.ActionMoveToStartOfDocument,
"edit_end_of_document": readline.ActionMoveToEndOfDocument,
"edit_forward_word": readline.ActionMoveToEndOfWord,
"edit_backward_word": readline.ActionMoveToStartOfWord,
"edit_backspace": readline.ActionBackspace,
"edit_delete": readline.ActionDelete,
"edit_kill_to_start_of_line": readline.ActionKillToStartOfLine,
"edit_kill_to_end_of_line": readline.ActionKillToEndOfLine,
"edit_kill_word_left": readline.ActionKillPreviousWord,
"edit_kill_word_right": readline.ActionKillNextWord,
"edit_yank": readline.ActionYank,
}
func (h *Handler) draw_frame(width, height int, in_progress bool) {
lp := h.lp
prefix, suffix := "", ""
@@ -45,16 +70,41 @@ func (h *Handler) draw_frame(width, height int, in_progress bool) {
}
func (h *Handler) draw_search_text(available_width int) {
text := h.state.SearchText()
available_width /= 2
if wcswidth.Stringwidth(text) > available_width {
g := wcswidth.SplitIntoGraphemes(text)
available_width -= 2
g = g[len(g)-available_width:]
text = "…" + strings.Join(g, "")
all_graphemes := wcswidth.SplitIntoGraphemes(h.state.SearchText())
cursor_pos := len(wcswidth.SplitIntoGraphemes(h.search_rl.TextBeforeCursor()))
start, end := 0, len(all_graphemes)
left_ellipsis, right_ellipsis := false, false
if len(all_graphemes) > available_width && available_width > 0 {
start = cursor_pos - available_width/2
start = max(0, start)
end = min(len(all_graphemes), start+available_width)
start = max(0, end-available_width)
left_ellipsis = start > 0
right_ellipsis = end < len(all_graphemes)
if left_ellipsis {
start++
}
if right_ellipsis {
end--
}
end = max(start, end)
}
h.lp.DrawSizedText(text+" ", loop.SizedText{Scale: 2})
h.lp.MoveCursorHorizontally(-2)
visible := make([]string, 0, end-start+2)
if left_ellipsis {
visible = append(visible, "…")
}
visible = append(visible, all_graphemes[start:end]...)
if right_ellipsis {
visible = append(visible, "…")
}
cursor_col := cursor_pos - start
if left_ellipsis {
cursor_col++
}
cursor_col = max(0, min(cursor_col, len(visible)))
h.lp.DrawSizedText(strings.Join(visible, "")+" ", loop.SizedText{Scale: 2})
h.lp.MoveCursorHorizontally(-2 * (len(visible) - cursor_col + 1))
}
const SEARCH_BAR_HEIGHT = 4
@@ -115,16 +165,22 @@ func (h *Handler) draw_search_bar(y int) {
h.draw_search_text(available_width - 2)
}
func (h *Handler) handle_edit_keys(ev *loop.KeyEvent) bool {
switch {
case ev.MatchesPressOrRepeat("backspace"):
if h.state.SearchText() == "" {
h.lp.Beep()
} else {
g := wcswidth.SplitIntoGraphemes(h.state.search_text)
h.set_query(strings.Join(g[:len(g)-1], ""))
return true
}
// perform_edit_action runs a readline editing action against the search text
// box and syncs the resulting text back into the query used to filter results.
func (h *Handler) perform_edit_action(ac readline.Action) (err error) {
if err = h.search_rl.PerformAction(ac, 1); err == nil {
h.set_query(h.search_rl.AllText())
}
return false
return
}
// forward_key_event_to_search_rl is used for keys that are not claimed by any
// configured keyboard shortcut, so that ordinary readline editing (backspace,
// delete, ctrl+k, ctrl+w, etc.) works in the search box without needing
// explicit configuration.
func (h *Handler) forward_key_event_to_search_rl(ev *loop.KeyEvent) (err error) {
if err = h.search_rl.OnKeyEvent(ev); err == nil {
h.set_query(h.search_rl.AllText())
}
return
}