Skip to content

Commit

Permalink
feat: extended Coordinates mouse reporting & additional buttons suppo…
Browse files Browse the repository at this point in the history
…rt (#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>
  • Loading branch information
aymanbagabas and robin-samuel committed Dec 4, 2023
1 parent 2bcb0af commit a154847
Show file tree
Hide file tree
Showing 13 changed files with 1,052 additions and 229 deletions.
14 changes: 3 additions & 11 deletions examples/mouse/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
}
2 changes: 1 addition & 1 deletion examples/simple/testdata/TestApp.golden
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[?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.
[?25h[?1002l[?1003l
[?25h[?1002l[?1003l[?1006l
23 changes: 18 additions & 5 deletions key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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

Check failure on line 610 in key.go

View workflow job for this annotation

GitHub Actions / lint-soft

mnd: Magic number: 3, in <operation> detected (gomnd)
return mouseEventSGRLen, MouseMsg(parseSGRMouseEvent(b))
}
}
}

// Detect escape sequence and control characters other than NUL,
Expand Down
33 changes: 22 additions & 11 deletions key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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,
}),
},
},
Expand Down

0 comments on commit a154847

Please sign in to comment.