From 834d0d384880cc12eafb2ce3416fbe33792f3e59 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 5 Jul 2025 11:37:17 +0530 Subject: [PATCH] Make the filter labels clickable --- kittens/choose_files/footer.go | 29 ++++++++++++++++++++++++---- kittens/choose_files/main.go | 22 +++++++++++++++++++++ tools/tui/mouse.go | 35 ++++++++++++++++++++++++++-------- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/kittens/choose_files/footer.go b/kittens/choose_files/footer.go index 12f6e3e15..6d7cf79f4 100644 --- a/kittens/choose_files/footer.go +++ b/kittens/choose_files/footer.go @@ -11,6 +11,12 @@ import ( var _ = fmt.Print +type single_line_region struct { + x, width, y int + id string + callback func(string) error +} + func (h *Handler) draw_footer() (num_lines int, err error) { lines := []string{} screen_width := h.screen_size.width @@ -20,26 +26,41 @@ func (h *Handler) draw_footer() (num_lines int, err error) { pos := 0 current_style := sctx.SprintFunc("italic fg=green intense") non_current_style := sctx.SprintFunc("dim") - w := func(text string, sfunc func(...any) string) { - sz := wcswidth.Stringwidth(text) + var crs []single_line_region + w := func(presep, text string, sfunc func(...any) string, click_name string) { + sz := len(presep) if sz+pos >= screen_width { lines = append(lines, buf.String()) pos = 0 buf.Reset() } + buf.WriteString(presep) + pos += sz + sz = wcswidth.Stringwidth(text) if sfunc != nil { text = sfunc(text) } buf.WriteString(text) + if click_name != "" { + crs = append(crs, single_line_region{x: pos, width: sz, y: len(lines), id: click_name, callback: func(filter string) error { + h.set_filter(filter) + h.state.redraw_needed = true + return nil + }}) + } pos += sz } - w("󰈲 Filter:", nil) + w("", "󰈲 Filter:", nil, "") for _, name := range h.state.filter_names { - w(" "+name, utils.IfElse(name == h.state.current_filter, current_style, non_current_style)) + w(" ", name, utils.IfElse(name == h.state.current_filter, current_style, non_current_style), name) } if s := buf.String(); s != "" { lines = append(lines, s) } + offset := h.screen_size.height - len(lines) + for _, cr := range crs { + h.state.mouse_state.AddCellRegion(cr.id, cr.x, cr.y+offset, cr.x+cr.width, cr.y+offset, cr.callback) + } } if len(lines) > 0 { h.lp.MoveCursorTo(1, h.screen_size.height-len(lines)+1) diff --git a/kittens/choose_files/main.go b/kittens/choose_files/main.go index c45327efe..3331a3b07 100644 --- a/kittens/choose_files/main.go +++ b/kittens/choose_files/main.go @@ -13,6 +13,7 @@ import ( "github.com/kovidgoyal/kitty/tools/cli" "github.com/kovidgoyal/kitty/tools/config" "github.com/kovidgoyal/kitty/tools/tty" + "github.com/kovidgoyal/kitty/tools/tui" "github.com/kovidgoyal/kitty/tools/tui/loop" "github.com/kovidgoyal/kitty/tools/tui/readline" "github.com/kovidgoyal/kitty/tools/utils" @@ -116,6 +117,8 @@ type State struct { selections []string current_idx CollectionIndex last_render render_state + mouse_state tui.MouseState + redraw_needed bool } func (s State) BaseDir() string { return utils.IfElse(s.base_dir == "", default_cwd, s.base_dir) } @@ -182,9 +185,11 @@ type Handler struct { } func (h *Handler) draw_screen() (err error) { + h.state.redraw_needed = false h.lp.StartAtomicUpdate() defer h.lp.EndAtomicUpdate() h.lp.ClearScreen() + h.state.mouse_state.ClearCellRegions() switch h.state.screen { case NORMAL: matches, is_complete := h.get_results() @@ -202,6 +207,8 @@ func (h *Handler) draw_screen() (err error) { case SAVE_FILE: err = h.draw_save_file_name_screen() } + h.state.mouse_state.UpdateHoveredIds() + h.state.mouse_state.ApplyHoverStyles(h.lp) return } @@ -402,6 +409,19 @@ func (h *Handler) OnKeyEvent(ev *loop.KeyEvent) (err error) { return } +func (h *Handler) OnMouseEvent(event *loop.MouseEvent) (err error) { + h.state.redraw_needed = h.state.mouse_state.UpdateState(event) + if event.Event_type == loop.MOUSE_CLICK && event.Buttons&loop.LEFT_MOUSE_BUTTON != 0 { + if err = h.state.mouse_state.ClickHoveredRegions(); err != nil { + return + } + } + if h.state.redraw_needed { + err = h.draw_screen() + } + return +} + func (h *Handler) OnText(text string, from_key_event, in_bracketed_paste bool) (err error) { switch h.state.screen { case NORMAL: @@ -533,6 +553,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) { if err != nil { return 1, err } + lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING) handler := Handler{lp: lp, err_chan: make(chan error, 8), rl: readline.New(lp, readline.RlInit{ Prompt: "> ", ContinuationPrompt: ". ", })} @@ -571,6 +592,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) { } lp.OnKeyEvent = handler.OnKeyEvent lp.OnText = handler.OnText + lp.OnMouseEvent = handler.OnMouseEvent lp.OnWakeup = func() (err error) { select { case err = <-handler.err_chan: diff --git a/tools/tui/mouse.go b/tools/tui/mouse.go index a2f0c7aaf..6b354b547 100644 --- a/tools/tui/mouse.go +++ b/tools/tui/mouse.go @@ -205,10 +205,15 @@ func (ms *MouseSelection) DragScroll(ev *loop.MouseEvent, lp *loop.Loop, callbac ms.drag_scroll.mouse_event = *ev } +type Point struct { + X, Y int +} + type CellRegion struct { - TopLeft, BottomRight struct{ X, Y int } + TopLeft, BottomRight Point Id string OnClick []func(id string) error + PointerShape loop.PointerShape } func (c CellRegion) Contains(x, y int) bool { // 0-based @@ -224,6 +229,7 @@ type MouseState struct { regions []*CellRegion region_id_map map[string][]*CellRegion + region_line_map map[int][]*CellRegion hovered_ids *utils.Set[string] default_url_style struct { value string @@ -231,25 +237,35 @@ type MouseState struct { } } -func (m *MouseState) AddCellRegion(id string, start_x, start_y, end_x, end_y int, on_click ...func(id string) error) *CellRegion { - cr := CellRegion{TopLeft: struct{ X, Y int }{start_x, start_y}, BottomRight: struct{ X, Y int }{end_x, end_y}, Id: id, OnClick: on_click} +func (m *MouseState) AddRegion(cr CellRegion) *CellRegion { m.regions = append(m.regions, &cr) if m.region_id_map == nil { m.region_id_map = make(map[string][]*CellRegion) + m.region_line_map = make(map[int][]*CellRegion) + } + m.region_id_map[cr.Id] = append(m.region_id_map[cr.Id], &cr) + for y := cr.TopLeft.Y; y <= cr.BottomRight.Y; y++ { + m.region_line_map[y] = append(m.region_line_map[y], &cr) } - m.region_id_map[id] = append(m.region_id_map[id], &cr) return &cr } +func (m *MouseState) AddCellRegion(id string, start_x, start_y, end_x, end_y int, on_click ...func(id string) error) *CellRegion { + cr := CellRegion{ + TopLeft: Point{start_x, start_y}, BottomRight: Point{end_x, end_y}, Id: id, OnClick: on_click, PointerShape: loop.POINTER_POINTER} + return m.AddRegion(cr) +} + func (m *MouseState) ClearCellRegions() { m.regions = nil m.region_id_map = nil m.hovered_ids = nil + m.region_line_map = nil } func (m *MouseState) UpdateHoveredIds() (changed bool) { h := utils.NewSet[string]() - for _, r := range m.regions { + for _, r := range m.region_line_map[m.Cell.Y] { if r.Contains(m.Cell.X, m.Cell.Y) { h.Add(r.Id) } @@ -260,7 +276,8 @@ func (m *MouseState) UpdateHoveredIds() (changed bool) { } func (m *MouseState) ApplyHoverStyles(lp *loop.Loop, style ...string) { - if m.hovered_ids == nil { + if m.hovered_ids == nil || m.hovered_ids.Len() == 0 { + lp.ClearPointerShapes() return } hs := "" @@ -287,15 +304,17 @@ func (m *MouseState) ApplyHoverStyles(lp *loop.Loop, style ...string) { hs = style[0] } is_hovered := false + ps := loop.DEFAULT_POINTER for id := range m.hovered_ids.Iterable() { for _, r := range m.region_id_map[id] { lp.StyleRegion(hs, r.TopLeft.X, r.TopLeft.Y, r.BottomRight.X, r.BottomRight.Y) is_hovered = true + ps = r.PointerShape } } if is_hovered { - if s, has := lp.CurrentPointerShape(); !has || s != loop.POINTER_POINTER { - lp.PushPointerShape(loop.POINTER_POINTER) + if s, has := lp.CurrentPointerShape(); !has || s != ps { + lp.PushPointerShape(ps) } } else { lp.ClearPointerShapes()