feat: extended Coordinates mouse reporting & additional buttons support (#594)

* feat(mouse): add extended mouse & shift key support

Support SGR(1006) mouse mode
Support parsing shift key press
Support additional mouse buttons
Report which button was released
Report button motion

* fix: key.go sgr len missing calculation (#841)

* chore(test): add sgr mouse msg detect test

---------

Co-authored-by: robinsamuel <96998379+robin-samuel@users.noreply.github.com>
This commit is contained in:
Ayman Bagabas 2023-12-04 08:50:59 -08:00 committed by GitHub
parent 2bcb0af2e2
commit a154847611
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1058 additions and 235 deletions

View File

@ -4,21 +4,19 @@ package main
// coordinates and events. // coordinates and events.
import ( import (
"fmt"
"log" "log"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
) )
func main() { func main() {
p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseAllMotion()) p := tea.NewProgram(model{}, tea.WithMouseAllMotion())
if _, err := p.Run(); err != nil { if _, err := p.Run(); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
type model struct { type model struct {
init bool
mouseEvent tea.MouseEvent mouseEvent tea.MouseEvent
} }
@ -34,20 +32,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
case tea.MouseMsg: case tea.MouseMsg:
m.init = true return m, tea.Printf("(X: %d, Y: %d) %s", msg.X, msg.Y, tea.MouseEvent(msg))
m.mouseEvent = tea.MouseEvent(msg)
} }
return m, nil return m, nil
} }
func (m model) View() string { func (m model) View() string {
s := "Do mouse stuff. When you're done press q to quit.\n\n" s := "Do mouse stuff. When you're done press q to quit.\n"
if m.init {
e := m.mouseEvent
s += fmt.Sprintf("(X: %d, Y: %d) %s", e.X, e.Y, e)
}
return s return s
} }

View File

