Support receiving batched mouse events

Mouse events may trigger more than a single events simultaneously.

Fixes #212.
This commit is contained in:
Christian Muehlhaeuser 2022-02-03 04:14:49 +01:00
parent db177f1939
commit 6301f93cb2
5 changed files with 280 additions and 180 deletions

38
key.go
View File

@ -292,9 +292,9 @@ var hexes = map[string]Key{
"1b4f44": {Type: KeyLeft, Alt: false}, "1b4f44": {Type: KeyLeft, Alt: false},
} }
// readInput reads keypress and mouse input from a TTY and returns a message // readInputs reads keypress and mouse inputs from a TTY and returns messages
// containing information about the key or mouse event accordingly. // containing information about the key or mouse events accordingly.
func readInput(input io.Reader) (Msg, error) { func readInputs(input io.Reader) ([]Msg, error) {
var buf [256]byte var buf [256]byte
// Read and block // Read and block
@ -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 // See if it's a mouse event. For now we're parsing X10-type mouse events
// only. // only.
mouseEvent, err := parseX10MouseEvent(buf[:numBytes]) mouseEvent, err := parseX10MouseEvents(buf[:numBytes])
if err == nil { 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? // Is it a special sequence, like an arrow key?
if k, ok := sequences[string(buf[:numBytes])]; ok { 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 // Some of these need special handling
hex := fmt.Sprintf("%x", buf[:numBytes]) hex := fmt.Sprintf("%x", buf[:numBytes])
if k, ok := hexes[hex]; ok { 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 // Is the alt key pressed? The buffer will be prefixed with an escape
@ -330,7 +338,9 @@ func readInput(input io.Reader) (Msg, error) {
if c == utf8.RuneError { if c == utf8.RuneError {
return nil, errors.New("could not decode rune after removing initial escape") 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 var runes []rune
@ -353,15 +363,21 @@ func readInput(input io.Reader) (Msg, error) {
} else if len(runes) > 1 { } else if len(runes) > 1 {
// We received multiple runes, so we know this isn't a control // We received multiple runes, so we know this isn't a control
// character, sequence, and so on. // 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? // Is the first rune a control character?
r := KeyType(runes[0]) r := KeyType(runes[0])
if numBytes == 1 && r <= keyUS || r == keyDEL { 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 // 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
} }

View File

@ -58,14 +58,18 @@ func TestReadInput(t *testing.T) {
"shift+tab": {'\x1b', '[', 'Z'}, "shift+tab": {'\x1b', '[', 'Z'},
} { } {
t.Run(out, func(t *testing.T) { t.Run(out, func(t *testing.T) {
msg, err := readInput(bytes.NewReader(in)) msgs, err := readInputs(bytes.NewReader(in))
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) 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) 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]) t.Fatalf(`expected a mousemsg %q, got %q`, out, mouseEventTypes[m.Type])
} }
}) })

141
mouse.go
View File

@ -1,6 +1,9 @@
package tea package tea
import "errors" import (
"bytes"
"errors"
)
// 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
@ -55,78 +58,92 @@ var mouseEventTypes = map[MouseEventType]string{
MouseMotion: "motion", MouseMotion: "motion",
} }
// Parse an X10-encoded mouse event; the simplest kind. The last release of // Parse X10-encoded mouse events; the simplest kind. The last release of X10
// X10 was December 1986, by the way. // was December 1986, by the way.
// //
// X10 mouse events look like: // X10 mouse events look like:
// //
// ESC [M Cb Cx Cy // ESC [M Cb Cx Cy
// //
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking // See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
func parseX10MouseEvent(buf []byte) (m MouseEvent, err error) { func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) {
if len(buf) != 6 || string(buf[:3]) != "\x1b[M" { var r []MouseEvent
return m, errors.New("not an X10 mouse event")
seq := []byte("\x1b[M")
if !bytes.Contains(buf, seq) {
return r, errors.New("not an X10 mouse event")
} }
const byteOffset = 32 for _, v := range bytes.Split(buf, seq) {
if len(v) == 0 {
e := buf[3] - byteOffset continue
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 { if len(v) != 3 {
// Check the low two bits. return r, errors.New("not an X10 mouse event")
// We do not separate clicking and dragging. }
switch e & bitsMask {
case bitsLeft: var m MouseEvent
m.Type = MouseLeft const byteOffset = 32
case bitsMiddle: e := v[0] - byteOffset
m.Type = MouseMiddle
case bitsRight: const (
m.Type = MouseRight bitShift = 0b0000_0100
case bitsRelease: bitAlt = 0b0000_1000
if e&bitMotion != 0 { bitCtrl = 0b0001_0000
m.Type = MouseMotion bitMotion = 0b0010_0000
} else { bitWheel = 0b0100_0000
m.Type = MouseRelease
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&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
r = append(r, m)
} }
if e&bitAlt != 0 { return r, nil
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(buf[4]) - byteOffset - 1
m.Y = int(buf[5]) - byteOffset - 1
return m, nil
} }

View File

@ -122,209 +122,268 @@ func TestParseX10MouseEvent(t *testing.T) {
tt := []struct { tt := []struct {
name string name string
buf []byte buf []byte
expected MouseEvent expected []MouseEvent
}{ }{
// Position. // Position.
{ {
name: "zero position", name: "zero position",
buf: encode(0b0010_0000, 0, 0), buf: encode(0b0010_0000, 0, 0),
expected: MouseEvent{ expected: []MouseEvent{
X: 0, {
Y: 0, X: 0,
Type: MouseLeft, Y: 0,
Type: MouseLeft,
},
}, },
}, },
{ {
name: "max position", name: "max position",
buf: encode(0b0010_0000, 222, 222), // Because 255 (max int8) - 32 - 1. buf: encode(0b0010_0000, 222, 222), // Because 255 (max int8) - 32 - 1.
expected: MouseEvent{ expected: []MouseEvent{
X: 222, {
Y: 222, X: 222,
Type: MouseLeft, Y: 222,
Type: MouseLeft,
},
}, },
}, },
// Simple. // Simple.
{ {
name: "left", name: "left",
buf: encode(0b0000_0000, 32, 16), buf: encode(0b0000_0000, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseLeft, Y: 16,
Type: MouseLeft,
},
}, },
}, },
{ {
name: "left in motion", name: "left in motion",
buf: encode(0b0010_0000, 32, 16), buf: encode(0b0010_0000, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseLeft, Y: 16,
Type: MouseLeft,
},
}, },
}, },
{ {
name: "middle", name: "middle",
buf: encode(0b0000_0001, 32, 16), buf: encode(0b0000_0001, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseMiddle, Y: 16,
Type: MouseMiddle,
},
}, },
}, },
{ {
name: "middle in motion", name: "middle in motion",
buf: encode(0b0010_0001, 32, 16), buf: encode(0b0010_0001, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseMiddle, Y: 16,
Type: MouseMiddle,
},
}, },
}, },
{ {
name: "right", name: "right",
buf: encode(0b0000_0010, 32, 16), buf: encode(0b0000_0010, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseRight, Y: 16,
Type: MouseRight,
},
}, },
}, },
{ {
name: "right in motion", name: "right in motion",
buf: encode(0b0010_0010, 32, 16), buf: encode(0b0010_0010, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseRight, Y: 16,
Type: MouseRight,
},
}, },
}, },
{ {
name: "motion", name: "motion",
buf: encode(0b0010_0011, 32, 16), buf: encode(0b0010_0011, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseMotion, Y: 16,
Type: MouseMotion,
},
}, },
}, },
{ {
name: "wheel up", name: "wheel up",
buf: encode(0b0100_0000, 32, 16), buf: encode(0b0100_0000, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseWheelUp, Y: 16,
Type: MouseWheelUp,
},
}, },
}, },
{ {
name: "wheel down", name: "wheel down",
buf: encode(0b0100_0001, 32, 16), buf: encode(0b0100_0001, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseWheelDown, Y: 16,
Type: MouseWheelDown,
},
}, },
}, },
{ {
name: "release", name: "release",
buf: encode(0b0000_0011, 32, 16), buf: encode(0b0000_0011, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseRelease, Y: 16,
Type: MouseRelease,
},
}, },
}, },
// Combinations. // Combinations.
{ {
name: "alt+right", name: "alt+right",
buf: encode(0b0010_1010, 32, 16), buf: encode(0b0010_1010, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseRight, Y: 16,
Alt: true, Type: MouseRight,
Alt: true,
},
}, },
}, },
{ {
name: "ctrl+right", name: "ctrl+right",
buf: encode(0b0011_0010, 32, 16), buf: encode(0b0011_0010, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseRight, Y: 16,
Ctrl: true, Type: MouseRight,
Ctrl: true,
},
}, },
}, },
{ {
name: "ctrl+alt+right", name: "ctrl+alt+right",
buf: encode(0b0011_1010, 32, 16), buf: encode(0b0011_1010, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseRight, Y: 16,
Alt: true, Type: MouseRight,
Ctrl: true, Alt: true,
Ctrl: true,
},
}, },
}, },
{ {
name: "alt+wheel down", name: "alt+wheel down",
buf: encode(0b0100_1001, 32, 16), buf: encode(0b0100_1001, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseWheelDown, Y: 16,
Alt: true, Type: MouseWheelDown,
Alt: true,
},
}, },
}, },
{ {
name: "ctrl+wheel down", name: "ctrl+wheel down",
buf: encode(0b0101_0001, 32, 16), buf: encode(0b0101_0001, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseWheelDown, Y: 16,
Ctrl: true, Type: MouseWheelDown,
Ctrl: true,
},
}, },
}, },
{ {
name: "ctrl+alt+wheel down", name: "ctrl+alt+wheel down",
buf: encode(0b0101_1001, 32, 16), buf: encode(0b0101_1001, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseWheelDown, Y: 16,
Alt: true, Type: MouseWheelDown,
Ctrl: true, Alt: true,
Ctrl: true,
},
}, },
}, },
// Unknown. // Unknown.
{ {
name: "wheel with unknown bit", name: "wheel with unknown bit",
buf: encode(0b0100_0010, 32, 16), buf: encode(0b0100_0010, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseUnknown, Y: 16,
Type: MouseUnknown,
},
}, },
}, },
{ {
name: "unknown with modifier", name: "unknown with modifier",
buf: encode(0b0100_1010, 32, 16), buf: encode(0b0100_1010, 32, 16),
expected: MouseEvent{ expected: []MouseEvent{
X: 32, {
Y: 16, X: 32,
Type: MouseUnknown, Y: 16,
Alt: true, Type: MouseUnknown,
Alt: true,
},
}, },
}, },
// Overflow position. // Overflow position.
{ {
name: "overflow position", name: "overflow position",
buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1. buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1.
expected: MouseEvent{ expected: []MouseEvent{
X: -6, {
Y: -33, X: -6,
Type: MouseLeft, Y: -33,
Type: MouseLeft,
},
},
},
// Batched events.
{
name: "batched events",
buf: append(encode(0b0010_0000, 32, 16), encode(0b0000_0011, 64, 32)...),
expected: []MouseEvent{
{
X: 32,
Y: 16,
Type: MouseLeft,
},
{
X: 64,
Y: 32,
Type: MouseRelease,
},
}, },
}, },
} }
@ -333,18 +392,20 @@ func TestParseX10MouseEvent(t *testing.T) {
tc := tt[i] tc := tt[i]
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actual, err := parseX10MouseEvent(tc.buf) actual, err := parseX10MouseEvents(tc.buf)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", t.Fatalf("unexpected error for test: %v",
err, err,
) )
} }
if tc.expected != actual { for i := range tc.expected {
t.Fatalf("expected %#v but got %#v", if tc.expected[i] != actual[i] {
tc.expected, t.Fatalf("expected %#v but got %#v",
actual, tc.expected[i],
) actual[i],
)
}
} }
}) })
} }
@ -377,7 +438,7 @@ func TestParseX10MouseEvent_error(t *testing.T) {
tc := tt[i] tc := tt[i]
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
_, err := parseX10MouseEvent(tc.buf) _, err := parseX10MouseEvents(tc.buf)
if err == nil { if err == nil {
t.Fatalf("expected error but got nil") t.Fatalf("expected error but got nil")

6
tea.go
View File

@ -421,7 +421,7 @@ func (p *Program) StartReturningModel() (Model, error) {
return return
} }
msg, err := readInput(cancelReader) msgs, err := readInputs(cancelReader)
if err != nil { if err != nil {
if !errors.Is(err, io.EOF) && !errors.Is(err, errCanceled) { if !errors.Is(err, io.EOF) && !errors.Is(err, errCanceled) {
errs <- err errs <- err
@ -430,7 +430,9 @@ func (p *Program) StartReturningModel() (Model, error) {
return return
} }
p.msgs <- msg for _, msg := range msgs {
p.msgs <- msg
}
} }
}() }()
} else { } else {