From 6301f93cb2331ab9510fb67952e426999dbc3c39 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Thu, 3 Feb 2022 04:14:49 +0100 Subject: [PATCH] Support receiving batched mouse events Mouse events may trigger more than a single events simultaneously. Fixes #212. --- key.go | 38 +++++--- key_test.go | 10 +- mouse.go | 141 +++++++++++++++------------ mouse_test.go | 265 +++++++++++++++++++++++++++++++------------------- tea.go | 6 +- 5 files changed, 280 insertions(+), 180 deletions(-) diff --git a/key.go b/key.go index ae4bf1a..9714444 100644 --- a/key.go +++ b/key.go @@ -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 @@ -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 @@ -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 @@ -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 } diff --git a/key_test.go b/key_test.go index ecb551c..931fa84 100644 --- a/key_test.go +++ b/key_test.go @@ -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]) } }) diff --git a/mouse.go b/mouse.go index 6cca0b6..5fb7427 100644 --- a/mouse.go +++ b/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 @@ -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") +func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) { + var r []MouseEvent + + seq := []byte("\x1b[M") + if !bytes.Contains(buf, seq) { + return r, errors.New("not an X10 mouse event") } - const byteOffset = 32 - - e := buf[3] - 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 + for _, v := range bytes.Split(buf, seq) { + if len(v) == 0 { + continue } - } 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 len(v) != 3 { + return r, errors.New("not an X10 mouse event") + } + + 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&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 { - 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 + return r, nil } diff --git a/mouse_test.go b/mouse_test.go index 9a4df8a..d9c108f 100644 --- a/mouse_test.go +++ b/mouse_test.go @@ -122,209 +122,268 @@ func TestParseX10MouseEvent(t *testing.T) { tt := []struct { name string buf []byte - expected MouseEvent + expected []MouseEvent }{ // Position. { name: "zero position", buf: encode(0b0010_0000, 0, 0), - expected: MouseEvent{ - X: 0, - Y: 0, - Type: MouseLeft, + expected: []MouseEvent{ + { + X: 0, + Y: 0, + Type: MouseLeft, + }, }, }, { name: "max position", buf: encode(0b0010_0000, 222, 222), // Because 255 (max int8) - 32 - 1. - expected: MouseEvent{ - X: 222, - Y: 222, - Type: MouseLeft, + expected: []MouseEvent{ + { + X: 222, + Y: 222, + Type: MouseLeft, + }, }, }, // Simple. { name: "left", buf: encode(0b0000_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseLeft, + }, }, }, { name: "left in motion", buf: encode(0b0010_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseLeft, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseLeft, + }, }, }, { name: "middle", buf: encode(0b0000_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMiddle, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseMiddle, + }, }, }, { name: "middle in motion", buf: encode(0b0010_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMiddle, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseMiddle, + }, }, }, { name: "right", buf: encode(0b0000_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseRight, + }, }, }, { name: "right in motion", buf: encode(0b0010_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseRight, + }, }, }, { name: "motion", buf: encode(0b0010_0011, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseMotion, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseMotion, + }, }, }, { name: "wheel up", buf: encode(0b0100_0000, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelUp, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelUp, + }, }, }, { name: "wheel down", buf: encode(0b0100_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelDown, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelDown, + }, }, }, { name: "release", buf: encode(0b0000_0011, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRelease, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseRelease, + }, }, }, // Combinations. { name: "alt+right", buf: encode(0b0010_1010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, - Alt: true, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseRight, + Alt: true, + }, }, }, { name: "ctrl+right", buf: encode(0b0011_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, - Ctrl: true, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseRight, + Ctrl: true, + }, }, }, { name: "ctrl+alt+right", buf: encode(0b0011_1010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseRight, - Alt: true, - Ctrl: true, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseRight, + Alt: true, + Ctrl: true, + }, }, }, { name: "alt+wheel down", buf: encode(0b0100_1001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelDown, - Alt: true, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelDown, + Alt: true, + }, }, }, { name: "ctrl+wheel down", buf: encode(0b0101_0001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelDown, - Ctrl: true, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelDown, + Ctrl: true, + }, }, }, { name: "ctrl+alt+wheel down", buf: encode(0b0101_1001, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseWheelDown, - Alt: true, - Ctrl: true, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseWheelDown, + Alt: true, + Ctrl: true, + }, }, }, // Unknown. { name: "wheel with unknown bit", buf: encode(0b0100_0010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseUnknown, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseUnknown, + }, }, }, { name: "unknown with modifier", buf: encode(0b0100_1010, 32, 16), - expected: MouseEvent{ - X: 32, - Y: 16, - Type: MouseUnknown, - Alt: true, + expected: []MouseEvent{ + { + X: 32, + Y: 16, + Type: MouseUnknown, + Alt: true, + }, }, }, // Overflow position. { name: "overflow position", buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1. - expected: MouseEvent{ - X: -6, - Y: -33, - Type: MouseLeft, + expected: []MouseEvent{ + { + X: -6, + 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] t.Run(tc.name, func(t *testing.T) { - actual, err := parseX10MouseEvent(tc.buf) + actual, err := parseX10MouseEvents(tc.buf) if err != nil { - t.Fatalf("unexpected error: %v", + t.Fatalf("unexpected error for test: %v", err, ) } - if tc.expected != actual { - t.Fatalf("expected %#v but got %#v", - tc.expected, - actual, - ) + for i := range tc.expected { + if tc.expected[i] != actual[i] { + t.Fatalf("expected %#v but got %#v", + tc.expected[i], + actual[i], + ) + } } }) } @@ -377,7 +438,7 @@ func TestParseX10MouseEvent_error(t *testing.T) { tc := tt[i] t.Run(tc.name, func(t *testing.T) { - _, err := parseX10MouseEvent(tc.buf) + _, err := parseX10MouseEvents(tc.buf) if err == nil { t.Fatalf("expected error but got nil") diff --git a/tea.go b/tea.go index 3baaf5b..b883fe8 100644 --- a/tea.go +++ b/tea.go @@ -421,7 +421,7 @@ func (p *Program) StartReturningModel() (Model, error) { return } - msg, err := readInput(cancelReader) + msgs, err := readInputs(cancelReader) if err != nil { if !errors.Is(err, io.EOF) && !errors.Is(err, errCanceled) { errs <- err @@ -430,7 +430,9 @@ func (p *Program) StartReturningModel() (Model, error) { return } - p.msgs <- msg + for _, msg := range msgs { + p.msgs <- msg + } } }() } else {