mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-06 09:15:57 +02:00
Implement mouse interaction with result list
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"io/fs"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
@@ -211,6 +213,71 @@ func (s *SortedResults) AddSortedSlice(sl []*ResultItem) {
|
||||
s.slices = append(s.slices, sl)
|
||||
}
|
||||
|
||||
func (s *SortedResults) Apply(first, last CollectionIndex, action func(*ResultItem) (keep_going bool)) {
|
||||
s.lock()
|
||||
defer s.unlock()
|
||||
if first.Slice >= len(s.slices) || first.Pos >= len(s.slices[first.Slice]) {
|
||||
return
|
||||
}
|
||||
amt := utils.IfElse(first.Less(last), 1, -1)
|
||||
var did_wrap bool
|
||||
for {
|
||||
if !action(s.slices[first.Slice][first.Pos]) {
|
||||
break
|
||||
}
|
||||
if first.Compare(last) == 0 {
|
||||
break
|
||||
}
|
||||
first, did_wrap = s.increment_with_wrap_around(first, amt)
|
||||
if did_wrap {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SortedResults) Closest(idx CollectionIndex, matches func(*ResultItem) bool) *CollectionIndex {
|
||||
s.lock()
|
||||
defer s.unlock()
|
||||
if idx.Slice >= len(s.slices) || idx.Pos >= len(s.slices[idx.Slice]) {
|
||||
return nil
|
||||
}
|
||||
|
||||
type result struct {
|
||||
idx CollectionIndex
|
||||
count int
|
||||
}
|
||||
var a, b result
|
||||
iterate := func(idx CollectionIndex, amt int, result *result) {
|
||||
var did_wrap bool
|
||||
var count int
|
||||
result.count = -1
|
||||
for {
|
||||
idx, did_wrap = s.increment_with_wrap_around(idx, amt)
|
||||
if did_wrap {
|
||||
break
|
||||
}
|
||||
count++
|
||||
if matches(s.slices[idx.Slice][idx.Pos]) {
|
||||
result.idx = idx
|
||||
result.count = count
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
go func() { iterate(idx, 1, &a) }()
|
||||
go func() { iterate(idx, -1, &a) }()
|
||||
if a.count < 0 && b.count < 0 {
|
||||
return nil
|
||||
}
|
||||
return utils.IfElse(a.count < b.count, &b.idx, &a.idx)
|
||||
}
|
||||
|
||||
func (s *SortedResults) IncrementIndexWithWrapAroundAndCheck(idx CollectionIndex, amt int) (ans CollectionIndex, did_wrap bool) {
|
||||
s.lock()
|
||||
defer s.unlock()
|
||||
return s.increment_with_wrap_around(idx, amt)
|
||||
}
|
||||
|
||||
func (s *SortedResults) IncrementIndexWithWrapAround(idx CollectionIndex, amt int) CollectionIndex {
|
||||
s.lock()
|
||||
defer s.unlock()
|
||||
@@ -259,6 +326,27 @@ func (s *SortedResults) increment_with_wrap_around(idx CollectionIndex, amt int)
|
||||
return idx, did_wrap
|
||||
}
|
||||
|
||||
// Return a - b
|
||||
func (s *SortedResults) SignedDistance(a, b CollectionIndex) (ans int) {
|
||||
s.lock()
|
||||
defer s.unlock()
|
||||
return s.signed_distance(a, b)
|
||||
}
|
||||
|
||||
// Return a - b
|
||||
func (s *SortedResults) signed_distance(a, b CollectionIndex) (ans int) {
|
||||
mult := -1
|
||||
if b.Less(a) {
|
||||
a, b = b, a
|
||||
mult = 1
|
||||
}
|
||||
limit := min(b.Slice, len(s.slices))
|
||||
for ; a.Slice < limit; a.NextSlice() {
|
||||
ans += len(s.slices[a.Slice]) - a.Pos
|
||||
}
|
||||
return mult * (ans + (b.Pos - a.Pos))
|
||||
}
|
||||
|
||||
// Return |a - b|
|
||||
func (s *SortedResults) distance(a, b CollectionIndex) (ans int) {
|
||||
if b.Less(a) {
|
||||
@@ -271,13 +359,13 @@ func (s *SortedResults) distance(a, b CollectionIndex) (ans int) {
|
||||
return ans + (b.Pos - a.Pos)
|
||||
}
|
||||
|
||||
func (s *SortedResults) SplitIntoColumns(calc_num_cols func(int) int, num_per_column, num_before_current int, current CollectionIndex) (ans [][]*ResultItem, num_before int) {
|
||||
func (s *SortedResults) SplitIntoColumns(calc_num_cols func(int) int, num_per_column, num_before_current int, current CollectionIndex) (ans [][]*ResultItem, num_before int, first_idx CollectionIndex) {
|
||||
s.lock()
|
||||
defer s.unlock()
|
||||
num_cols := calc_num_cols(s.len)
|
||||
total := num_cols * num_per_column
|
||||
if total < 1 {
|
||||
return nil, 0
|
||||
return nil, 0, CollectionIndex{}
|
||||
}
|
||||
num_before = min(total-1, num_before_current)
|
||||
idx, did_wrap := s.increment_with_wrap_around(current, -num_before)
|
||||
@@ -290,6 +378,7 @@ func (s *SortedResults) SplitIntoColumns(calc_num_cols func(int) int, num_per_co
|
||||
idx = CollectionIndex{}
|
||||
}
|
||||
}
|
||||
first_idx = idx
|
||||
num_before = s.distance(idx, current)
|
||||
// fmt.Printf("111111 idx: %v current: %v num_before: %d\n", idx, current, num_before)
|
||||
ans = make([][]*ResultItem, num_cols)
|
||||
|
||||
@@ -99,6 +99,7 @@ func (m Mode) WindowTitle() string {
|
||||
|
||||
type render_state struct {
|
||||
num_matches, num_of_slots, num_before, num_per_column, num_columns int
|
||||
first_idx CollectionIndex
|
||||
}
|
||||
|
||||
type State struct {
|
||||
@@ -175,12 +176,14 @@ func (s *State) AddSelection(abspath string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *State) ToggleSelection(abspath string) {
|
||||
func (s *State) ToggleSelection(abspath string) (added bool) {
|
||||
before := len(s.selections)
|
||||
s.selections = slices.DeleteFunc(s.selections, func(x string) bool { return x == abspath })
|
||||
if len(s.selections) == before {
|
||||
s.selections = append(s.selections, abspath)
|
||||
added = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *State) IsSelected(x *ResultItem) bool {
|
||||
@@ -279,15 +282,33 @@ func (h *Handler) current_abspath() string {
|
||||
|
||||
}
|
||||
|
||||
func (h *Handler) toggle_selection() bool {
|
||||
m := h.current_abspath()
|
||||
if m != "" {
|
||||
h.state.ToggleSelection(m)
|
||||
func (s *State) CanSelect(r *ResultItem) bool {
|
||||
return utils.IfElse(s.OnlyDirs(), r.ftype.IsDir(), !r.ftype.IsDir())
|
||||
}
|
||||
|
||||
func (h *Handler) toggle_selection_at(idx CollectionIndex) bool {
|
||||
matches, _ := h.get_results()
|
||||
if r := matches.At(idx); r != nil && h.state.CanSelect(r) {
|
||||
m := filepath.Join(h.state.CurrentDir(), r.text)
|
||||
if added := h.state.ToggleSelection(m); added {
|
||||
h.result_manager.last_click_anchor = &idx
|
||||
} else {
|
||||
h.result_manager.last_click_anchor = nil
|
||||
if len(h.state.selections) > 0 {
|
||||
x := utils.NewSetWithItems(h.state.selections...)
|
||||
cdir := h.state.CurrentDir()
|
||||
h.result_manager.last_click_anchor = matches.Closest(idx, func(q *ResultItem) bool { return x.Has(filepath.Join(cdir, q.text)) })
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Handler) toggle_selection() bool {
|
||||
return h.toggle_selection_at(h.state.CurrentIndex())
|
||||
}
|
||||
|
||||
func (h *Handler) change_current_dir(dir string) {
|
||||
if dir != h.state.CurrentDir() {
|
||||
h.state.SetCurrentDir(dir)
|
||||
@@ -346,6 +367,31 @@ func (h *Handler) change_filter(delta int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *Handler) accept_idx(idx CollectionIndex) (accepted bool, err error) {
|
||||
matches, _ := h.get_results()
|
||||
if r := matches.At(idx); r != nil {
|
||||
m := filepath.Join(h.state.CurrentDir(), r.text)
|
||||
|
||||
if h.state.mode.SelectFiles() {
|
||||
var s os.FileInfo
|
||||
if s, err = os.Stat(m); err != nil {
|
||||
return false, nil
|
||||
}
|
||||
if s.IsDir() {
|
||||
return true, h.change_to_current_dir_if_possible()
|
||||
}
|
||||
}
|
||||
|
||||
h.state.AddSelection(m)
|
||||
h.result_manager.last_click_anchor = &idx
|
||||
if len(h.state.selections) > 0 {
|
||||
return true, h.finish_selection()
|
||||
}
|
||||
return true, h.draw_screen()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) dispatch_action(name, args string) (err error) {
|
||||
switch name {
|
||||
case "quit":
|
||||
@@ -380,26 +426,13 @@ func (h *Handler) dispatch_action(name, args string) (err error) {
|
||||
return h.draw_screen()
|
||||
}
|
||||
case "accept":
|
||||
m := h.current_abspath()
|
||||
if m == "" {
|
||||
accepted, aerr := h.accept_idx(h.state.CurrentIndex())
|
||||
if aerr != nil {
|
||||
return aerr
|
||||
}
|
||||
if !accepted {
|
||||
h.lp.Beep()
|
||||
return
|
||||
}
|
||||
if h.state.mode.SelectFiles() {
|
||||
var s os.FileInfo
|
||||
if s, err = os.Stat(m); err != nil {
|
||||
h.lp.Beep()
|
||||
return nil
|
||||
}
|
||||
if s.IsDir() {
|
||||
return h.change_to_current_dir_if_possible()
|
||||
}
|
||||
}
|
||||
h.state.AddSelection(m)
|
||||
if len(h.state.selections) > 0 {
|
||||
return h.finish_selection()
|
||||
}
|
||||
return h.draw_screen()
|
||||
case "toggle":
|
||||
switch args {
|
||||
case "dotfiles":
|
||||
|
||||
@@ -47,7 +47,11 @@ map('Quit', 'quit esc quit')
|
||||
map('Quit', 'quit ctrl+c quit')
|
||||
|
||||
map('Accept current result', 'accept enter accept')
|
||||
map('Select current result', 'select shift+enter select')
|
||||
map('Select current result', 'select shift+enter select', long_text='''
|
||||
When selecting multiple files, this will add the current file to the list of selected files.
|
||||
You can also toggle the selected status of a file by holding down the :kbd:`Ctrl` key and clicking on
|
||||
it. Similarly, the :kbd:`Alt` key can be held to click and extend the range of selected files.
|
||||
''')
|
||||
|
||||
map('Next result', 'next_result down next 1')
|
||||
map('Previous result', 'prev_result up next -1')
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/icons"
|
||||
"github.com/kovidgoyal/kitty/tools/tui"
|
||||
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/style"
|
||||
@@ -123,7 +125,7 @@ func icon_for(path string, x os.FileMode) string {
|
||||
return ans
|
||||
}
|
||||
|
||||
func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x, available_width int) {
|
||||
func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x, y, available_width, colnum int) {
|
||||
root_dir := h.state.CurrentDir()
|
||||
for i, m := range matches {
|
||||
h.lp.QueueWriteString("\r")
|
||||
@@ -137,9 +139,11 @@ func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x
|
||||
}
|
||||
text := m.text
|
||||
add_ellipsis := false
|
||||
if wcswidth.Stringwidth(text) > available_width-3 {
|
||||
width := wcswidth.Stringwidth(text)
|
||||
if width > available_width-3 {
|
||||
text = wcswidth.TruncateToVisualLength(text, available_width-4)
|
||||
add_ellipsis = true
|
||||
width = available_width - 3
|
||||
}
|
||||
is_current := i == current_idx
|
||||
if is_current {
|
||||
@@ -153,6 +157,54 @@ func (h *Handler) draw_column_of_matches(matches ResultsType, current_idx int, x
|
||||
}
|
||||
h.render_match_with_positions(text, add_ellipsis, m.sorted_positions(), is_current)
|
||||
h.lp.MoveCursorVertically(1)
|
||||
cr := h.state.mouse_state.AddCellRegion(fmt.Sprintf("result-%d-%d", colnum, i), x, y-1+i, x+width+2, y-1+i)
|
||||
cr.HoverStyle = HOVER_STYLE
|
||||
var data struct {
|
||||
colnum, i int
|
||||
}
|
||||
data.colnum, data.i = colnum, i
|
||||
cr.OnClickEvent = func(id string, ev *loop.MouseEvent, cell_offset tui.Point) error {
|
||||
if ev.Buttons&loop.LEFT_MOUSE_BUTTON == 0 {
|
||||
return nil
|
||||
}
|
||||
ctrl_mod := utils.IfElse(runtime.GOOS == "darwin", loop.SUPER, loop.CTRL)
|
||||
mods := ev.Mods & (ctrl_mod | loop.ALT) // shift alone and ctrl+shift are used for kitty bindings
|
||||
matches, _ := h.get_results()
|
||||
num_before := h.state.last_render.num_of_slots*data.colnum + data.i
|
||||
idx, did_wrap := matches.IncrementIndexWithWrapAroundAndCheck(h.state.last_render.first_idx, num_before)
|
||||
if did_wrap {
|
||||
h.lp.Beep()
|
||||
return nil
|
||||
}
|
||||
d := matches.SignedDistance(idx, h.state.current_idx)
|
||||
h.state.SetCurrentIndex(idx)
|
||||
h.state.last_render.num_before = max(0, h.state.last_render.num_before+d)
|
||||
switch mods {
|
||||
case 0:
|
||||
h.dispatch_action("accept", "")
|
||||
case ctrl_mod, ctrl_mod | loop.ALT:
|
||||
h.dispatch_action("select", "")
|
||||
case loop.ALT:
|
||||
r := matches.At(idx)
|
||||
if (r != nil && h.state.IsSelected(r)) || h.result_manager.last_click_anchor == nil {
|
||||
h.dispatch_action("select", "")
|
||||
return nil
|
||||
}
|
||||
already_selected := utils.NewSetWithItems(h.state.selections...)
|
||||
cdir := h.state.CurrentDir()
|
||||
matches.Apply(idx, *h.result_manager.last_click_anchor, func(r *ResultItem) bool {
|
||||
m := filepath.Join(cdir, r.text)
|
||||
if !already_selected.Has(m) && h.state.CanSelect(r) {
|
||||
already_selected.Add(m)
|
||||
h.state.selections = append(h.state.selections, m)
|
||||
}
|
||||
return true
|
||||
})
|
||||
return h.draw_screen()
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,14 +226,15 @@ func (h *Handler) draw_list_of_results(matches *SortedResults, y, height int) in
|
||||
}
|
||||
return num_cols
|
||||
}
|
||||
columns, num_before := matches.SplitIntoColumns(calc_num_cols, height, h.state.last_render.num_before, h.state.CurrentIndex())
|
||||
columns, num_before, first_idx := matches.SplitIntoColumns(calc_num_cols, height, h.state.last_render.num_before, h.state.CurrentIndex())
|
||||
h.state.last_render.num_before = num_before
|
||||
h.state.last_render.num_per_column = height
|
||||
h.state.last_render.num_columns = num_cols
|
||||
h.state.last_render.first_idx = first_idx
|
||||
x := 1
|
||||
for _, col := range columns {
|
||||
for i, col := range columns {
|
||||
h.lp.MoveCursorTo(x, y)
|
||||
h.draw_column_of_matches(col, num_before, x, col_width-1)
|
||||
h.draw_column_of_matches(col, num_before, x, y, col_width-1, i)
|
||||
num_before -= height
|
||||
x += col_width
|
||||
}
|
||||
|
||||
@@ -640,6 +640,8 @@ type ResultManager struct {
|
||||
scorer *FileSystemScorer
|
||||
mutex sync.Mutex
|
||||
last_wakeup_at time.Time
|
||||
|
||||
last_click_anchor *CollectionIndex
|
||||
}
|
||||
|
||||
func NewResultManager(err_chan chan error, settings Settings, WakeupMainThread func() bool) *ResultManager {
|
||||
@@ -658,6 +660,7 @@ func (m *ResultManager) new_scorer() {
|
||||
m.scorer.respect_ignores = m.settings.RespectIgnores()
|
||||
m.scorer.show_hidden = m.settings.ShowHidden()
|
||||
m.scorer.global_ignore = m.settings.GlobalIgnores()
|
||||
m.last_click_anchor = nil
|
||||
}
|
||||
|
||||
func (m *ResultManager) on_results(err error, is_finished bool) {
|
||||
@@ -709,6 +712,7 @@ func (m *ResultManager) set_something(callback func()) {
|
||||
m.new_scorer()
|
||||
m.scorer.Start()
|
||||
} else {
|
||||
m.last_click_anchor = nil
|
||||
callback()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user