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{
@ -316,27 +321,33 @@ func TestReadInput(t *testing.T) {
[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
[]Msg{ []Msg{
MouseMsg{ MouseMsg{
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),
}, },
[]Msg{ []Msg{
MouseMsg(MouseEvent{ MouseMsg(MouseEvent{
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,
}), }),
}, },
}, },

290
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.
@ -13,11 +15,22 @@ func (m MouseMsg) String() string {
// MouseEvent represents a mouse event, which could be a click, a scroll wheel // MouseEvent represents a mouse event, which could be a click, a scroll wheel
// movement, a cursor movement, or a combination. // movement, a cursor movement, or a combination.
type MouseEvent struct { type MouseEvent struct {
X int X int
Y int Y int
Shift bool
Alt bool
Ctrl bool
Action MouseAction
Button MouseButton
// Deprecated: Use MouseAction & MouseButton instead.
Type MouseEventType Type MouseEventType
Alt bool }
Ctrl 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. // 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.Type = MouseLeft m.Button = MouseButtonNone
case bitsMiddle:
m.Type = MouseMiddle
case bitsRight:
m.Type = MouseRight
case bitsRelease:
if e&bitMotion != 0 {
m.Type = MouseMotion
} else {
m.Type = MouseRelease
}
} }
} }
if e&bitAlt != 0 { // Motion bit doesn't get reported for wheel events.
m.Alt = true if e&bitMotion != 0 && !m.IsWheel() {
} m.Action = MouseActionMotion
if e&bitCtrl != 0 {
m.Ctrl = true
} }
// (1,1) is the upper left. We subtract 1 to normalize it to (0,0). // Modifiers
m.X = int(v[1]) - byteOffset - 1 m.Alt = e&bitAlt != 0
m.Y = int(v[2]) - byteOffset - 1 m.Ctrl = e&bitCtrl != 0
m.Shift = e&bitShift != 0
// 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
}
return m return m
} }

File diff suppressed because it is too large Load Diff

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) {

25
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: case enableMouseCellMotionMsg, enableMouseAllMotionMsg:
p.renderer.enableMouseCellMotion() switch msg.(type) {
case enableMouseCellMotionMsg:
case enableMouseAllMotionMsg: p.renderer.enableMouseCellMotion()
p.renderer.enableMouseAllMotion() case enableMouseAllMotionMsg:
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()