From 37cebbc817f587697f876c941427fc6c4a7673ed Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 7 Mar 2023 07:20:46 +0530 Subject: [PATCH] Implement decoding of mouse events in Go --- tools/tui/loop/api.go | 3 ++ tools/tui/loop/mouse.go | 109 ++++++++++++++++++++++++++++++++++++++++ tools/tui/loop/run.go | 14 ++++++ 3 files changed, 126 insertions(+) create mode 100644 tools/tui/loop/mouse.go diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index ce83f83ea..f80a53d7e 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -74,6 +74,9 @@ type Loop struct { // Called when a key event happens OnKeyEvent func(event *KeyEvent) error + // Called when a mouse event happens + OnMouseEvent func(event *MouseEvent) error + // Called when text is received either from a key event or directly from the terminal // Called with an empty string when bracketed paste ends OnText func(text string, from_key_event bool, in_bracketed_paste bool) error diff --git a/tools/tui/loop/mouse.go b/tools/tui/loop/mouse.go new file mode 100644 index 000000000..b927cb3d3 --- /dev/null +++ b/tools/tui/loop/mouse.go @@ -0,0 +1,109 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package loop + +import ( + "fmt" + "strconv" + "strings" + + "kitty/tools/utils" +) + +var _ = fmt.Print + +type MouseEventType int +type MouseButtonFlag int + +const ( + MOUSE_PRESS MouseEventType = iota + MOUSE_RELEASE + MOUSE_MOVE +) +const ( + SHIFT_INDICATOR int = 1 << 2 + ALT_INDICATOR = 1 << 3 + CTRL_INDICATOR = 1 << 4 + MOTION_INDICATOR = 1 << 5 +) + +const NONE, LEFT, MIDDLE, RIGHT, FOURTH, FIFTH, SIXTH, SEVENTH MouseButtonFlag = 0, 1, 2, 4, 8, 16, 32, 64 +const WHEEL_UP, WHEEL_DOWN, WHEEL_LEFT, WHEEL_RIGHT MouseButtonFlag = -1, -2, -4, -8 + +var bmap = [...]MouseButtonFlag{LEFT, MIDDLE, RIGHT} +var ebmap = [...]MouseButtonFlag{FOURTH, FIFTH, SIXTH, SEVENTH} +var wbmap = [...]MouseButtonFlag{WHEEL_UP, WHEEL_DOWN, WHEEL_LEFT, WHEEL_RIGHT} + +type MouseEvent struct { + Event_type MouseEventType + Buttons MouseButtonFlag + Mods KeyModifiers + Cell, Pixel struct{ x, y int } +} + +func pixel_to_cell(px, length, cell_length int) int { + px = utils.Max(0, utils.Min(px, length-1)) + return px / cell_length +} + +func decode_sgr_mouse(text string, screen_size ScreenSize) *MouseEvent { + parts := strings.Split(text, ";") + if len(parts) != 3 { + return nil + } + cb, err := strconv.Atoi(parts[0]) + if err != nil { + return nil + } + ans := MouseEvent{} + ans.Pixel.x, err = strconv.Atoi(parts[1]) + if err != nil { + return nil + } + if len(parts[2]) < 1 { + return nil + } + ans.Pixel.y, err = strconv.Atoi(parts[2][:len(parts[2])-1]) + m := parts[2][len(parts[2])-1] + if m == 'm' { + ans.Event_type = MOUSE_RELEASE + } else if cb&MOTION_INDICATOR != 0 { + ans.Event_type = MOUSE_MOVE + } + cb3 := cb & 3 + if cb >= 128 { + ans.Buttons |= ebmap[cb3] + } else if cb >= 64 { + ans.Buttons |= wbmap[cb3] + } else if cb3 < 3 { + ans.Buttons |= bmap[cb3] + } + if cb&SHIFT_INDICATOR != 0 { + ans.Mods |= SHIFT + } + if cb&ALT_INDICATOR != 0 { + ans.Mods |= ALT + } + if cb&CTRL_INDICATOR != 0 { + ans.Mods |= CTRL + } + ans.Cell.x = pixel_to_cell(ans.Pixel.x, int(screen_size.WidthPx), int(screen_size.CellWidth)) + ans.Cell.y = pixel_to_cell(ans.Pixel.y, int(screen_size.HeightPx), int(screen_size.CellHeight)) + + return &ans +} + +func MouseEventFromCSI(csi string, screen_size ScreenSize) *MouseEvent { + if len(csi) == 0 { + return nil + } + last_char := csi[len(csi)-1] + if last_char != 'm' && last_char != 'M' { + return nil + } + csi = csi[:len(csi)-1] + if !strings.HasPrefix(csi, "<") { + return nil + } + return decode_sgr_mouse(csi[1:], screen_size) +} diff --git a/tools/tui/loop/run.go b/tools/tui/loop/run.go index 4832c4f07..87624d353 100644 --- a/tools/tui/loop/run.go +++ b/tools/tui/loop/run.go @@ -73,12 +73,26 @@ func (self *Loop) handle_csi(raw []byte) error { if ke != nil { return self.handle_key_event(ke) } + sz, err := self.ScreenSize() + if err == nil { + me := MouseEventFromCSI(csi, sz) + if me != nil { + return self.handle_mouse_event(me) + } + } if self.OnEscapeCode != nil { return self.OnEscapeCode(CSI, raw) } return nil } +func (self *Loop) handle_mouse_event(ev *MouseEvent) error { + if self.OnMouseEvent != nil { + return self.OnMouseEvent(ev) + } + return nil +} + func (self *Loop) handle_key_event(ev *KeyEvent) error { if self.OnKeyEvent != nil { err := self.OnKeyEvent(ev)