Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support receiving batched mouse events #215

Merged
merged 1 commit into from Feb 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
38 changes: 27 additions & 11 deletions key.go
Expand Up @@ -292,9 +292,9 @@ var hexes = map[string]Key{
"1b4f44": {Type: KeyLeft, Alt: false},
}

// readInput reads keypress and mouse input from a TTY and returns a message
// containing information about the key or mouse event accordingly.
func readInput(input io.Reader) (Msg, error) {
// readInputs reads keypress and mouse inputs from a TTY and returns messages
// containing information about the key or mouse events accordingly.
func readInputs(input io.Reader) ([]Msg, error) {
var buf [256]byte

// Read and block
Expand All @@ -305,20 +305,28 @@ func readInput(input io.Reader) (Msg, error) {

// See if it's a mouse event. For now we're parsing X10-type mouse events
// only.
mouseEvent, err := parseX10MouseEvent(buf[:numBytes])
mouseEvent, err := parseX10MouseEvents(buf[:numBytes])
if err == nil {
return MouseMsg(mouseEvent), nil
var m []Msg
for _, v := range mouseEvent {
m = append(m, MouseMsg(v))
}
return m, nil
}

// Is it a special sequence, like an arrow key?
if k, ok := sequences[string(buf[:numBytes])]; ok {
return KeyMsg(Key{Type: k}), nil
return []Msg{
KeyMsg(Key{Type: k}),
}, nil
}

// Some of these need special handling
hex := fmt.Sprintf("%x", buf[:numBytes])
if k, ok := hexes[hex]; ok {
return KeyMsg(k), nil
return []Msg{
KeyMsg(k),
}, nil
}

// Is the alt key pressed? The buffer will be prefixed with an escape
Expand All @@ -330,7 +338,9 @@ func readInput(input io.Reader) (Msg, error) {
if c == utf8.RuneError {
return nil, errors.New("could not decode rune after removing initial escape")
}
return KeyMsg(Key{Alt: true, Type: KeyRunes, Runes: []rune{c}}), nil
return []Msg{
KeyMsg(Key{Alt: true, Type: KeyRunes, Runes: []rune{c}}),
}, nil
}

var runes []rune
Expand All @@ -353,15 +363,21 @@ func readInput(input io.Reader) (Msg, error) {
} else if len(runes) > 1 {
// We received multiple runes, so we know this isn't a control
// character, sequence, and so on.
return KeyMsg(Key{Type: KeyRunes, Runes: runes}), nil
return []Msg{
KeyMsg(Key{Type: KeyRunes, Runes: runes}),
}, nil
}

// Is the first rune a control character?
r := KeyType(runes[0])
if numBytes == 1 && r <= keyUS || r == keyDEL {
return KeyMsg(Key{Type: r}), nil
return []Msg{
KeyMsg(Key{Type: r}),
}, nil
}

// Welp, it's just a regular, ol' single rune
return KeyMsg(Key{Type: KeyRunes, Runes: runes}), nil
return []Msg{
KeyMsg(Key{Type: KeyRunes, Runes: runes}),
}, nil
}
10 changes: 7 additions & 3 deletions key_test.go
Expand Up @@ -58,14 +58,18 @@ func TestReadInput(t *testing.T) {
"shift+tab": {'\x1b', '[', 'Z'},
} {
t.Run(out, func(t *testing.T) {
msg, err := readInput(bytes.NewReader(in))
msgs, err := readInputs(bytes.NewReader(in))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if m, ok := msg.(KeyMsg); ok && m.String() != out {
if len(msgs) == 0 {
t.Fatalf("unexpected empty message list")
}

if m, ok := msgs[0].(KeyMsg); ok && m.String() != out {
t.Fatalf(`expected a keymsg %q, got %q`, out, m)
}
if m, ok := msg.(MouseMsg); ok && mouseEventTypes[m.Type] != out {
if m, ok := msgs[0].(MouseMsg); ok && mouseEventTypes[m.Type] != out {
t.Fatalf(`expected a mousemsg %q, got %q`, out, mouseEventTypes[m.Type])
}
})
Expand Down
137 changes: 77 additions & 60 deletions mouse.go
@@ -1,6 +1,9 @@
package tea

import "errors"
import (
"bytes"
"errors"
)

// 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
Expand Down Expand Up @@ -55,78 +58,92 @@ var mouseEventTypes = map[MouseEventType]string{
MouseMotion: "motion",
}

// Parse an X10-encoded mouse event; the simplest kind. The last release of
// X10 was December 1986, by the way.
// Parse X10-encoded mouse events; the simplest kind. The last release of X10
// was December 1986, by the way.
//
// X10 mouse events look like:
//
// ESC [M Cb Cx Cy
//
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
func parseX10MouseEvent(buf []byte) (m MouseEvent, err error) {
if len(buf) != 6 || string(buf[:3]) != "\x1b[M" {
return m, errors.New("not an X10 mouse event")
}

const byteOffset = 32

e := buf[3] - byteOffset
func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) {
var r []MouseEvent

const (
bitShift = 0b0000_0100
bitAlt = 0b0000_1000
bitCtrl = 0b0001_0000
bitMotion = 0b0010_0000
bitWheel = 0b0100_0000

bitsMask = 0b0000_0011
seq := []byte("\x1b[M")
if !bytes.Contains(buf, seq) {
return r, errors.New("not an X10 mouse event")
}

bitsLeft = 0b0000_0000
bitsMiddle = 0b0000_0001
bitsRight = 0b0000_0010
bitsRelease = 0b0000_0011
for _, v := range bytes.Split(buf, seq) {
if len(v) == 0 {
continue
}
if len(v) != 3 {
return r, errors.New("not an X10 mouse event")
}

bitsWheelUp = 0b0000_0000
bitsWheelDown = 0b0000_0001
)
var m MouseEvent
const byteOffset = 32
e := v[0] - byteOffset

const (
bitShift = 0b0000_0100
bitAlt = 0b0000_1000
bitCtrl = 0b0001_0000
bitMotion = 0b0010_0000
bitWheel = 0b0100_0000

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
}
} 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
}
}
}

if e&bitWheel != 0 {
// Check the low two bits.
switch e & bitsMask {
case bitsWheelUp:
m.Type = MouseWheelUp
case bitsWheelDown:
m.Type = MouseWheelDown
if e&bitAlt != 0 {
m.Alt = true
}
} 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
}
if e&bitCtrl != 0 {
m.Ctrl = true
}
}

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

// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
m.X = int(buf[4]) - byteOffset - 1
m.Y = int(buf[5]) - byteOffset - 1
r = append(r, m)
}

return m, nil
return r, nil
}