Skip to content

Commit

Permalink
Support receiving batched mouse events
Browse files Browse the repository at this point in the history
Mouse events may trigger more than a single events simultaneously.

Fixes #212.
  • Loading branch information
muesli committed Feb 13, 2022
1 parent db177f1 commit 6301f93
Show file tree
Hide file tree
Showing 5 changed files with 278 additions and 178 deletions.
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
}

0 comments on commit 6301f93

Please sign in to comment.