@ -1,3 +1,3 @@
[?25lHi. This program will exit in 10 seconds. To quit sooner press any key [?25lHi. This program will exit in 10 seconds. To quit sooner press any key
Hi. This program will exit in 9 seconds. To quit sooner press any key. Hi. This program will exit in 9 seconds. To quit sooner press any key.
[?25h[?1002l[?1003l [?25h[?1002l[?1003l[?1006l

23
key.go
View File

@ -566,7 +566,7 @@ loop:
canHaveMoreData := numBytes == len(buf) canHaveMoreData := numBytes == len(buf)
var i, w int var i, w int
for i, w = 0, 0; i < len(b); i += w { for i, w = 0, 07; i < len(b); i += w {
var msg Msg var msg Msg
w, msg = detectOneMsg(b[i:], canHaveMoreData) w, msg = detectOneMsg(b[i:], canHaveMoreData)
if w == 0 { if w == 0 {
@ -591,13 +591,26 @@ loop:
} }
} }
var unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`) var (
unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`)
mouseSGRRegex = regexp.MustCompile(`(\d+);(\d+);(\d+)([Mm])`)
)
func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) { func detectOneMsg(b []byte, canHaveMoreData bool) (w int, msg Msg) {
// Detect mouse events. // Detect mouse events.
const mouseEventLen = 6 // X10 mouse events have a length of 6 bytes
if len(b) >= mouseEventLen && b[0] == '\x1b' && b[1] == '[' && b[2] == 'M' { const mouseEventX10Len = 6
return mouseEventLen, MouseMsg(parseX10MouseEvent(b)) if len(b) >= mouseEventX10Len && b[0] == '\x1b' && b[1] == '[' {
switch b[2] {
case 'M':
return mouseEventX10Len, MouseMsg(parseX10MouseEvent(b))
case '<':
if matchIndices := mouseSGRRegex.FindSubmatchIndex(b[3:]); matchIndices != nil {
// SGR mouse events length is the length of the match plus the length of the escape sequence
mouseEventSGRLen := matchIndices[1] + 3
return mouseEventSGRLen, MouseMsg(parseSGRMouseEvent(b))
}
}
} }
// Detect escape sequence and control characters other than NUL, // Detect escape sequence and control characters other than NUL,

View File

@ -137,7 +137,12 @@ func TestDetectOneMsg(t *testing.T) {
// Mouse event. // Mouse event.
seqTest{ seqTest{
[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
MouseMsg{X: 32, Y: 16, Type: MouseWheelUp}, MouseMsg{X: 32, Y: 16, Type: MouseWheelUp, Button: MouseButtonWheelUp, Action: MouseActionPress},
},
// SGR Mouse event.
seqTest{
[]byte("\x1b[<0;33;17M"),
MouseMsg{X: 32, Y: 16, Type: MouseLeft, Button: MouseButtonLeft, Action: MouseActionPress},
}, },
// Runes. // Runes.
seqTest{ seqTest{
@ -319,10 +324,12 @@ func TestReadInput(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseWheelUp, Type: MouseWheelUp,
Button: MouseButtonWheelUp,
Action: MouseActionPress,
}, },
}, },
}, },
{"left release", {"left motion release",
[]byte{ []byte{
'\x1b', '[', 'M', byte(32) + 0b0010_0000, byte(32 + 33), byte(16 + 33), '\x1b', '[', 'M', byte(32) + 0b0010_0000, byte(32 + 33), byte(16 + 33),
'\x1b', '[', 'M', byte(32) + 0b0000_0011, byte(64 + 33), byte(32 + 33), '\x1b', '[', 'M', byte(32) + 0b0000_0011, byte(64 + 33), byte(32 + 33),
@ -332,11 +339,15 @@ func TestReadInput(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseLeft, Type: MouseLeft,
Button: MouseButtonLeft,
Action: MouseActionMotion,
}), }),
MouseMsg(MouseEvent{ MouseMsg(MouseEvent{
X: 64, X: 64,
Y: 32, Y: 32,
Type: MouseRelease, Type: MouseRelease,
Button: MouseButtonNone,
Action: MouseActionRelease,
}), }),
}, },
}, },

282
mouse.go
View File

@ -1,5 +1,7 @@
package tea package tea
import "strconv"
// MouseMsg contains information about a mouse event and are sent to a programs // 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 // update function when mouse activity occurs. Note that the mouse must first
// be enabled in order for the mouse events to be received. // be enabled in order for the mouse events to be received.
@ -15,9 +17,20 @@ func (m MouseMsg) String() string {
type MouseEvent struct { type MouseEvent struct {
X int X int
Y int Y int
Type MouseEventType Shift bool
Alt bool Alt bool
Ctrl bool Ctrl bool
Action MouseAction
Button MouseButton
// Deprecated: Use MouseAction & MouseButton instead.
Type MouseEventType
}
// 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. // String returns a string representation of a mouse event.
@ -28,38 +41,170 @@ func (m MouseEvent) String() (s string) {
if m.Alt { if m.Alt {
s += "alt+" s += "alt+"
} }
s += mouseEventTypes[m.Type] if m.Shift {
s += "shift+"
}
if m.Button == MouseButtonNone {
if m.Action == MouseActionMotion || m.Action == MouseActionRelease {
s += mouseActions[m.Action]
} else {
s += "unknown"
}
} else if m.IsWheel() {
s += mouseButtons[m.Button]
} else {
btn := mouseButtons[m.Button]
if btn != "" {
s += btn
}
act := mouseActions[m.Action]
if act != "" {
s += " " + act
}
}
return s 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
)
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",
}
// MouseEventType indicates the type of mouse event occurring. // MouseEventType indicates the type of mouse event occurring.
//
// Deprecated: Use MouseAction & MouseButton instead.
type MouseEventType int type MouseEventType int
// Mouse event types. // Mouse event types.
//
// Deprecated: Use MouseAction & MouseButton instead.
const ( const (
MouseUnknown MouseEventType = iota MouseUnknown MouseEventType = iota
MouseLeft MouseLeft
MouseRight MouseRight
MouseMiddle MouseMiddle
MouseRelease MouseRelease // mouse button release (X10 only)
MouseWheelUp MouseWheelUp
MouseWheelDown MouseWheelDown
MouseWheelLeft
MouseWheelRight
MouseBackward
MouseForward
MouseMotion MouseMotion
) )
var mouseEventTypes = map[MouseEventType]string{ // Parse SGR-encoded mouse events; SGR extended mouse events. SGR mouse events
MouseUnknown: "unknown", // look like:
MouseLeft: "left", //
MouseRight: "right", // ESC [ < Cb ; Cx ; Cy (M or m)
MouseMiddle: "middle", //
MouseRelease: "release", // where:
MouseWheelUp: "wheel up", //
MouseWheelDown: "wheel down", // Cb is the encoded button code
MouseMotion: "motion", // 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 parseSGRMouseEvent(buf []byte) MouseEvent {
str := string(buf[3:])
matches := mouseSGRRegex.FindStringSubmatch(str)
if len(matches) != 5 {
// Unreachable, we already checked the regex in `detectOneMsg`.
panic("invalid 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
return m
} }
const x10MouseByteOffset = 32
// Parse X10-encoded mouse events; the simplest kind. The last release of X10 // 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 coordinates to 223 (=255-032).
// //
// X10 mouse events look like: // X10 mouse events look like:
// //
@ -68,9 +213,22 @@ var mouseEventTypes = map[MouseEventType]string{
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking // See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
func parseX10MouseEvent(buf []byte) MouseEvent { func parseX10MouseEvent(buf []byte) MouseEvent {
v := buf[3:6] v := buf[3:6]
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]) - x10MouseByteOffset - 1
m.Y = int(v[2]) - x10MouseByteOffset - 1
return m
}
// See: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Extended-coordinates
func parseMouseButton(b int, isSGR bool) MouseEvent {
var m MouseEvent var m MouseEvent
const byteOffset = 32 e := b
e := v[0] - byteOffset if !isSGR {
e -= x10MouseByteOffset
}
const ( const (
bitShift = 0b0000_0100 bitShift = 0b0000_0100
@ -78,55 +236,73 @@ func parseX10MouseEvent(buf []byte) MouseEvent {
bitCtrl = 0b0001_0000 bitCtrl = 0b0001_0000
bitMotion = 0b0010_0000 bitMotion = 0b0010_0000
bitWheel = 0b0100_0000 bitWheel = 0b0100_0000
bitAdd = 0b1000_0000 // additional buttons 8-11
bitsMask = 0b0000_0011 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 { if e&bitAdd != 0 {
// Check the low two bits. m.Button = MouseButtonBackward + MouseButton(e&bitsMask)
switch e & bitsMask { } else if e&bitWheel != 0 {
case bitsWheelUp: m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask)
m.Type = MouseWheelUp
case bitsWheelDown:
m.Type = MouseWheelDown
}
} else { } else {
// Check the low two bits. m.Button = MouseButtonLeft + MouseButton(e&bitsMask)
// We do not separate clicking and dragging. // X10 reports a button release as 0b0000_0011 (3)
switch e & bitsMask { if e&bitsMask == bitsMask {
case bitsLeft: m.Action = MouseActionRelease
m.Button = MouseButtonNone
}
}
// Motion bit doesn't get reported for wheel events.
if e&bitMotion != 0 && !m.IsWheel() {
m.Action = MouseActionMotion
}
// Modifiers
m.Alt = e&bitAlt != 0
m.Ctrl = e&bitCtrl != 0
m.Shift = e&bitShift != 0
// backward compatibility
switch {
case m.Button == MouseButtonLeft && m.Action == MouseActionPress:
m.Type = MouseLeft m.Type = MouseLeft
case bitsMiddle: case m.Button == MouseButtonMiddle && m.Action == MouseActionPress:
m.Type = MouseMiddle m.Type = MouseMiddle
case bitsRight: case m.Button == MouseButtonRight && m.Action == MouseActionPress:
m.Type = MouseRight m.Type = MouseRight
case bitsRelease: case m.Button == MouseButtonNone && m.Action == MouseActionRelease:
if e&bitMotion != 0 {
m.Type = MouseMotion
} else {
m.Type = MouseRelease 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
} }
}
if e&bitAlt != 0 {
m.Alt = true
}
if e&bitCtrl != 0 {
m.Ctrl = true
}
// (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
return m return m
} }

View File

@ -1,6 +1,9 @@
package tea package tea
import "testing" import (
"fmt"
"testing"
)
func TestMouseEvent_String(t *testing.T) { func TestMouseEvent_String(t *testing.T) {
tt := []struct { tt := []struct {
@ -10,68 +13,167 @@ func TestMouseEvent_String(t *testing.T) {
}{ }{
{ {
name: "unknown", name: "unknown",
event: MouseEvent{Type: MouseUnknown}, event: MouseEvent{
Action: MouseActionPress,
Button: MouseButtonNone,
Type: MouseUnknown,
},
expected: "unknown", expected: "unknown",
}, },
{ {
name: "left", name: "left",
event: MouseEvent{Type: MouseLeft}, event: MouseEvent{
expected: "left", Action: MouseActionPress,
Button: MouseButtonLeft,
Type: MouseLeft,
},
expected: "left press",
}, },
{ {
name: "right", name: "right",
event: MouseEvent{Type: MouseRight}, event: MouseEvent{
expected: "right", Action: MouseActionPress,
Button: MouseButtonRight,
Type: MouseRight,
},
expected: "right press",
}, },
{ {
name: "middle", name: "middle",
event: MouseEvent{Type: MouseMiddle}, event: MouseEvent{
expected: "middle", Action: MouseActionPress,
Button: MouseButtonMiddle,
Type: MouseMiddle,
},
expected: "middle press",
}, },
{ {
name: "release", name: "release",
event: MouseEvent{Type: MouseRelease}, event: MouseEvent{
Action: MouseActionRelease,
Button: MouseButtonNone,
Type: MouseRelease,
},
expected: "release", expected: "release",
}, },
{ {
name: "wheel up", name: "wheel up",
event: MouseEvent{Type: MouseWheelUp}, event: MouseEvent{
Action: MouseActionPress,
Button: MouseButtonWheelUp,
Type: MouseWheelUp,
},
expected: "wheel up", expected: "wheel up",
}, },
{ {
name: "wheel down", name: "wheel down",
event: MouseEvent{Type: MouseWheelDown}, event: MouseEvent{
Action: MouseActionPress,
Button: MouseButtonWheelDown,
Type: MouseWheelDown,
},
expected: "wheel down", expected: "wheel down",
}, },
{
name: "wheel left",
event: MouseEvent{
Action: MouseActionPress,
Button: MouseButtonWheelLeft,
Type: MouseWheelLeft,
},
expected: "wheel left",
},
{
name: "wheel right",
event: MouseEvent{
Action: MouseActionPress,
Button: MouseButtonWheelRight,
Type: MouseWheelRight,
},
expected: "wheel right",
},
{ {
name: "motion", name: "motion",
event: MouseEvent{Type: MouseMotion}, event: MouseEvent{
Action: MouseActionMotion,
Button: MouseButtonNone,
Type: MouseMotion,
},
expected: "motion", expected: "motion",
}, },
{
name: "shift+left release",
event: MouseEvent{
Type: MouseLeft,
Action: MouseActionRelease,
Button: MouseButtonLeft,
Shift: true,
},
expected: "shift+left release",
},
{
name: "shift+left",
event: MouseEvent{
Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
Shift: true,
},
expected: "shift+left press",
},
{
name: "ctrl+shift+left",
event: MouseEvent{
Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
Shift: true,
Ctrl: true,
},
expected: "ctrl+shift+left press",
},
{ {
name: "alt+left", name: "alt+left",
event: MouseEvent{ event: MouseEvent{
Type: MouseLeft, Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
Alt: true, Alt: true,
}, },
expected: "alt+left", expected: "alt+left press",
}, },
{ {
name: "ctrl+left", name: "ctrl+left",
event: MouseEvent{ event: MouseEvent{
Type: MouseLeft, Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
Ctrl: true, Ctrl: true,
}, },
expected: "ctrl+left", expected: "ctrl+left press",
}, },
{ {
name: "ctrl+alt+left", name: "ctrl+alt+left",
event: MouseEvent{ event: MouseEvent{
Type: MouseLeft, Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
Alt: true, Alt: true,
Ctrl: true, Ctrl: true,
}, },
expected: "ctrl+alt+left", expected: "ctrl+alt+left press",
},
{
name: "ctrl+alt+shift+left",
event: MouseEvent{
Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
Alt: true,
Ctrl: true,
Shift: true,
},
expected: "ctrl+alt+shift+left press",
}, },
{ {
name: "ignore coordinates", name: "ignore coordinates",
@ -79,13 +181,17 @@ func TestMouseEvent_String(t *testing.T) {
X: 100, X: 100,
Y: 200, Y: 200,
Type: MouseLeft, Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
}, },
expected: "left", expected: "left press",
}, },
{ {
name: "broken type", name: "broken type",
event: MouseEvent{ event: MouseEvent{
Type: MouseEventType(-1000), Type: MouseEventType(-100),
Action: MouseAction(-110),
Button: MouseButton(-120),
}, },
expected: "", expected: "",
}, },
@ -127,20 +233,24 @@ func TestParseX10MouseEvent(t *testing.T) {
// Position. // Position.
{ {
name: "zero position", name: "zero position",
buf: encode(0b0010_0000, 0, 0), buf: encode(0b0000_0000, 0, 0),
expected: MouseEvent{ expected: MouseEvent{
X: 0, X: 0,
Y: 0, Y: 0,
Type: MouseLeft, Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
}, },
}, },
{ {
name: "max position", 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{ expected: MouseEvent{
X: 222, X: 222,
Y: 222, Y: 222,
Type: MouseLeft, Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
}, },
}, },
// Simple. // Simple.
@ -151,6 +261,8 @@ func TestParseX10MouseEvent(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseLeft, Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
}, },
}, },
{ {
@ -160,6 +272,8 @@ func TestParseX10MouseEvent(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseLeft, Type: MouseLeft,
Action: MouseActionMotion,
Button: MouseButtonLeft,
}, },
}, },
{ {
@ -169,6 +283,8 @@ func TestParseX10MouseEvent(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseMiddle, Type: MouseMiddle,
Action: MouseActionPress,
Button: MouseButtonMiddle,
}, },
}, },
{ {
@ -178,6 +294,8 @@ func TestParseX10MouseEvent(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseMiddle, Type: MouseMiddle,
Action: MouseActionMotion,
Button: MouseButtonMiddle,
}, },
}, },
{ {
@ -187,6 +305,8 @@ func TestParseX10MouseEvent(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseRight, Type: MouseRight,
Action: MouseActionPress,
Button: MouseButtonRight,
}, },
}, },
{ {
@ -196,6 +316,8 @@ func TestParseX10MouseEvent(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseRight, Type: MouseRight,
Action: MouseActionMotion,
Button: MouseButtonRight,
}, },
}, },
{ {
@ -205,6 +327,8 @@ func TestParseX10MouseEvent(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseMotion, Type: MouseMotion,
Action: MouseActionMotion,
Button: MouseButtonNone,
}, },
}, },
{ {
@ -214,6 +338,8 @@ func TestParseX10MouseEvent(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseWheelUp, Type: MouseWheelUp,
Action: MouseActionPress,
Button: MouseButtonWheelUp,
}, },
}, },
{ {
@ -223,6 +349,30 @@ func TestParseX10MouseEvent(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseWheelDown, 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,
}, },
}, },
{ {
@ -232,38 +382,138 @@ func TestParseX10MouseEvent(t *testing.T) {
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseRelease, 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. // Combinations.
{ {
name: "alt+right", name: "alt+right",
buf: encode(0b0010_1010, 32, 16), buf: encode(0b0000_1010, 32, 16),
expected: MouseEvent{ expected: MouseEvent{
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseRight,
Alt: true, Alt: true,
Type: MouseRight,
Action: MouseActionPress,
Button: MouseButtonRight,
}, },
}, },
{ {
name: "ctrl+right", name: "ctrl+right",
buf: encode(0b0001_0010, 32, 16),
expected: MouseEvent{
X: 32,
Y: 16,
Ctrl: true,
Type: MouseRight,
Action: MouseActionPress,
Button: MouseButtonRight,
},
},
{
name: "left in motion",
buf: encode(0b0010_0000, 32, 16),
expected: MouseEvent{
X: 32,
Y: 16,
Alt: false,
Type: MouseLeft,
Action: MouseActionMotion,
Button: MouseButtonLeft,
},
},
{
name: "alt+right in motion",
buf: encode(0b0010_1010, 32, 16),
expected: MouseEvent{
X: 32,
Y: 16,
Alt: true,
Type: MouseRight,
Action: MouseActionMotion,
Button: MouseButtonRight,
},
},
{
name: "ctrl+right in motion",
buf: encode(0b0011_0010, 32, 16), buf: encode(0b0011_0010, 32, 16),
expected: MouseEvent{ expected: MouseEvent{
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseRight,
Ctrl: true, Ctrl: true,
Type: MouseRight,
Action: MouseActionMotion,
Button: MouseButtonRight,
}, },
}, },
{ {
name: "ctrl+alt+right", name: "ctrl+alt+right",
buf: encode(0b0011_1010, 32, 16), buf: encode(0b0001_1010, 32, 16),
expected: MouseEvent{ expected: MouseEvent{
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseRight,
Alt: true, Alt: true,
Ctrl: true, Ctrl: true,
Type: MouseRight,
Action: MouseActionPress,
Button: MouseButtonRight,
},
},
{
name: "ctrl+wheel up",
buf: encode(0b0101_0000, 32, 16),
expected: MouseEvent{
X: 32,
Y: 16,
Ctrl: true,
Type: MouseWheelUp,
Action: MouseActionPress,
Button: MouseButtonWheelUp,
}, },
}, },
{ {
@ -272,18 +522,10 @@ func TestParseX10MouseEvent(t *testing.T) {
expected: MouseEvent{ expected: MouseEvent{
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseWheelDown,
Alt: true, Alt: true,
},
},
{
name: "ctrl+wheel down",
buf: encode(0b0101_0001, 32, 16),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseWheelDown, Type: MouseWheelDown,
Ctrl: true, Action: MouseActionPress,
Button: MouseButtonWheelDown,
}, },
}, },
{ {
@ -292,29 +534,11 @@ func TestParseX10MouseEvent(t *testing.T) {
expected: MouseEvent{ expected: MouseEvent{
X: 32, X: 32,
Y: 16, Y: 16,
Type: MouseWheelDown,
Alt: true, Alt: true,
Ctrl: true, Ctrl: true,
}, Type: MouseWheelDown,
}, Action: MouseActionPress,
// Unknown. Button: MouseButtonWheelDown,
{
name: "wheel with unknown bit",
buf: encode(0b0100_0010, 32, 16),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseUnknown,
},
},
{
name: "unknown with modifier",
buf: encode(0b0100_1010, 32, 16),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseUnknown,
Alt: true,
}, },
}, },
// Overflow position. // Overflow position.
@ -325,6 +549,8 @@ func TestParseX10MouseEvent(t *testing.T) {
X: -6, X: -6,
Y: -33, Y: -33,
Type: MouseLeft, Type: MouseLeft,
Action: MouseActionMotion,
Button: MouseButtonLeft,
}, },
}, },
} }
@ -344,3 +570,370 @@ func TestParseX10MouseEvent(t *testing.T) {
}) })
} }
} }
// func TestParseX10MouseEvent_error(t *testing.T) {
// tt := []struct {
// name string
// buf []byte
// }{
// {
// name: "empty buf",
// buf: nil,
// },
// {
// name: "wrong high bit",
// buf: []byte("\x1a[M@A1"),
// },
// {
// name: "short buf",
// buf: []byte("\x1b[M@A"),
// },
// {
// name: "long buf",
// buf: []byte("\x1b[M@A11"),
// },
// }
//
// for i := range tt {
// tc := tt[i]
//
// t.Run(tc.name, func(t *testing.T) {
// _, err := parseX10MouseEvent(tc.buf)
//
// if err == nil {
// t.Fatalf("expected error but got nil")
// }
// })
// }
// }
func TestParseSGRMouseEvent(t *testing.T) {
encode := func(b, x, y int, r bool) []byte {
re := 'M'
if r {
re = 'm'
}
return []byte(fmt.Sprintf("\x1b[<%d;%d;%d%c", b, x+1, y+1, re))
}
tt := []struct {
name string
buf []byte
expected MouseEvent
}{
// Position.
{
name: "zero position",
buf: encode(0, 0, 0, false),
expected: MouseEvent{
X: 0,
Y: 0,
Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
},
},
{
name: "225 position",
buf: encode(0, 225, 225, false),
expected: MouseEvent{
X: 225,
Y: 225,
Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
},
},
// Simple.
{
name: "left",
buf: encode(0, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseLeft,
Action: MouseActionPress,
Button: MouseButtonLeft,
},
},
{
name: "left in motion",
buf: encode(32, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseLeft,
Action: MouseActionMotion,
Button: MouseButtonLeft,
},
},
{
name: "left release",
buf: encode(0, 32, 16, true),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseRelease,
Action: MouseActionRelease,
Button: MouseButtonLeft,
},
},
{
name: "middle",
buf: encode(1, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseMiddle,
Action: MouseActionPress,
Button: MouseButtonMiddle,
},
},
{
name: "middle in motion",
buf: encode(33, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseMiddle,
Action: MouseActionMotion,
Button: MouseButtonMiddle,
},
},
{
name: "middle release",
buf: encode(1, 32, 16, true),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseRelease,
Action: MouseActionRelease,
Button: MouseButtonMiddle,
},
},
{
name: "right",
buf: encode(2, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseRight,
Action: MouseActionPress,
Button: MouseButtonRight,
},
},
{
name: "right release",
buf: encode(2, 32, 16, true),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseRelease,
Action: MouseActionRelease,
Button: MouseButtonRight,
},
},
{
name: "motion",
buf: encode(35, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseMotion,
Action: MouseActionMotion,
Button: MouseButtonNone,
},
},
{
name: "wheel up",
buf: encode(64, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseWheelUp,
Action: MouseActionPress,
Button: MouseButtonWheelUp,
},
},
{
name: "wheel down",
buf: encode(65, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseWheelDown,
Action: MouseActionPress,
Button: MouseButtonWheelDown,
},
},
{
name: "wheel left",
buf: encode(66, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseWheelLeft,
Action: MouseActionPress,
Button: MouseButtonWheelLeft,
},
},
{
name: "wheel right",
buf: encode(67, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseWheelRight,
Action: MouseActionPress,
Button: MouseButtonWheelRight,
},
},
{
name: "backward",
buf: encode(128, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseBackward,
Action: MouseActionPress,
Button: MouseButtonBackward,
},
},
{
name: "backward in motion",
buf: encode(160, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseBackward,
Action: MouseActionMotion,
Button: MouseButtonBackward,
},
},
{
name: "forward",
buf: encode(129, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseForward,
Action: MouseActionPress,
Button: MouseButtonForward,
},
},
{
name: "forward in motion",
buf: encode(161, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseForward,
Action: MouseActionMotion,
Button: MouseButtonForward,
},
},
// Combinations.
{
name: "alt+right",
buf: encode(10, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Alt: true,
Type: MouseRight,
Action: MouseActionPress,
Button: MouseButtonRight,
},
},
{
name: "ctrl+right",
buf: encode(18, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Ctrl: true,
Type: MouseRight,
Action: MouseActionPress,
Button: MouseButtonRight,
},
},
{
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,
},
},
{
name: "alt+wheel press",
buf: encode(73, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Alt: true,
Type: MouseWheelDown,
Action: MouseActionPress,
Button: MouseButtonWheelDown,
},
},
{
name: "ctrl+wheel press",
buf: encode(81, 32, 16, false),
expected: MouseEvent{
X: 32,
Y: 16,
Ctrl: true,
Type: MouseWheelDown,
Action: MouseActionPress,
Button: MouseButtonWheelDown,
},
},
{
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,
},
},
{
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,
},
},
}
for i := range tt {
tc := tt[i]
t.Run(tc.name, func(t *testing.T) {
actual := parseSGRMouseEvent(tc.buf)
if tc.expected != actual {
t.Fatalf("expected %#v but got %#v",
tc.expected,
actual,
)
}
})
}
}

View File

@ -17,3 +17,5 @@ func (n nilRenderer) enableMouseCellMotion() {}
func (n nilRenderer) disableMouseCellMotion() {} func (n nilRenderer) disableMouseCellMotion() {}
func (n nilRenderer) enableMouseAllMotion() {} func (n nilRenderer) enableMouseAllMotion() {}
func (n nilRenderer) disableMouseAllMotion() {} func (n nilRenderer) disableMouseAllMotion() {}
func (n nilRenderer) enableMouseSGRMode() {}
func (n nilRenderer) disableMouseSGRMode() {}

View File

@ -108,6 +108,9 @@ func WithAltScreen() ProgramOption {
// movement events are also captured if a mouse button is pressed (i.e., drag // 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. // 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 // To enable mouse cell motion once the program has already started running use
// the EnableMouseCellMotion command. To disable the mouse when the program is // the EnableMouseCellMotion command. To disable the mouse when the program is
// running use the DisableMouse command. // running use the DisableMouse command.
@ -127,6 +130,9 @@ func WithMouseCellMotion() ProgramOption {
// wheel, and motion events, which are delivered regardless of whether a mouse // wheel, and motion events, which are delivered regardless of whether a mouse
// button is pressed, effectively enabling support for hover interactions. // 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 // Many modern terminals support this, but not all. If in doubt, use
// EnableMouseCellMotion instead. // EnableMouseCellMotion instead.
// //

View File

@ -40,16 +40,22 @@ type renderer interface {
// events if a mouse button is pressed (i.e., drag events). // events if a mouse button is pressed (i.e., drag events).
enableMouseCellMotion() enableMouseCellMotion()
// DisableMouseCellMotion disables Mouse Cell Motion tracking. // disableMouseCellMotion disables Mouse Cell Motion tracking.
disableMouseCellMotion() 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 // events, regardless of whether a mouse button is pressed. Many modern
// terminals support this, but not all. // terminals support this, but not all.
enableMouseAllMotion() enableMouseAllMotion()
// DisableMouseAllMotion disables All Motion mouse tracking. // disableMouseAllMotion disables All Motion mouse tracking.
disableMouseAllMotion() disableMouseAllMotion()
// enableMouseSGRMode enables mouse extended mode (SGR).
enableMouseSGRMode()
// disableMouseSGRMode disables mouse extended mode (SGR).
disableMouseSGRMode()
} }
// repaintMsg forces a full repaint. // repaintMsg forces a full repaint.

View File

@ -14,42 +14,42 @@ func TestClearMsg(t *testing.T) {
{ {
name: "clear_screen", name: "clear_screen",
cmds: []Cmd{ClearScreen}, 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[?1003l\x1b[?1006l",
}, },
{ {
name: "altscreen", name: "altscreen",
cmds: []Cmd{EnterAltScreen, ExitAltScreen}, 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[?1003l\x1b[?1006l",
}, },
{ {
name: "altscreen_autoexit", name: "altscreen_autoexit",
cmds: []Cmd{EnterAltScreen}, 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[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h",
}, },
{ {
name: "mouse_cellmotion", name: "mouse_cellmotion",
cmds: []Cmd{EnableMouseCellMotion}, 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[?1003l\x1b[?1006l",
}, },
{ {
name: "mouse_allmotion", name: "mouse_allmotion",
cmds: []Cmd{EnableMouseAllMotion}, 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[?1003l\x1b[?1006l",
}, },
{ {
name: "mouse_disable", name: "mouse_disable",
cmds: []Cmd{EnableMouseAllMotion, DisableMouse}, 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[?1003l\x1b[?1006lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
}, },
{ {
name: "cursor_hide", name: "cursor_hide",
cmds: []Cmd{HideCursor}, 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[?1003l\x1b[?1006l",
}, },
{ {
name: "cursor_hideshow", name: "cursor_hideshow",
cmds: []Cmd{HideCursor, ShowCursor}, 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[?1003l\x1b[?1006l",
}, },
} }

View File

@ -396,6 +396,20 @@ func (r *standardRenderer) disableMouseAllMotion() {
r.out.DisableMouseAllMotion() r.out.DisableMouseAllMotion()
} }
func (r *standardRenderer) enableMouseSGRMode() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.out.EnableMouseExtendedMode()
}
func (r *standardRenderer) disableMouseSGRMode() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.out.DisableMouseExtendedMode()
}
// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea // setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
// renderer. // renderer.
func (r *standardRenderer) setIgnoredLines(from int, to int) { func (r *standardRenderer) setIgnoredLines(from int, to int) {

17
tea.go
View File

@ -301,6 +301,12 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
return ch return ch
} }
func (p *Program) disableMouse() {
p.renderer.disableMouseCellMotion()
p.renderer.disableMouseAllMotion()
p.renderer.disableMouseSGRMode()
}
// eventLoop is the central message loop. It receives and handles the default // eventLoop is the central message loop. It receives and handles the default
// Bubble Tea messages, update the model and triggers redraws. // Bubble Tea messages, update the model and triggers redraws.
func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
@ -335,15 +341,18 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
case exitAltScreenMsg: case exitAltScreenMsg:
p.renderer.exitAltScreen() p.renderer.exitAltScreen()
case enableMouseCellMotionMsg, enableMouseAllMotionMsg:
switch msg.(type) {
case enableMouseCellMotionMsg: case enableMouseCellMotionMsg:
p.renderer.enableMouseCellMotion() p.renderer.enableMouseCellMotion()
case enableMouseAllMotionMsg: case enableMouseAllMotionMsg:
p.renderer.enableMouseAllMotion() p.renderer.enableMouseAllMotion()
}
// mouse mode (1006) is a no-op if the terminal doesn't support it.
p.renderer.enableMouseSGRMode()
case disableMouseMsg: case disableMouseMsg:
p.renderer.disableMouseCellMotion() p.disableMouse()
p.renderer.disableMouseAllMotion()
case showCursorMsg: case showCursorMsg:
p.renderer.showCursor() p.renderer.showCursor()
@ -489,8 +498,10 @@ func (p *Program) Run() (Model, error) {
} }
if p.startupOptions&withMouseCellMotion != 0 { if p.startupOptions&withMouseCellMotion != 0 {
p.renderer.enableMouseCellMotion() p.renderer.enableMouseCellMotion()
p.renderer.enableMouseSGRMode()
} else if p.startupOptions&withMouseAllMotion != 0 { } else if p.startupOptions&withMouseAllMotion != 0 {
p.renderer.enableMouseAllMotion() p.renderer.enableMouseAllMotion()
p.renderer.enableMouseSGRMode()
} }
// Initialize the program. // Initialize the program.

3
tty.go
View File

@ -35,8 +35,7 @@ func (p *Program) initTerminal() error {
func (p *Program) restoreTerminalState() error { func (p *Program) restoreTerminalState() error {
if p.renderer != nil { if p.renderer != nil {
p.renderer.showCursor() p.renderer.showCursor()
p.renderer.disableMouseCellMotion() p.disableMouse()
p.renderer.disableMouseAllMotion()
if p.renderer.altScreen() { if p.renderer.altScreen() {
p.renderer.exitAltScreen() p.renderer.exitAltScreen()