forked from Mirrors/bubbletea
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:
parent
2bcb0af2e2
commit
a154847611
|
@ -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
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
[?25lHi. This program will exit in 10 seconds. To quit sooner press any key
|
||||
[70D[1A[70D[2KHi. This program will exit in 9 seconds. To quit sooner press any key.
|
||||
[70D[2K[?25h[?1002l[?1003l
|
||||
[70D[2K[?25h[?1002l[?1003l[?1006l
|
23
key.go
23
key.go
|
@ -566,7 +566,7 @@ loop:
|
|||
canHaveMoreData := numBytes == len(buf)
|
||||
|
||||
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
|
||||
w, msg = detectOneMsg(b[i:], canHaveMoreData)
|
||||
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) {
|
||||
// Detect mouse events.
|
||||
const mouseEventLen = 6
|
||||
if len(b) >= mouseEventLen && b[0] == '\x1b' && b[1] == '[' && b[2] == 'M' {
|
||||
return mouseEventLen, MouseMsg(parseX10MouseEvent(b))
|
||||
// X10 mouse events have a length of 6 bytes
|
||||
const mouseEventX10Len = 6
|
||||
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,
|
||||
|
|
33
key_test.go
33
key_test.go
|
@ -137,7 +137,12 @@ func TestDetectOneMsg(t *testing.T) {
|
|||
// Mouse event.
|
||||
seqTest{
|
||||
[]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.
|
||||
seqTest{
|
||||
|
@ -316,27 +321,33 @@ func TestReadInput(t *testing.T) {
|
|||
[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
|
||||
[]Msg{
|
||||
MouseMsg{
|
||||
X: 32,
|
||||
Y: 16,
|
||||
Type: MouseWheelUp,
|
||||
X: 32,
|
||||
Y: 16,
|
||||
Type: MouseWheelUp,
|
||||
Button: MouseButtonWheelUp,
|
||||
Action: MouseActionPress,
|
||||
},
|
||||
},
|
||||
},
|
||||
{"left release",
|
||||
{"left motion release",
|
||||
[]byte{
|
||||
'\x1b', '[', 'M', byte(32) + 0b0010_0000, byte(32 + 33), byte(16 + 33),
|
||||
'\x1b', '[', 'M', byte(32) + 0b0000_0011, byte(64 + 33), byte(32 + 33),
|
||||
},
|
||||
[]Msg{
|
||||
MouseMsg(MouseEvent{
|
||||
X: 32,
|
||||
Y: 16,
|
||||
Type: MouseLeft,
|
||||
X: 32,
|
||||
Y: 16,
|
||||
Type: MouseLeft,
|
||||
Button: MouseButtonLeft,
|
||||
Action: MouseActionMotion,
|
||||
}),
|
||||
MouseMsg(MouseEvent{
|
||||
X: 64,
|
||||
Y: 32,
|
||||
Type: MouseRelease,
|
||||
X: 64,
|
||||
Y: 32,
|
||||
Type: MouseRelease,
|
||||
Button: MouseButtonNone,
|
||||
Action: MouseActionRelease,
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
|
290
mouse.go
290
mouse.go
|
@ -1,5 +1,7 @@
|
|||
package tea
|
||||
|
||||
import "strconv"
|
||||
|
||||
// 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 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
|
||||
// movement, a cursor movement, or a combination.
|
||||
type MouseEvent struct {
|
||||
X int
|
||||
Y int
|
||||
X int
|
||||
Y int
|
||||
Shift bool
|
||||
Alt bool
|
||||
Ctrl bool
|
||||
Action MouseAction
|
||||
Button MouseButton
|
||||
|
||||
// Deprecated: Use MouseAction & MouseButton instead.
|
||||
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.
|
||||
|
@ -28,38 +41,170 @@ func (m MouseEvent) String() (s string) {
|
|||
if m.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
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// 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",
|
||||
// Parse SGR-encoded mouse events; 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 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
|
||||
// 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:
|
||||
//
|
||||
|
@ -68,9 +213,22 @@ var mouseEventTypes = map[MouseEventType]string{
|
|||
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
|
||||
func parseX10MouseEvent(buf []byte) MouseEvent {
|
||||
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
|
||||
const byteOffset = 32
|
||||
e := v[0] - byteOffset
|
||||
e := b
|
||||
if !isSGR {
|
||||
e -= x10MouseByteOffset
|
||||
}
|
||||
|
||||
const (
|
||||
bitShift = 0b0000_0100
|
||||
|
@ -78,55 +236,73 @@ func parseX10MouseEvent(buf []byte) MouseEvent {
|
|||
bitCtrl = 0b0001_0000
|
||||
bitMotion = 0b0010_0000
|
||||
bitWheel = 0b0100_0000
|
||||
bitAdd = 0b1000_0000 // additional buttons 8-11
|
||||
|
||||
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
|
||||
}
|
||||
if e&bitAdd != 0 {
|
||||
m.Button = MouseButtonBackward + MouseButton(e&bitsMask)
|
||||
} else if e&bitWheel != 0 {
|
||||
m.Button = MouseButtonWheelUp + MouseButton(e&bitsMask)
|
||||
} 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.Button = MouseButtonLeft + MouseButton(e&bitsMask)
|
||||
// X10 reports a button release as 0b0000_0011 (3)
|
||||
if e&bitsMask == bitsMask {
|
||||
m.Action = MouseActionRelease
|
||||
m.Button = MouseButtonNone
|
||||
}
|
||||
}
|
||||
|
||||
if e&bitAlt != 0 {
|
||||
m.Alt = true
|
||||
}
|
||||
if e&bitCtrl != 0 {
|
||||
m.Ctrl = true
|
||||
// Motion bit doesn't get reported for wheel events.
|
||||
if e&bitMotion != 0 && !m.IsWheel() {
|
||||
m.Action = MouseActionMotion
|
||||
}
|
||||
|
||||
// (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
|
||||
// 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
|
||||
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
|
||||
}
|
||||
|
|
853
mouse_test.go
853
mouse_test.go
File diff suppressed because it is too large
Load Diff
|
@ -17,3 +17,5 @@ func (n nilRenderer) enableMouseCellMotion() {}
|
|||
func (n nilRenderer) disableMouseCellMotion() {}
|
||||
func (n nilRenderer) enableMouseAllMotion() {}
|
||||
func (n nilRenderer) disableMouseAllMotion() {}
|
||||
func (n nilRenderer) enableMouseSGRMode() {}
|
||||
func (n nilRenderer) disableMouseSGRMode() {}
|
||||
|
|
|
@ -108,6 +108,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.
|
||||
|
@ -127,6 +130,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.
|
||||
//
|
||||
|
|
12
renderer.go
12
renderer.go
|
@ -40,16 +40,22 @@ type renderer interface {
|
|||
// events if a mouse button is pressed (i.e., drag events).
|
||||
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.
|
||||
enableMouseAllMotion()
|
||||
|
||||
// DisableMouseAllMotion disables All Motion mouse tracking.
|
||||
// disableMouseAllMotion disables All Motion mouse tracking.
|
||||
disableMouseAllMotion()
|
||||
|
||||
// enableMouseSGRMode enables mouse extended mode (SGR).
|
||||
enableMouseSGRMode()
|
||||
|
||||
// disableMouseSGRMode disables mouse extended mode (SGR).
|
||||
disableMouseSGRMode()
|
||||
}
|
||||
|
||||
// repaintMsg forces a full repaint.
|
||||
|
|
|
@ -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[?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[?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[?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[?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[?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[?1003l\x1b[?1006lsuccess\r\n\x1b[0D\x1b[2K\x1b[?25h\x1b[?1002l\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[?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[?1003l\x1b[?1006l",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -396,6 +396,20 @@ func (r *standardRenderer) 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
|
||||
// renderer.
|
||||
func (r *standardRenderer) setIgnoredLines(from int, to int) {
|
||||
|
|
25
tea.go
25
tea.go
|
@ -301,6 +301,12 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
|
|||
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
|
||||
// Bubble Tea messages, update the model and triggers redraws.
|
||||
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:
|
||||
p.renderer.exitAltScreen()
|
||||
|
||||
case enableMouseCellMotionMsg:
|
||||
p.renderer.enableMouseCellMotion()
|
||||
|
||||
case enableMouseAllMotionMsg:
|
||||
p.renderer.enableMouseAllMotion()
|
||||
case enableMouseCellMotionMsg, enableMouseAllMotionMsg:
|
||||
switch msg.(type) {
|
||||
case enableMouseCellMotionMsg:
|
||||
p.renderer.enableMouseCellMotion()
|
||||
case enableMouseAllMotionMsg:
|
||||
p.renderer.enableMouseAllMotion()
|
||||
}
|
||||
// mouse mode (1006) is a no-op if the terminal doesn't support it.
|
||||
p.renderer.enableMouseSGRMode()
|
||||
|
||||
case disableMouseMsg:
|
||||
p.renderer.disableMouseCellMotion()
|
||||
p.renderer.disableMouseAllMotion()
|
||||
p.disableMouse()
|
||||
|
||||
case showCursorMsg:
|
||||
p.renderer.showCursor()
|
||||
|
@ -489,8 +498,10 @@ func (p *Program) Run() (Model, error) {
|
|||
}
|
||||
if p.startupOptions&withMouseCellMotion != 0 {
|
||||
p.renderer.enableMouseCellMotion()
|
||||
p.renderer.enableMouseSGRMode()
|
||||
} else if p.startupOptions&withMouseAllMotion != 0 {
|
||||
p.renderer.enableMouseAllMotion()
|
||||
p.renderer.enableMouseSGRMode()
|
||||
}
|
||||
|
||||
// Initialize the program.
|
||||
|
|
3
tty.go
3
tty.go
|
@ -35,8 +35,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()
|
||||
|
|
Loading…
Reference in New Issue