diff --git a/examples/go.mod b/examples/go.mod index 1ee91e8780..22777d70e2 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -12,7 +12,7 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-isatty v0.0.17 github.com/muesli/reflow v0.3.0 - github.com/muesli/termenv v0.13.0 + github.com/muesli/termenv v0.13.1-0.20221110215218-eb6391ce0665 ) replace github.com/charmbracelet/bubbletea => ../ diff --git a/examples/go.sum b/examples/go.sum index 08124408eb..355e0aefcc 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -2,8 +2,9 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E= +github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/charmbracelet/bubbles v0.13.1-0.20220815142520-649f78e1fd8b h1:ppafRlD8VXOEqnUPkMvO7et9rpSsKVjUX+K3QG7tAB0= @@ -53,8 +54,9 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/muesli/termenv v0.13.1-0.20221110215218-eb6391ce0665 h1:/pxOi1t70m6p/8AkJQjJBlZKB53hw5r5YLsLuCNmbM4= +github.com/muesli/termenv v0.13.1-0.20221110215218-eb6391ce0665/go.mod h1:er0g7V37a54K9xuhQub9EKVAu9xiELxV7TtyZss04mo= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/examples/mouse/main.go b/examples/mouse/main.go index ea5c11d40a..da923ad863 100644 --- a/examples/mouse/main.go +++ b/examples/mouse/main.go @@ -4,21 +4,19 @@ package main // coordinates and events. import ( - "fmt" "log" tea "github.com/charmbracelet/bubbletea" ) func main() { - p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseAllMotion()) + p := tea.NewProgram(model{}, tea.WithMouseAllMotion()) if _, err := p.Run(); err != nil { log.Fatal(err) } } type model struct { - init bool mouseEvent tea.MouseEvent } @@ -34,20 +32,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case tea.MouseMsg: - m.init = true - m.mouseEvent = tea.MouseEvent(msg) + return m, tea.Printf("(X: %d, Y: %d) %s", msg.X, msg.Y, tea.MouseEvent(msg)) } return m, nil } func (m model) View() string { - s := "Do mouse stuff. When you're done press q to quit.\n\n" - - if m.init { - e := m.mouseEvent - s += fmt.Sprintf("(X: %d, Y: %d) %s", e.X, e.Y, e) - } + s := "Do mouse stuff. When you're done press q to quit.\n" return s } diff --git a/go.mod b/go.mod index d3a0c9746a..74990eb6f9 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,14 @@ module github.com/charmbracelet/bubbletea go 1.16 require ( + github.com/aymanbagabas/go-osc52 v1.2.1 // indirect github.com/containerd/console v1.0.3 - github.com/mattn/go-isatty v0.0.16 + github.com/mattn/go-isatty v0.0.17 github.com/mattn/go-localereader v0.0.1 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b github.com/muesli/cancelreader v0.2.2 github.com/muesli/reflow v0.3.0 - github.com/muesli/termenv v0.13.0 + github.com/muesli/termenv v0.14.0 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/text v0.3.7 // indirect ) diff --git a/go.sum b/go.sum index 76e0059e1d..87f3389fd6 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,14 @@ -github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E= +github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -20,6 +23,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/muesli/termenv v0.14.0 h1:8x9NFfOe8lmIWK4pgy3IfVEy47f+ppe3tUqdPZG2Uy0= +github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/key.go b/key.go index c2e5e3ab01..4296d96f37 100644 --- a/key.go +++ b/key.go @@ -581,7 +581,7 @@ func readInputs(input io.Reader) ([]Msg, error) { // Check if it's a mouse event. For now we're parsing X10-type mouse events // only. - mouseEvent, err := parseX10MouseEvents(b) + mouseEvent, err := parseMouseEvents(b) if err == nil { var m []Msg for _, v := range mouseEvent { diff --git a/mouse.go b/mouse.go index 1b712d12b9..518cdebeea 100644 --- a/mouse.go +++ b/mouse.go @@ -3,8 +3,13 @@ package tea import ( "bytes" "errors" + "regexp" + "strconv" + "strings" ) +const mouseX10ByteOffset = 32 + // MouseMsg contains information about a mouse event and are sent to a programs // update function when mouse activity occurs. Note that the mouse must first // be enabled via in order the mouse events to be received. @@ -13,12 +18,24 @@ type MouseMsg MouseEvent // MouseEvent represents a mouse event, which could be a click, a scroll wheel // movement, a cursor movement, or a combination. type MouseEvent struct { - X int - Y int - Type MouseEventType - Shift bool - Alt bool - Ctrl bool + X int + Y int + Shift bool + Alt bool + Ctrl bool + Action MouseAction + Button MouseButton + + // Deprecated: Use MouseAction & MouseButton instead. + Type MouseEventType + + isSGR bool +} + +// IsWheel returns true if the mouse event is a wheel event. +func (m MouseEvent) IsWheel() bool { + return m.Button == MouseButtonWheelUp || m.Button == MouseButtonWheelDown || + m.Button == MouseButtonWheelLeft || m.Button == MouseButtonWheelRight } // String returns a string representation of a mouse event. @@ -32,38 +49,212 @@ func (m MouseEvent) String() (s string) { if m.Shift { s += "shift+" } - s += mouseEventTypes[m.Type] + + if m.isSGR { + if m.Button == MouseButtonNone && m.Action == MouseActionMotion { + s += mouseActions[m.Action] + } else if m.IsWheel() { + s += mouseButtons[m.Button] + } else { + s += mouseButtons[m.Button] + s += " " + s += mouseActions[m.Action] + } + } else { + s += mouseEventTypes[m.Type] + } + return s } +// MouseAction represents the action that occurred during a mouse event. +type MouseAction int + +// Mouse event actions. +const ( + MouseActionPress MouseAction = iota + MouseActionRelease + MouseActionMotion +) + +var mouseActions = map[MouseAction]string{ + MouseActionPress: "press", + MouseActionRelease: "release", + MouseActionMotion: "motion", +} + +// MouseButton represents the button that was pressed during a mouse event. +type MouseButton int + +// Mouse event buttons +// +// This is based on X11 mouse button codes. +// +// 1 = left button +// 2 = middle button (pressing the scroll wheel) +// 3 = right button +// 4 = turn scroll wheel up +// 5 = turn scroll wheel down +// 6 = push scroll wheel left +// 7 = push scroll wheel right +// 8 = 4th button (aka browser backward button) +// 9 = 5th button (aka browser forward button) +// 10 +// 11 +// +// Other buttons are not supported. +const ( + MouseButtonNone MouseButton = iota + MouseButtonLeft + MouseButtonMiddle + MouseButtonRight + MouseButtonWheelUp + MouseButtonWheelDown + MouseButtonWheelLeft + MouseButtonWheelRight + MouseButtonBackward + MouseButtonForward + MouseButton10 + MouseButton11 + + MouseButtonUnknown +) + +var mouseButtons = map[MouseButton]string{ + MouseButtonNone: "none", + MouseButtonLeft: "left", + MouseButtonMiddle: "middle", + MouseButtonRight: "right", + MouseButtonWheelUp: "wheel up", + MouseButtonWheelDown: "wheel down", + MouseButtonWheelLeft: "wheel left", + MouseButtonWheelRight: "wheel right", + MouseButtonBackward: "backward", + MouseButtonForward: "forward", + MouseButton10: "button 10", + MouseButton11: "button 11", + MouseButtonUnknown: "unknown", +} + // MouseEventType indicates the type of mouse event occurring. +// +// Deprecated: Use MouseAction & MouseButton instead. type MouseEventType int // Mouse event types. +// +// Deprecated: Use MouseAction & MouseButton instead. const ( MouseUnknown MouseEventType = iota MouseLeft MouseRight MouseMiddle - MouseRelease + MouseRelease // mouse button release (X10 only) MouseWheelUp MouseWheelDown + MouseWheelLeft + MouseWheelRight + MouseBackward + MouseForward MouseMotion ) var mouseEventTypes = map[MouseEventType]string{ - MouseUnknown: "unknown", - MouseLeft: "left", - MouseRight: "right", - MouseMiddle: "middle", - MouseRelease: "release", - MouseWheelUp: "wheel up", - MouseWheelDown: "wheel down", - MouseMotion: "motion", + MouseUnknown: "unknown", + MouseLeft: "left", + MouseRight: "right", + MouseMiddle: "middle", + MouseRelease: "release", + MouseWheelUp: "wheel up", + MouseWheelDown: "wheel down", + MouseWheelLeft: "wheel left", + MouseWheelRight: "wheel right", + MouseBackward: "backward", + MouseForward: "forward", + MouseMotion: "motion", +} + +var ( + mouseX10Seq = []byte("\x1b[M") + mouseSGRSeq = []byte("\x1b[<") +) + +func parseMouseEvents(buf []byte) ([]MouseEvent, error) { + if len(buf) == 0 { + return nil, errors.New("empty buffer") + } + + switch { + case bytes.Contains(buf, mouseSGRSeq): + return parseSGRMouseEvents(string(buf)) + case bytes.Contains(buf, mouseX10Seq): + return parseX10MouseEvents(buf) + } + + return nil, errors.New("not a mouse event") +} + +var mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`) + +// parseSGRMouseEvents parses SGR extended mouse events. SGR mouse events look +// like: +// +// ESC [ < Cb ; Cx ; Cy (M or m) +// +// where: +// +// Cb is the encoded button code +// Cx is the x-coordinate of the mouse +// Cy is the y-coordinate of the mouse +// M is for button press, m is for button release +// +// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates +func parseSGRMouseEvents(buf string) ([]MouseEvent, error) { + var ev []MouseEvent + + seq := string(mouseSGRSeq) + if !strings.Contains(buf, seq) { + return nil, errors.New("not a SGR mouse event") + } + + for _, v := range strings.Split(buf, seq) { + if len(v) == 0 { + continue + } + + matches := mouseSGRRegex.FindStringSubmatch(v) + if len(matches) != 5 { + return nil, errors.New("not a SGR mouse event") + } + + b, _ := strconv.Atoi(matches[1]) + px := matches[2] + py := matches[3] + release := matches[4] == "m" + m := parseMouseButton(b, true) + // Wheel buttons don't have release events + // Motion can be reported as a release event in some terminals (Windows Terminal) + if m.Action != MouseActionMotion && !m.IsWheel() && release { + m.Action = MouseActionRelease + m.Type = MouseRelease + } + + x, _ := strconv.Atoi(px) + y, _ := strconv.Atoi(py) + + // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). + m.X = x - 1 + m.Y = y - 1 + + ev = append(ev, m) + } + + return ev, nil } // Parse X10-encoded mouse events; the simplest kind. The last release of X10 -// was December 1986, by the way. +// was December 1986, by the way. The original X10 mouse protocol limits the Cx +// and Cy ordinates to 223 (=255 - 32). // // X10 mouse events look like: // @@ -73,7 +264,7 @@ var mouseEventTypes = map[MouseEventType]string{ func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) { var r []MouseEvent - seq := []byte("\x1b[M") + seq := mouseX10Seq if !bytes.Contains(buf, seq) { return r, errors.New("not an X10 mouse event") } @@ -86,65 +277,99 @@ func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) { return r, errors.New("not an X10 mouse event") } - var m MouseEvent - const byteOffset = 32 - e := v[0] - byteOffset - - const ( - bitShift = 0b0000_0100 - bitAlt = 0b0000_1000 - bitCtrl = 0b0001_0000 - bitMotion = 0b0010_0000 - bitWheel = 0b0100_0000 - - bitsMask = 0b0000_0011 - - bitsLeft = 0b0000_0000 - bitsMiddle = 0b0000_0001 - bitsRight = 0b0000_0010 - bitsRelease = 0b0000_0011 - - bitsWheelUp = 0b0000_0000 - bitsWheelDown = 0b0000_0001 - ) - - if e&bitWheel != 0 { - // Check the low two bits. - switch e & bitsMask { - case bitsWheelUp: - m.Type = MouseWheelUp - case bitsWheelDown: - m.Type = MouseWheelDown - } - } else { - // Check the low two bits. - // We do not separate clicking and dragging. - switch e & bitsMask { - case bitsLeft: - m.Type = MouseLeft - case bitsMiddle: - m.Type = MouseMiddle - case bitsRight: - m.Type = MouseRight - case bitsRelease: - if e&bitMotion != 0 { - m.Type = MouseMotion - } else { - m.Type = MouseRelease - } - } - } - - m.Shift = e&bitShift != 0 - m.Alt = e&bitAlt != 0 - m.Ctrl = e&bitCtrl != 0 + m := parseMouseButton(int(v[0]), false) // (1,1) is the upper left. We subtract 1 to normalize it to (0,0). - m.X = int(v[1]) - byteOffset - 1 - m.Y = int(v[2]) - byteOffset - 1 + m.X = int(v[1]) - mouseX10ByteOffset - 1 + m.Y = int(v[2]) - mouseX10ByteOffset - 1 r = append(r, m) } return r, nil } + +// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates +func parseMouseButton(b int, isSGR bool) MouseEvent { + var m MouseEvent + m.isSGR = isSGR + e := b + if !isSGR { + e -= mouseX10ByteOffset + } + + const ( + bitShift = 0b0000_0100 + bitAlt = 0b0000_1000 + bitCtrl = 0b0001_0000 + bitMotion = 0b0010_0000 + bitWheel = 0b0100_0000 + bitAdd = 0b1000_0000 // additional buttons 8-11 + + bitsMask = 0b0000_0011 + ) + + if e&bitAdd != 0 { + m.Button = MouseButtonBackward + MouseButton(e&bitsMask) + } else if e&bitWheel != 0 { + m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask) + } else { + m.Button = MouseButtonLeft + MouseButton(e&bitsMask) + // X10 reports a button release as 0b0000_0011 (3) + if e&bitsMask == bitsMask { + m.Action = MouseActionRelease + m.Button = MouseButtonNone + } + } + // Motion bit doesn't get reported for wheel events but we check for it + // anyway in case of a faulty terminal emulator. + if e&bitMotion != 0 && !m.IsWheel() { + m.Action = MouseActionMotion + } + + // backward compatibility + switch { + case m.Button == MouseButtonLeft && m.Action == MouseActionPress: + m.Type = MouseLeft + case m.Button == MouseButtonMiddle && m.Action == MouseActionPress: + m.Type = MouseMiddle + case m.Button == MouseButtonRight && m.Action == MouseActionPress: + m.Type = MouseRight + case m.Button == MouseButtonNone && m.Action == MouseActionRelease: + m.Type = MouseRelease + case m.Button == MouseButtonWheelUp && m.Action == MouseActionPress: + m.Type = MouseWheelUp + case m.Button == MouseButtonWheelDown && m.Action == MouseActionPress: + m.Type = MouseWheelDown + case m.Button == MouseButtonWheelLeft && m.Action == MouseActionPress: + m.Type = MouseWheelLeft + case m.Button == MouseButtonWheelRight && m.Action == MouseActionPress: + m.Type = MouseWheelRight + case m.Button == MouseButtonBackward && m.Action == MouseActionPress: + m.Type = MouseBackward + case m.Button == MouseButtonForward && m.Action == MouseActionPress: + m.Type = MouseForward + case m.Action == MouseActionMotion: + m.Type = MouseMotion + switch m.Button { + case MouseButtonLeft: + m.Type = MouseLeft + case MouseButtonMiddle: + m.Type = MouseMiddle + case MouseButtonRight: + m.Type = MouseRight + case MouseButtonBackward: + m.Type = MouseBackward + case MouseButtonForward: + m.Type = MouseForward + } + default: + m.Type = MouseUnknown + } + + m.Alt = e&bitAlt != 0 + m.Ctrl = e&bitCtrl != 0 + m.Shift = e&bitShift != 0 + + return m +} diff --git a/mouse_test.go b/mouse_test.go index 8aac7086bf..3bdd7661aa 100644 --- a/mouse_test.go +++ b/mouse_test.go @@ -1,6 +1,9 @@ package tea -import "testing" +import ( + "fmt" + "testing" +) func TestMouseEvent_String(t *testing.T) { tt := []struct { @@ -9,110 +12,176 @@ func TestMouseEvent_String(t *testing.T) { expected string }{ { - name: "unknown", - event: MouseEvent{Type: MouseUnknown}, + name: "unknown", + event: MouseEvent{ + Action: MouseActionPress, + Button: MouseButtonNone, + Type: MouseUnknown, + }, expected: "unknown", }, { - name: "left", - event: MouseEvent{Type: MouseLeft}, + name: "left", + event: MouseEvent{ + Action: MouseActionPress, + Button: MouseButtonNone, + Type: MouseLeft, + }, expected: "left", }, { - name: "right", - event: MouseEvent{Type: MouseRight}, + name: "right", + event: MouseEvent{ + Action: MouseActionPress, + Button: MouseButtonNone, + Type: MouseRight, + }, expected: "right", }, { - name: "middle", - event: MouseEvent{Type: MouseMiddle}, + name: "middle", + event: MouseEvent{ + Action: MouseActionPress, + Button: MouseButtonNone, + Type: MouseMiddle, + }, expected: "middle", }, { - name: "release", - event: MouseEvent{Type: MouseRelease}, + name: "release", + event: MouseEvent{ + Action: MouseActionPress, + Button: MouseButtonNone, + Type: MouseRelease, + }, expected: "release", }, { - name: "wheel up", - event: MouseEvent{Type: MouseWheelUp}, + name: "wheel up", + event: MouseEvent{ + Action: MouseActionPress, + Button: MouseButtonNone, + Type: MouseWheelUp, + }, expected: "wheel up", }, { - name: "wheel down", - event: MouseEvent{Type: MouseWheelDown}, + name: "wheel down", + event: MouseEvent{ + Action: MouseActionPress, + Button: MouseButtonNone, + Type: MouseWheelDown, + }, expected: "wheel down", }, { - name: "motion", - event: MouseEvent{Type: MouseMotion}, + name: "wheel left", + event: MouseEvent{ + Action: MouseActionPress, + Button: MouseButtonNone, + Type: MouseWheelLeft, + }, + expected: "wheel left", + }, + { + name: "wheel right", + event: MouseEvent{ + Action: MouseActionPress, + Button: MouseButtonNone, + Type: MouseWheelRight, + }, + expected: "wheel right", + }, + { + name: "motion", + event: MouseEvent{ + Action: MouseActionPress, + Button: MouseButtonNone, + Type: MouseMotion, + }, expected: "motion", }, { name: "shift+left", event: MouseEvent{ - Type: MouseLeft, - Shift: true, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonNone, + Shift: true, }, expected: "shift+left", }, { name: "ctrl+shift+left", event: MouseEvent{ - Type: MouseLeft, - Shift: true, - Ctrl: true, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonNone, + Shift: true, + Ctrl: true, }, expected: "ctrl+shift+left", }, { name: "alt+left", event: MouseEvent{ - Type: MouseLeft, - Alt: true, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonNone, + Alt: true, }, expected: "alt+left", }, { name: "ctrl+left", event: MouseEvent{ - Type: MouseLeft, - Ctrl: true, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonNone, + Ctrl: true, }, expected: "ctrl+left", }, { name: "ctrl+alt+left", event: MouseEvent{ - Type: MouseLeft, - Alt: true, - Ctrl: true, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonNone, + Alt: true, + Ctrl: true, }, expected: "ctrl+alt+left", }, { name: "ctrl+alt+shift+left", event: MouseEvent{ - Type: MouseLeft, - Alt: true, - Ctrl: true, - Shift: true, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonNone, + Alt: true, + Ctrl: true, + Shift: true, }, expected: "ctrl+alt+shift+left", }, { name: "ignore coordinates", event: MouseEvent{ - X: 100, - Y: 200, - Type: MouseLeft, + X: 100, + Y: 200, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonNone, }, expected: "left", }, { name: "broken type", event: MouseEvent{ - Type: MouseEventType(-1000), + Type: MouseEventType(-100), + Action: MouseActionPress, + Button: MouseButtonNone, }, expected: "", }, @@ -154,23 +223,27 @@ func TestParseX10MouseEvent(t *testing.T) { // Position. { name: "zero position", - buf: encode(0b0010_0000, 0, 0), + buf: encode(0b0000_0000, 0, 0), expected: []MouseEvent{ { - X: 0, - Y: 0, - Type: MouseLeft, + X: 0, + Y: 0, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonLeft, }, }, }, { name: "max position", - buf: encode(0b0010_0000, 222, 222), // Because 255 (max int8) - 32 - 1. + buf: encode(0b0000_0000, 222, 222), // Because 255 (max int8) - 32 - 1. expected: []MouseEvent{ { - X: 222, - Y: 222, - Type: MouseLeft, + X: 222, + Y: 222, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonLeft, }, }, }, @@ -180,9 +253,11 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0000_0000, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseLeft, + X: 32, + Y: 16, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonLeft, }, }, }, @@ -191,9 +266,11 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0010_0000, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseLeft, + X: 32, + Y: 16, + Type: MouseLeft, + Action: MouseActionMotion, + Button: MouseButtonLeft, }, }, }, @@ -202,9 +279,11 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0000_0001, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseMiddle, + X: 32, + Y: 16, + Type: MouseMiddle, + Action: MouseActionPress, + Button: MouseButtonMiddle, }, }, }, @@ -213,9 +292,11 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0010_0001, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseMiddle, + X: 32, + Y: 16, + Type: MouseMiddle, + Action: MouseActionMotion, + Button: MouseButtonMiddle, }, }, }, @@ -224,9 +305,11 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0000_0010, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseRight, + X: 32, + Y: 16, + Type: MouseRight, + Action: MouseActionPress, + Button: MouseButtonRight, }, }, }, @@ -235,9 +318,11 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0010_0010, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseRight, + X: 32, + Y: 16, + Type: MouseRight, + Action: MouseActionMotion, + Button: MouseButtonRight, }, }, }, @@ -246,9 +331,11 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0010_0011, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseMotion, + X: 32, + Y: 16, + Type: MouseMotion, + Action: MouseActionMotion, + Button: MouseButtonNone, }, }, }, @@ -257,9 +344,11 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0100_0000, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseWheelUp, + X: 32, + Y: 16, + Type: MouseWheelUp, + Action: MouseActionPress, + Button: MouseButtonWheelUp, }, }, }, @@ -268,9 +357,37 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0100_0001, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseWheelDown, + X: 32, + Y: 16, + Type: MouseWheelDown, + Action: MouseActionPress, + Button: MouseButtonWheelDown, + }, + }, + }, + { + name: "wheel left", + buf: encode(0b0100_0010, 32, 16), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelLeft, + Action: MouseActionPress, + Button: MouseButtonWheelLeft, + }, + }, + }, + { + name: "wheel right", + buf: encode(0b0100_0011, 32, 16), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelRight, + Action: MouseActionPress, + Button: MouseButtonWheelRight, }, }, }, @@ -279,108 +396,178 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0000_0011, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseRelease, + X: 32, + Y: 16, + Type: MouseRelease, + Action: MouseActionRelease, + Button: MouseButtonNone, + }, + }, + }, + { + name: "backward", + buf: encode(0b1000_0000, 32, 16), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseBackward, + Action: MouseActionPress, + Button: MouseButtonBackward, + }, + }, + }, + { + name: "forward", + buf: encode(0b1000_0001, 32, 16), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseForward, + Action: MouseActionPress, + Button: MouseButtonForward, + }, + }, + }, + { + name: "button 10", + buf: encode(0b1000_0010, 32, 16), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseUnknown, + Action: MouseActionPress, + Button: MouseButton10, + }, + }, + }, + { + name: "button 11", + buf: encode(0b1000_0011, 32, 16), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseUnknown, + Action: MouseActionPress, + Button: MouseButton11, }, }, }, // Combinations. { name: "alt+right", - buf: encode(0b0010_1010, 32, 16), + buf: encode(0b0000_1010, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseRight, - Alt: true, + X: 32, + Y: 16, + Alt: true, + Type: MouseRight, + Action: MouseActionPress, + Button: MouseButtonRight, }, }, }, { name: "ctrl+right", - buf: encode(0b0011_0010, 32, 16), + buf: encode(0b0001_0010, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseRight, - Ctrl: true, + X: 32, + Y: 16, + Ctrl: true, + Type: MouseRight, + Action: MouseActionPress, + Button: MouseButtonRight, }, }, }, { - name: "ctrl+alt+right", - buf: encode(0b0011_1010, 32, 16), + name: "alt+right in motion", + buf: encode(0b0010_1010, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseRight, - Alt: true, - Ctrl: true, + X: 32, + Y: 16, + Alt: true, + Type: MouseRight, + Action: MouseActionMotion, + Button: MouseButtonRight, }, }, }, { - name: "alt+wheel down", - buf: encode(0b0100_1001, 32, 16), + name: "ctrl+right in motion", + buf: encode(0b0011_0010, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseWheelDown, - Alt: true, + X: 32, + Y: 16, + Ctrl: true, + Type: MouseRight, + Action: MouseActionMotion, + Button: MouseButtonRight, }, }, }, { - name: "ctrl+wheel down", - buf: encode(0b0101_0001, 32, 16), + name: "ctrl+alt+right", + buf: encode(0b0001_1010, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseWheelDown, - Ctrl: true, + X: 32, + Y: 16, + Alt: true, + Ctrl: true, + Type: MouseRight, + Action: MouseActionPress, + Button: MouseButtonRight, }, }, }, { - name: "ctrl+alt+wheel down", - buf: encode(0b0101_1001, 32, 16), + name: "ctrl+wheel up", + buf: encode(0b0101_0000, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseWheelDown, - Alt: true, - Ctrl: true, + X: 32, + Y: 16, + Ctrl: true, + Type: MouseWheelUp, + Action: MouseActionPress, + Button: MouseButtonWheelUp, }, }, }, - // Unknown. { - name: "wheel with unknown bit", - buf: encode(0b0100_0010, 32, 16), + name: "alt+wheel down", + buf: encode(0b0100_1001, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseUnknown, + X: 32, + Y: 16, + Alt: true, + Type: MouseWheelDown, + Action: MouseActionPress, + Button: MouseButtonWheelDown, }, }, }, { - name: "unknown with modifier", - buf: encode(0b0100_1010, 32, 16), + name: "ctrl+alt+wheel down", + buf: encode(0b0101_1001, 32, 16), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseUnknown, - Alt: true, + X: 32, + Y: 16, + Alt: true, + Ctrl: true, + Type: MouseWheelDown, + Action: MouseActionPress, + Button: MouseButtonWheelDown, }, }, }, @@ -390,9 +577,11 @@ func TestParseX10MouseEvent(t *testing.T) { buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1. expected: []MouseEvent{ { - X: -6, - Y: -33, - Type: MouseLeft, + X: -6, + Y: -33, + Type: MouseLeft, + Action: MouseActionMotion, + Button: MouseButtonLeft, }, }, }, @@ -402,14 +591,18 @@ func TestParseX10MouseEvent(t *testing.T) { buf: append(encode(0b0010_0000, 32, 16), encode(0b0000_0011, 64, 32)...), expected: []MouseEvent{ { - X: 32, - Y: 16, - Type: MouseLeft, + X: 32, + Y: 16, + Type: MouseLeft, + Action: MouseActionMotion, + Button: MouseButtonLeft, }, { - X: 64, - Y: 32, - Type: MouseRelease, + X: 64, + Y: 32, + Type: MouseRelease, + Action: MouseActionRelease, + Button: MouseButtonNone, }, }, }, @@ -473,3 +666,451 @@ func TestParseX10MouseEvent_error(t *testing.T) { }) } } + +func TestParseSGRMouseEvent(t *testing.T) { + encode := func(b, x, y int, r bool) string { + re := 'M' + if r { + re = 'm' + } + return fmt.Sprintf("\x1b[<%d;%d;%d%c", b, x+1, y+1, re) + } + + tt := []struct { + name string + buf string + expected []MouseEvent + }{ + // Position. + { + name: "zero position", + buf: encode(0, 0, 0, false), + expected: []MouseEvent{ + { + X: 0, + Y: 0, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonLeft, + isSGR: true, + }, + }, + }, + { + name: "225 position", + buf: encode(0, 225, 225, false), + expected: []MouseEvent{ + { + X: 225, + Y: 225, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonLeft, + isSGR: true, + }, + }, + }, + // Simple. + { + name: "left", + buf: encode(0, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonLeft, + isSGR: true, + }, + }, + }, + { + name: "left in motion", + buf: encode(32, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseLeft, + Action: MouseActionMotion, + Button: MouseButtonLeft, + isSGR: true, + }, + }, + }, + { + name: "left release", + buf: encode(0, 32, 16, true), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseRelease, + Action: MouseActionRelease, + Button: MouseButtonLeft, + isSGR: true, + }, + }, + }, + { + name: "middle", + buf: encode(1, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseMiddle, + Action: MouseActionPress, + Button: MouseButtonMiddle, + isSGR: true, + }, + }, + }, + { + name: "middle in motion", + buf: encode(33, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseMiddle, + Action: MouseActionMotion, + Button: MouseButtonMiddle, + isSGR: true, + }, + }, + }, + { + name: "middle release", + buf: encode(1, 32, 16, true), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseRelease, + Action: MouseActionRelease, + Button: MouseButtonMiddle, + isSGR: true, + }, + }, + }, + { + name: "right", + buf: encode(2, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseRight, + Action: MouseActionPress, + Button: MouseButtonRight, + isSGR: true, + }, + }, + }, + { + name: "right release", + buf: encode(2, 32, 16, true), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseRelease, + Action: MouseActionRelease, + Button: MouseButtonRight, + isSGR: true, + }, + }, + }, + { + name: "motion", + buf: encode(35, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseMotion, + Action: MouseActionMotion, + Button: MouseButtonNone, + isSGR: true, + }, + }, + }, + { + name: "wheel up", + buf: encode(64, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelUp, + Action: MouseActionPress, + Button: MouseButtonWheelUp, + isSGR: true, + }, + }, + }, + { + name: "wheel down", + buf: encode(65, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelDown, + Action: MouseActionPress, + Button: MouseButtonWheelDown, + isSGR: true, + }, + }, + }, + { + name: "wheel left", + buf: encode(66, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelLeft, + Action: MouseActionPress, + Button: MouseButtonWheelLeft, + isSGR: true, + }, + }, + }, + { + name: "wheel right", + buf: encode(67, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelRight, + Action: MouseActionPress, + Button: MouseButtonWheelRight, + isSGR: true, + }, + }, + }, + { + name: "backward", + buf: encode(128, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseBackward, + Action: MouseActionPress, + Button: MouseButtonBackward, + isSGR: true, + }, + }, + }, + { + name: "backward in motion", + buf: encode(160, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseBackward, + Action: MouseActionMotion, + Button: MouseButtonBackward, + isSGR: true, + }, + }, + }, + { + name: "forward", + buf: encode(129, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseForward, + Action: MouseActionPress, + Button: MouseButtonForward, + isSGR: true, + }, + }, + }, + { + name: "forward in motion", + buf: encode(161, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseForward, + Action: MouseActionMotion, + Button: MouseButtonForward, + isSGR: true, + }, + }, + }, + // Combinations. + { + name: "alt+right", + buf: encode(10, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Alt: true, + Type: MouseRight, + Action: MouseActionPress, + Button: MouseButtonRight, + isSGR: true, + }, + }, + }, + { + name: "ctrl+right", + buf: encode(18, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Ctrl: true, + Type: MouseRight, + Action: MouseActionPress, + Button: MouseButtonRight, + isSGR: true, + }, + }, + }, + { + name: "ctrl+alt+right", + buf: encode(26, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Alt: true, + Ctrl: true, + Type: MouseRight, + Action: MouseActionPress, + Button: MouseButtonRight, + isSGR: true, + }, + }, + }, + { + name: "alt+wheel press", + buf: encode(73, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Alt: true, + Type: MouseWheelDown, + Action: MouseActionPress, + Button: MouseButtonWheelDown, + isSGR: true, + }, + }, + }, + { + name: "ctrl+wheel press", + buf: encode(81, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Ctrl: true, + Type: MouseWheelDown, + Action: MouseActionPress, + Button: MouseButtonWheelDown, + isSGR: true, + }, + }, + }, + { + name: "ctrl+alt+wheel press", + buf: encode(89, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Alt: true, + Ctrl: true, + Type: MouseWheelDown, + Action: MouseActionPress, + Button: MouseButtonWheelDown, + isSGR: true, + }, + }, + }, + { + name: "ctrl+alt+shift+wheel press", + buf: encode(93, 32, 16, false), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Shift: true, + Alt: true, + Ctrl: true, + Type: MouseWheelDown, + Action: MouseActionPress, + Button: MouseButtonWheelDown, + isSGR: true, + }, + }, + }, + // Batched events. + { + name: "batched events", + buf: encode(0, 32, 16, false) + encode(35, 40, 30, false) + encode(0, 64, 32, true), + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseLeft, + Action: MouseActionPress, + Button: MouseButtonLeft, + isSGR: true, + }, + { + X: 40, + Y: 30, + Type: MouseMotion, + Action: MouseActionMotion, + Button: MouseButtonNone, + isSGR: true, + }, + { + X: 64, + Y: 32, + Type: MouseRelease, + Action: MouseActionRelease, + Button: MouseButtonLeft, + isSGR: true, + }, + }, + }, + } + + for i := range tt { + tc := tt[i] + + t.Run(tc.name, func(t *testing.T) { + actual, err := parseSGRMouseEvents(tc.buf) + if err != nil { + t.Fatalf("unexpected error for test: %v", + err, + ) + } + + for i := range tc.expected { + if tc.expected[i] != actual[i] { + t.Fatalf("expected %#v but got %#v", + tc.expected[i], + actual[i], + ) + } + } + }) + } +} diff --git a/options.go b/options.go index d9ce42c7c2..6953570546 100644 --- a/options.go +++ b/options.go @@ -93,6 +93,9 @@ func WithAltScreen() ProgramOption { // movement events are also captured if a mouse button is pressed (i.e., drag // events). Cell motion mode is better supported than all motion mode. // +// This will try to enable the mouse in extended mode (SGR), if that is not +// supported by the terminal it will fall back to normal mode (X10). +// // To enable mouse cell motion once the program has already started running use // the EnableMouseCellMotion command. To disable the mouse when the program is // running use the DisableMouse command. @@ -112,6 +115,9 @@ func WithMouseCellMotion() ProgramOption { // wheel, and motion events, which are delivered regardless of whether a mouse // button is pressed, effectively enabling support for hover interactions. // +// This will try to enable the mouse in extended mode (SGR), if that is not +// supported by the terminal it will fall back to normal mode (X10). +// // Many modern terminals support this, but not all. If in doubt, use // EnableMouseCellMotion instead. // diff --git a/renderer.go b/renderer.go index a6f416277f..ffa657470f 100644 --- a/renderer.go +++ b/renderer.go @@ -38,17 +38,23 @@ type renderer interface { // enableMouseCellMotion enables mouse click, release, wheel and motion // events if a mouse button is pressed (i.e., drag events). + // + // This will try to use the "1006" extended mode if available, falling back + // to "1002" if not. enableMouseCellMotion() - // DisableMouseCellMotion disables Mouse Cell Motion tracking. + // disableMouseCellMotion disables Mouse Cell Motion tracking. disableMouseCellMotion() - // EnableMouseAllMotion enables mouse click, release, wheel and motion + // enableMouseAllMotion enables mouse click, release, wheel and motion // events, regardless of whether a mouse button is pressed. Many modern // terminals support this, but not all. + // + // This will try to use the "1006" extended mode if available, falling back + // to "1003" if not. enableMouseAllMotion() - // DisableMouseAllMotion disables All Motion mouse tracking. + // disableMouseAllMotion disables All Motion mouse tracking. disableMouseAllMotion() } diff --git a/screen_test.go b/screen_test.go index 2f305e3d15..08d053394e 100644 --- a/screen_test.go +++ b/screen_test.go @@ -14,42 +14,42 @@ func TestClearMsg(t *testing.T) { { name: "clear_screen", cmds: []Cmd{ClearScreen}, - expected: "\x1b[?25l\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[2J\x1b[1;1H\x1b[1;1Hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1006l\x1b[?1003l\x1b[?1006l", }, { name: "altscreen", cmds: []Cmd{EnterAltScreen, ExitAltScreen}, - expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?25l\x1b[?1049l\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?25l\x1b[?1049l\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1006l\x1b[?1003l\x1b[?1006l", }, { name: "altscreen_autoexit", cmds: []Cmd{EnterAltScreen}, - expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?25lsuccess\r\n\x1b[2;0H\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1049l\x1b[?25h", + expected: "\x1b[?25l\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[1;1H\x1b[?25lsuccess\r\n\x1b[2;0H\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1006l\x1b[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h", }, { name: "mouse_cellmotion", cmds: []Cmd{EnableMouseCellMotion}, - expected: "\x1b[?25l\x1b[?1002hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?1002h\x1b[?1006hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1006l\x1b[?1003l\x1b[?1006l", }, { name: "mouse_allmotion", cmds: []Cmd{EnableMouseAllMotion}, - expected: "\x1b[?25l\x1b[?1003hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?1003h\x1b[?1006hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1006l\x1b[?1003l\x1b[?1006l", }, { name: "mouse_disable", cmds: []Cmd{EnableMouseAllMotion, DisableMouse}, - expected: "\x1b[?25l\x1b[?1003h\x1b[?1002l\x1b[?1003lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?1003h\x1b[?1006h\x1b[?1002l\x1b[?1006l\x1b[?1003l\x1b[?1006lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1006l\x1b[?1003l\x1b[?1006l", }, { name: "cursor_hide", cmds: []Cmd{HideCursor}, - expected: "\x1b[?25l\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?25lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1006l\x1b[?1003l\x1b[?1006l", }, { name: "cursor_hideshow", cmds: []Cmd{HideCursor, ShowCursor}, - expected: "\x1b[?25l\x1b[?25l\x1b[?25hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l", + expected: "\x1b[?25l\x1b[?25l\x1b[?25hsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1006l\x1b[?1003l\x1b[?1006l", }, } diff --git a/standard_renderer.go b/standard_renderer.go index 17b44e2301..90997c38f5 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -358,6 +358,8 @@ func (r *standardRenderer) enableMouseCellMotion() { defer r.mtx.Unlock() r.out.EnableMouseCellMotion() + // mouse mode (1006) is a no-op if the terminal doesn't support it. + r.out.EnableMouseExtendedMode() } func (r *standardRenderer) disableMouseCellMotion() { @@ -365,6 +367,7 @@ func (r *standardRenderer) disableMouseCellMotion() { defer r.mtx.Unlock() r.out.DisableMouseCellMotion() + r.out.DisableMouseExtendedMode() } func (r *standardRenderer) enableMouseAllMotion() { @@ -372,6 +375,8 @@ func (r *standardRenderer) enableMouseAllMotion() { defer r.mtx.Unlock() r.out.EnableMouseAllMotion() + // mouse mode (1006) is a no-op if the terminal doesn't support it. + r.out.EnableMouseExtendedMode() } func (r *standardRenderer) disableMouseAllMotion() { @@ -379,6 +384,7 @@ func (r *standardRenderer) disableMouseAllMotion() { defer r.mtx.Unlock() r.out.DisableMouseAllMotion() + r.out.DisableMouseExtendedMode() } // setIgnoredLines specifies lines not to be touched by the standard Bubble Tea diff --git a/tea.go b/tea.go index 27bc3e3f6e..523c7b6023 100644 --- a/tea.go +++ b/tea.go @@ -254,6 +254,11 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} { return ch } +func (p *Program) disableMouse() { + p.renderer.disableMouseCellMotion() + p.renderer.disableMouseAllMotion() +} + // eventLoop is the central message loop. It receives and handles the default // Bubble Tea messages, update the model and triggers redraws. func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { @@ -287,8 +292,7 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { p.renderer.enableMouseAllMotion() case disableMouseMsg: - p.renderer.disableMouseCellMotion() - p.renderer.disableMouseAllMotion() + p.disableMouse() case showCursorMsg: p.renderer.showCursor() diff --git a/tty.go b/tty.go index 3ab6639b75..f731c88643 100644 --- a/tty.go +++ b/tty.go @@ -33,8 +33,7 @@ func (p *Program) initTerminal() error { func (p *Program) restoreTerminalState() error { if p.renderer != nil { p.renderer.showCursor() - p.renderer.disableMouseCellMotion() - p.renderer.disableMouseAllMotion() + p.disableMouse() if p.renderer.altScreen() { p.renderer.exitAltScreen() diff --git a/tutorials/go.sum b/tutorials/go.sum index 76e0059e1d..564479c7df 100644 --- a/tutorials/go.sum +++ b/tutorials/go.sum @@ -1,5 +1,5 @@ -github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= -github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E= +github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -18,8 +18,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= -github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/muesli/termenv v0.13.1-0.20221110215218-eb6391ce0665 h1:/pxOi1t70m6p/8AkJQjJBlZKB53hw5r5YLsLuCNmbM4= +github.com/muesli/termenv v0.13.1-0.20221110215218-eb6391ce0665/go.mod h1:er0g7V37a54K9xuhQub9EKVAu9xiELxV7TtyZss04mo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=