mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-16 05:24:20 +02:00
240 lines
7.9 KiB
Go
240 lines
7.9 KiB
Go
package choose_files
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"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 := "", ""
|
|
if in_progress {
|
|
x := h.lp.SprintStyled("fg=cyan", " ")
|
|
prefix, suffix, _ = strings.Cut(x, " ")
|
|
lp.QueueWriteString(prefix)
|
|
}
|
|
for i := range height {
|
|
lp.SaveCursorPosition()
|
|
switch i {
|
|
case 0:
|
|
lp.QueueWriteString("╭")
|
|
lp.QueueWriteString(strings.Repeat("─", width-2))
|
|
lp.QueueWriteString("╮")
|
|
case height - 1:
|
|
lp.QueueWriteString("╰")
|
|
lp.QueueWriteString(strings.Repeat("─", width-2))
|
|
lp.QueueWriteString("╯")
|
|
default:
|
|
lp.QueueWriteString("│")
|
|
lp.MoveCursorHorizontally(width - 2)
|
|
lp.QueueWriteString("│")
|
|
}
|
|
lp.RestoreCursorPosition()
|
|
lp.MoveCursorVertically(1)
|
|
}
|
|
if suffix != "" {
|
|
lp.QueueWriteString(suffix)
|
|
}
|
|
}
|
|
|
|
func (h *Handler) draw_search_text(available_width int) {
|
|
available_width /= 2
|
|
if available_width > 0 {
|
|
available_width-- // reserve a cell for the space appended after the text, which hosts the cursor at end of line
|
|
}
|
|
all_graphemes := wcswidth.SplitIntoGraphemes(h.state.SearchText())
|
|
n := len(all_graphemes)
|
|
widths := make([]int, n)
|
|
total_width := 0
|
|
for i, g := range all_graphemes {
|
|
widths[i] = wcswidth.Stringwidth(g)
|
|
total_width += widths[i]
|
|
}
|
|
width_between := func(s, e int) (w int) {
|
|
for _, x := range widths[s:e] {
|
|
w += x
|
|
}
|
|
return
|
|
}
|
|
cursor_pos := min(n, len(wcswidth.SplitIntoGraphemes(h.search_rl.TextBeforeCursor())))
|
|
|
|
start, end := 0, n
|
|
left_ellipsis, right_ellipsis := false, false
|
|
if total_width > available_width && available_width > 0 {
|
|
// Grow a window around the cursor, one grapheme at a time on
|
|
// alternating sides, without exceeding the available display width.
|
|
start, end = cursor_pos, cursor_pos
|
|
budget := available_width
|
|
for {
|
|
grew := false
|
|
if start > 0 && widths[start-1] <= budget {
|
|
budget -= widths[start-1]
|
|
start--
|
|
grew = true
|
|
}
|
|
if end < n && widths[end] <= budget {
|
|
budget -= widths[end]
|
|
end++
|
|
grew = true
|
|
}
|
|
if !grew {
|
|
break
|
|
}
|
|
}
|
|
left_ellipsis = start > 0
|
|
right_ellipsis = end < n
|
|
ellipsis_width := wcswidth.Stringwidth("…")
|
|
reserved := 0
|
|
if left_ellipsis {
|
|
reserved += ellipsis_width
|
|
}
|
|
if right_ellipsis {
|
|
reserved += ellipsis_width
|
|
}
|
|
// Make room for the ellipsis marks by shrinking the window on the
|
|
// side farther from the cursor first, without ever removing the
|
|
// grapheme the cursor is on.
|
|
for start < end && width_between(start, end)+reserved > available_width {
|
|
if end > cursor_pos && (end-cursor_pos >= cursor_pos-start || start >= cursor_pos) {
|
|
end--
|
|
} else if start < cursor_pos {
|
|
start++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
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, "…")
|
|
}
|
|
visible_width_before_cursor := width_between(start, min(cursor_pos, end))
|
|
if left_ellipsis {
|
|
visible_width_before_cursor += wcswidth.Stringwidth("…")
|
|
}
|
|
visible_width_total := width_between(start, end)
|
|
if left_ellipsis {
|
|
visible_width_total += wcswidth.Stringwidth("…")
|
|
}
|
|
if right_ellipsis {
|
|
visible_width_total += wcswidth.Stringwidth("…")
|
|
}
|
|
h.lp.DrawSizedText(strings.Join(visible, "")+" ", loop.SizedText{Scale: 2})
|
|
h.lp.MoveCursorHorizontally(-2 * (visible_width_total - visible_width_before_cursor + 1))
|
|
}
|
|
|
|
const SEARCH_BAR_HEIGHT = 4
|
|
|
|
func (h *Handler) draw_controls(y int) (max_width int) {
|
|
type entry struct {
|
|
text string
|
|
callback func()
|
|
width int
|
|
}
|
|
lines := make([]entry, 0, SEARCH_BAR_HEIGHT)
|
|
add_control := func(icon, text string, callback func()) {
|
|
line := icon + " " + text
|
|
width := wcswidth.Stringwidth(line)
|
|
max_width = max(max_width, width)
|
|
lines = append(lines, entry{line, callback, width})
|
|
}
|
|
add_control(utils.IfElse(h.state.ShowHidden(), " ", " "), utils.IfElse(h.state.ShowHidden(), "hide dotfiles", "show dotfiles"), func() {
|
|
h.state.show_hidden = !h.state.show_hidden
|
|
h.result_manager.set_show_hidden()
|
|
})
|
|
add_control(" ", utils.IfElse(h.state.RespectIgnores(), "show ignored", "hide ignored"), func() {
|
|
h.state.respect_ignores = !h.state.respect_ignores
|
|
h.result_manager.set_respect_ignores()
|
|
})
|
|
add_control(" ", utils.IfElse(h.state.ShowPreview(), "hide preview", "show preview"), func() {
|
|
h.state.show_preview = !h.state.show_preview
|
|
})
|
|
add_control(utils.IfElse(h.state.SortByLastModified(), " ", " "), utils.IfElse(h.state.SortByLastModified(), "sort names", "sort dates"), func() {
|
|
h.state.sort_by_last_modified = !h.state.sort_by_last_modified
|
|
h.result_manager.set_sort_by_last_modified()
|
|
})
|
|
x := h.screen_size.width - max_width
|
|
for i, e := range lines {
|
|
h.lp.MoveCursorTo(x+1, y+i+1)
|
|
h.lp.QueueWriteString(e.text)
|
|
cb := e.callback
|
|
h.state.mouse_state.AddCellRegion("rcontrol-"+strconv.Itoa(i), x, y+i, x+e.width, y+i, func(_ string) error {
|
|
cb()
|
|
h.state.redraw_needed = true
|
|
return nil
|
|
}).HoverStyle = HOVER_STYLE
|
|
}
|
|
return max_width + 1
|
|
}
|
|
|
|
func (h *Handler) draw_search_bar(y int) {
|
|
left_margin, right_margin := 0, h.draw_controls(y)
|
|
h.lp.MoveCursorTo(1+left_margin, 1+y)
|
|
available_width := h.screen_size.width - left_margin - right_margin
|
|
h.draw_frame(available_width, SEARCH_BAR_HEIGHT, false)
|
|
for y1 := y; y1 < y+4; y1++ {
|
|
cr := h.state.mouse_state.AddCellRegion("search-bar", left_margin, y1, left_margin+available_width, y1)
|
|
cr.PointerShape = loop.TEXT_POINTER
|
|
cr.HoverStyle = "none"
|
|
}
|
|
h.lp.MoveCursorTo(1+left_margin+1, 2+y)
|
|
h.draw_search_text(available_width - 2)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|