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.
|
// 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
[70D[1A[70D[2KHi. This program will exit in 9 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)
|
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,
|
||||||
|
|
33
key_test.go
33
key_test.go
|
@ -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
290
mouse.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
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) 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() {}
|
||||||
|
|
|
@ -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.
|
||||||
//
|
//
|
||||||
|
|
12
renderer.go
12
renderer.go
|
@ -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.
|
||||||
|
|
|
@ -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",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
25
tea.go
|
@ -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
3
tty.go
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue