fix(key),test: simplify the input analysis code

This commit is contained in:
Raphael 'kena' Poss 2022-10-21 17:00:39 +02:00 committed by Christian Muehlhaeuser
parent c0cc6aa1fb
commit d9c675138c
5 changed files with 828 additions and 511 deletions

236
key.go
View File

@ -1,8 +1,9 @@
package tea
import (
"errors"
"fmt"
"io"
"regexp"
"unicode/utf8"
"github.com/mattn/go-localereader"
@ -358,18 +359,12 @@ var sequences = map[string]Key{
"\x1b[1;3B": {Type: KeyDown, Alt: true},
"\x1b[1;3C": {Type: KeyRight, Alt: true},
"\x1b[1;3D": {Type: KeyLeft, Alt: true},
"\x1b\x1b[A": {Type: KeyUp, Alt: true}, // urxvt
"\x1b\x1b[B": {Type: KeyDown, Alt: true}, // urxvt
"\x1b\x1b[C": {Type: KeyRight, Alt: true}, // urxvt
"\x1b\x1b[D": {Type: KeyLeft, Alt: true}, // urxvt
"\x1b[1;4A": {Type: KeyShiftUp, Alt: true},
"\x1b[1;4B": {Type: KeyShiftDown, Alt: true},
"\x1b[1;4C": {Type: KeyShiftRight, Alt: true},
"\x1b[1;4D": {Type: KeyShiftLeft, Alt: true},
"\x1b\x1b[a": {Type: KeyShiftUp, Alt: true}, // urxvt
"\x1b\x1b[b": {Type: KeyShiftDown, Alt: true}, // urxvt
"\x1b\x1b[c": {Type: KeyShiftRight, Alt: true}, // urxvt
"\x1b\x1b[d": {Type: KeyShiftLeft, Alt: true}, // urxvt
"\x1b[1;5A": {Type: KeyCtrlUp},
"\x1b[1;5B": {Type: KeyCtrlDown},
"\x1b[1;5C": {Type: KeyCtrlRight},
@ -396,27 +391,21 @@ var sequences = map[string]Key{
"\x1b[2~": {Type: KeyInsert},
"\x1b[3;2~": {Type: KeyInsert, Alt: true},
"\x1b\x1b[2~": {Type: KeyInsert, Alt: true}, // urxvt
"\x1b[3~": {Type: KeyDelete},
"\x1b[3;3~": {Type: KeyDelete, Alt: true},
"\x1b\x1b[3~": {Type: KeyDelete, Alt: true}, // urxvt
"\x1b[5~": {Type: KeyPgUp},
"\x1b[5;3~": {Type: KeyPgUp, Alt: true},
"\x1b\x1b[5~": {Type: KeyPgUp, Alt: true}, // urxvt
"\x1b[5;5~": {Type: KeyCtrlPgUp},
"\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt
"\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true},
"\x1b\x1b[5^": {Type: KeyCtrlPgUp, Alt: true}, // urxvt
"\x1b[6~": {Type: KeyPgDown},
"\x1b[6;3~": {Type: KeyPgDown, Alt: true},
"\x1b\x1b[6~": {Type: KeyPgDown, Alt: true}, // urxvt
"\x1b[6;5~": {Type: KeyCtrlPgDown},
"\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt
"\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true},
"\x1b\x1b[6^": {Type: KeyCtrlPgDown, Alt: true}, // urxvt
"\x1b[1~": {Type: KeyHome},
"\x1b[H": {Type: KeyHome}, // xterm, lxterm
@ -439,22 +428,14 @@ var sequences = map[string]Key{
"\x1b[1;8F": {Type: KeyCtrlShiftEnd, Alt: true}, // xterm, lxterm
"\x1b[7~": {Type: KeyHome}, // urxvt
"\x1b\x1b[7~": {Type: KeyHome, Alt: true}, // urxvt
"\x1b[7^": {Type: KeyCtrlHome}, // urxvt
"\x1b\x1b[7^": {Type: KeyCtrlHome, Alt: true}, // urxvt
"\x1b[7$": {Type: KeyShiftHome}, // urxvt
"\x1b\x1b[7$": {Type: KeyShiftHome, Alt: true}, // urxvt
"\x1b[7@": {Type: KeyCtrlShiftHome}, // urxvt
"\x1b\x1b[7@": {Type: KeyCtrlShiftHome, Alt: true}, // urxvt
"\x1b[8~": {Type: KeyEnd}, // urxvt
"\x1b\x1b[8~": {Type: KeyEnd, Alt: true}, // urxvt
"\x1b[8^": {Type: KeyCtrlEnd}, // urxvt
"\x1b\x1b[8^": {Type: KeyCtrlEnd, Alt: true}, // urxvt
"\x1b[8$": {Type: KeyShiftEnd}, // urxvt
"\x1b\x1b[8$": {Type: KeyShiftEnd, Alt: true}, // urxvt
"\x1b[8@": {Type: KeyCtrlShiftEnd}, // urxvt
"\x1b\x1b[8@": {Type: KeyCtrlShiftEnd, Alt: true}, // urxvt
// Function keys, Linux console
"\x1b[[A": {Type: KeyF1}, // linux console
@ -479,29 +460,16 @@ var sequences = map[string]Key{
"\x1b[13~": {Type: KeyF3}, // urxvt
"\x1b[14~": {Type: KeyF4}, // urxvt
"\x1b\x1b[11~": {Type: KeyF1, Alt: true}, // urxvt
"\x1b\x1b[12~": {Type: KeyF2, Alt: true}, // urxvt
"\x1b\x1b[13~": {Type: KeyF3, Alt: true}, // urxvt
"\x1b\x1b[14~": {Type: KeyF4, Alt: true}, // urxvt
"\x1b[15~": {Type: KeyF5}, // vt100, xterm, also urxvt
"\x1b[15;3~": {Type: KeyF5, Alt: true}, // vt100, xterm, also urxvt
"\x1b\x1b[15~": {Type: KeyF5, Alt: true}, // urxvt
"\x1b[17~": {Type: KeyF6}, // vt100, xterm, also urxvt
"\x1b[18~": {Type: KeyF7}, // vt100, xterm, also urxvt
"\x1b[19~": {Type: KeyF8}, // vt100, xterm, also urxvt
"\x1b[20~": {Type: KeyF9}, // vt100, xterm, also urxvt
"\x1b[21~": {Type: KeyF10}, // vt100, xterm, also urxvt
"\x1b\x1b[17~": {Type: KeyF6, Alt: true}, // urxvt
"\x1b\x1b[18~": {Type: KeyF7, Alt: true}, // urxvt
"\x1b\x1b[19~": {Type: KeyF8, Alt: true}, // urxvt
"\x1b\x1b[20~": {Type: KeyF9, Alt: true}, // urxvt
"\x1b\x1b[21~": {Type: KeyF10, Alt: true}, // urxvt
"\x1b[17;3~": {Type: KeyF6, Alt: true}, // vt100, xterm
"\x1b[18;3~": {Type: KeyF7, Alt: true}, // vt100, xterm
"\x1b[19;3~": {Type: KeyF8, Alt: true}, // vt100, xterm
@ -514,9 +482,6 @@ var sequences = map[string]Key{
"\x1b[23;3~": {Type: KeyF11, Alt: true}, // vt100, xterm
"\x1b[24;3~": {Type: KeyF12, Alt: true}, // vt100, xterm
"\x1b\x1b[23~": {Type: KeyF11, Alt: true}, // urxvt
"\x1b\x1b[24~": {Type: KeyF12, Alt: true}, // urxvt
"\x1b[1;2P": {Type: KeyF13},
"\x1b[1;2Q": {Type: KeyF14},
@ -526,9 +491,6 @@ var sequences = map[string]Key{
"\x1b[25;3~": {Type: KeyF13, Alt: true}, // vt100, xterm
"\x1b[26;3~": {Type: KeyF14, Alt: true}, // vt100, xterm
"\x1b\x1b[25~": {Type: KeyF13, Alt: true}, // urxvt
"\x1b\x1b[26~": {Type: KeyF14, Alt: true}, // urxvt
"\x1b[1;2R": {Type: KeyF15},
"\x1b[1;2S": {Type: KeyF16},
@ -538,9 +500,6 @@ var sequences = map[string]Key{
"\x1b[28;3~": {Type: KeyF15, Alt: true}, // vt100, xterm
"\x1b[29;3~": {Type: KeyF16, Alt: true}, // vt100, xterm
"\x1b\x1b[28~": {Type: KeyF15, Alt: true}, // urxvt
"\x1b\x1b[29~": {Type: KeyF16, Alt: true}, // urxvt
"\x1b[15;2~": {Type: KeyF17},
"\x1b[17;2~": {Type: KeyF18},
"\x1b[18;2~": {Type: KeyF19},
@ -551,11 +510,6 @@ var sequences = map[string]Key{
"\x1b[33~": {Type: KeyF19},
"\x1b[34~": {Type: KeyF20},
"\x1b\x1b[31~": {Type: KeyF17, Alt: true}, // urxvt
"\x1b\x1b[32~": {Type: KeyF18, Alt: true}, // urxvt
"\x1b\x1b[33~": {Type: KeyF19, Alt: true}, // urxvt
"\x1b\x1b[34~": {Type: KeyF20, Alt: true}, // urxvt
// Powershell sequences.
"\x1bOA": {Type: KeyUp, Alt: false},
"\x1bOB": {Type: KeyDown, Alt: false},
@ -563,102 +517,118 @@ var sequences = map[string]Key{
"\x1bOD": {Type: KeyLeft, Alt: false},
}
// unknownInputByteMsg is reported by the input reader when an invalid
// utf-8 byte is detected on the input. Currently, it is not handled
// further by bubbletea. However, having this event makes it possible
// to troubleshoot invalid inputs.
type unknownInputByteMsg byte
func (u unknownInputByteMsg) String() string {
return fmt.Sprintf("?%#02x?", int(u))
}
// unknownCSISequenceMsg is reported by the input reader when an
// unrecognized CSI sequence is detected on the input. Currently, it
// is not handled further by bubbletea. However, having this event
// makes it possible to troubleshoot invalid inputs.
type unknownCSISequenceMsg []byte
func (u unknownCSISequenceMsg) String() string {
return fmt.Sprintf("?CSI%+v?", []byte(u)[2:])
}
var spaceRunes = []rune{' '}
// 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
input = localereader.NewReader(input)
// Read and block
numBytes, err := input.Read(buf[:])
if err != nil {
return nil, err
}
b := buf[:numBytes]
b, err = localereader.UTF8(b)
if err != nil {
return nil, err
}
// Check if it's a mouse event. For now we're parsing X10-type mouse events
// only.
mouseEvent, err := parseX10MouseEvents(b)
if err == nil {
var m []Msg
for _, v := range mouseEvent {
m = append(m, MouseMsg(v))
}
return m, nil
}
var runeSets [][]rune
var runes []rune
// Translate input into runes. In most cases we'll receive exactly one
// rune, but there are cases, particularly when an input method editor is
// used, where we can receive multiple runes at once.
for i, w := 0, 0; i < len(b); i += w {
r, width := utf8.DecodeRune(b[i:])
if r == utf8.RuneError {
return nil, errors.New("could not decode rune")
}
if r == '\x1b' && len(runes) > 1 {
// a new key sequence has started
runeSets = append(runeSets, runes)
runes = []rune{}
}
runes = append(runes, r)
w = width
}
// add the final set of runes we decoded
runeSets = append(runeSets, runes)
if len(runeSets) == 0 {
return nil, errors.New("received 0 runes from input")
}
var msgs []Msg
for _, runes := range runeSets {
// Is it a sequence, like an arrow key?
if k, ok := sequences[string(runes)]; ok {
msgs = append(msgs, KeyMsg(k))
continue
for i, w := 0, 0; i < len(b); i += w {
var msg Msg
w, msg = detectOneMsg(b[i:])
msgs = append(msgs, msg)
}
// Is this an unrecognized CSI sequence? If so, ignore it.
if len(runes) > 2 && runes[0] == 0x1b && (runes[1] == '[' ||
(len(runes) > 3 && runes[1] == 0x1b && runes[2] == '[')) {
continue
}
// Is the alt key pressed? If so, the buffer will be prefixed with an
// escape.
alt := false
if len(runes) > 1 && runes[0] == 0x1b {
alt = true
runes = runes[1:]
}
for _, v := range runes {
// Is the first rune a control character?
r := KeyType(v)
if r <= keyUS || r == keyDEL {
msgs = append(msgs, KeyMsg(Key{Type: r, Alt: alt}))
continue
}
// If it's a space, override the type with KeySpace (but still include
// the rune).
if r == ' ' {
msgs = append(msgs, KeyMsg(Key{Type: KeySpace, Runes: []rune{v}, Alt: alt}))
continue
}
// Welp, just regular, ol' runes.
msgs = append(msgs, KeyMsg(Key{Type: KeyRunes, Runes: []rune{v}, Alt: alt}))
}
}
return msgs, nil
}
var unknownCSIRe = regexp.MustCompile(`^\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]`)
func detectOneMsg(b []byte) (w int, msg Msg) {
// Detect mouse events.
if len(b) >= 6 && b[0] == '\x1b' && b[1] == '[' && b[2] == 'M' {
return 6, MouseMsg(parseX10MouseEvent(b))
}
// Detect escape sequence and control characters other than NUL,
// possibly with an escape character in front to mark the Alt
// modifier.
var foundSeq bool
foundSeq, w, msg = detectSequence(b)
if foundSeq {
return
}
// No non-NUL control character or escape sequence.
// If we are seeing at least an escape character, remember it for later below.
alt := false
i := 0
if b[0] == '\x1b' {
alt = true
i++
}
// Are we seeing a standalone NUL? This is not handled by detectSequence().
if i < len(b) && b[i] == 0 {
return i + 1, KeyMsg{Type: keyNUL, Alt: alt}
}
// Find the longest sequence of runes that are not control
// characters from this point.
var runes []rune
for rw := 0; i < len(b); i += rw {
var r rune
r, rw = utf8.DecodeRune(b[i:])
if r == utf8.RuneError || r <= rune(keyUS) || r == rune(keyDEL) || r == ' ' {
// Rune errors are handled below; control characters and spaces will
// be handled by detectSequence in the next call to detectOneMsg.
break
}
runes = append(runes, r)
if alt {
// We only support a single rune after an escape alt modifier.
i += rw
break
}
}
// If we found at least one rune, we report the bunch of them as
// a single KeyRunes or KeySpace event.
if len(runes) > 0 {
k := Key{Type: KeyRunes, Runes: runes, Alt: alt}
if len(runes) == 1 && runes[0] == ' ' {
k.Type = KeySpace
}
return i, KeyMsg(k)
}
// We didn't find an escape sequence, nor a valid rune. Was this a
// lone escape character at the end of the input?
if alt && len(b) == 1 {
return 1, KeyMsg(Key{Type: KeyEscape})
}
// The character at the current position is neither an escape
// sequence, a valid rune start or a sole escape character. Report
// it as an invalid byte.
return 1, unknownInputByteMsg(b[0])
}

71
key_sequences.go Normal file
View File

@ -0,0 +1,71 @@
package tea
import "sort"
// extSequences is used by the map-based algorithm below. It contains
// the sequences plus their alternatives with an escape character
// prefixed, plus the control chars, plus the space.
// It does not contain the NUL character, which is handled specially
// by detectOneMsg.
var extSequences = func() map[string]Key {
s := map[string]Key{}
for seq, key := range sequences {
key := key
s[seq] = key
if !key.Alt {
key.Alt = true
s["\x1b"+seq] = key
}
}
for i := keyNUL + 1; i <= keyDEL; i++ {
if i == keyESC {
continue
}
s[string([]byte{byte(i)})] = Key{Type: i}
s[string([]byte{'\x1b', byte(i)})] = Key{Type: i, Alt: true}
if i == keyUS {
i = keyDEL - 1
}
}
s[" "] = Key{Type: KeySpace, Runes: spaceRunes}
s["\x1b "] = Key{Type: KeySpace, Alt: true, Runes: spaceRunes}
s["\x1b\x1b"] = Key{Type: KeyEscape, Alt: true}
return s
}()
// seqLengths is the sizes of valid sequences, starting with the
// largest size.
var seqLengths = func() []int {
sizes := map[int]struct{}{}
for seq := range extSequences {
sizes[len(seq)] = struct{}{}
}
lsizes := make([]int, 0, len(sizes))
for sz := range sizes {
lsizes = append(lsizes, sz)
}
sort.Slice(lsizes, func(i, j int) bool { return lsizes[i] > lsizes[j] })
return lsizes
}()
// detectSequence uses a longest prefix match over the input
// sequence and a hash map.
func detectSequence(input []byte) (hasSeq bool, width int, msg Msg) {
seqs := extSequences
for _, sz := range seqLengths {
if sz > len(input) {
continue
}
prefix := input[:sz]
key, ok := seqs[string(prefix)]
if ok {
return true, sz, KeyMsg(key)
}
}
// Is this an unknown CSI sequence?
if loc := unknownCSIRe.FindIndex(input); loc != nil {
return true, loc[1], unknownCSISequenceMsg(input[:loc[1]])
}
return false, 0, nil
}

View File

@ -2,8 +2,15 @@ package tea
import (
"bytes"
"flag"
"fmt"
"math/rand"
"reflect"
"runtime"
"sort"
"strings"
"testing"
"time"
)
func TestKeyString(t *testing.T) {
@ -48,13 +55,165 @@ func TestKeyTypeString(t *testing.T) {
})
}
type seqTest struct {
seq []byte
msg Msg
}
// buildBaseSeqTests returns sequence tests that are valid for the
// detectSequence() function.
func buildBaseSeqTests() []seqTest {
td := []seqTest{}
for seq, key := range sequences {
key := key
td = append(td, seqTest{[]byte(seq), KeyMsg(key)})
if !key.Alt {
key.Alt = true
td = append(td, seqTest{[]byte("\x1b" + seq), KeyMsg(key)})
}
}
// Add all the control characters.
for i := keyNUL + 1; i <= keyDEL; i++ {
if i == keyESC {
// Not handled in detectSequence(), so not part of the base test
// suite.
continue
}
td = append(td, seqTest{[]byte{byte(i)}, KeyMsg{Type: i}})
td = append(td, seqTest{[]byte{'\x1b', byte(i)}, KeyMsg{Type: i, Alt: true}})
if i == keyUS {
i = keyDEL - 1
}
}
// Additional special cases.
td = append(td,
// Unrecognized CSI sequence.
seqTest{
[]byte{'\x1b', '[', '-', '-', '-', '-', 'X'},
unknownCSISequenceMsg([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'}),
},
// A lone space character.
seqTest{
[]byte{' '},
KeyMsg{Type: KeySpace, Runes: []rune(" ")},
},
// An escape character with the alt modifier.
seqTest{
[]byte{'\x1b', ' '},
KeyMsg{Type: KeySpace, Runes: []rune(" "), Alt: true},
},
)
return td
}
func TestDetectSequence(t *testing.T) {
td := buildBaseSeqTests()
for _, tc := range td {
t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) {
hasSeq, width, msg := detectSequence(tc.seq)
if !hasSeq {
t.Fatalf("no sequence found")
}
if width != len(tc.seq) {
t.Errorf("parser did not consume the entire input: got %d, expected %d", width, len(tc.seq))
}
if !reflect.DeepEqual(tc.msg, msg) {
t.Errorf("expected event %#v (%T), got %#v (%T)", tc.msg, tc.msg, msg, msg)
}
})
}
}
func TestDetectOneMsg(t *testing.T) {
td := buildBaseSeqTests()
// Add tests for the inputs that detectOneMsg() can parse, but
// detectSequence() cannot.
td = append(td,
// Mouse event.
seqTest{
[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
MouseMsg{X: 32, Y: 16, Type: MouseWheelUp},
},
// Runes.
seqTest{
[]byte{'a'},
KeyMsg{Type: KeyRunes, Runes: []rune("a")},
},
seqTest{
[]byte{'\x1b', 'a'},
KeyMsg{Type: KeyRunes, Runes: []rune("a"), Alt: true},
},
seqTest{
[]byte{'a', 'a', 'a'},
KeyMsg{Type: KeyRunes, Runes: []rune("aaa")},
},
// Multi-byte rune.
seqTest{
[]byte("☃"),
KeyMsg{Type: KeyRunes, Runes: []rune("☃")},
},
seqTest{
[]byte("\x1b☃"),
KeyMsg{Type: KeyRunes, Runes: []rune("☃"), Alt: true},
},
// Standalone control chacters.
seqTest{
[]byte{'\x1b'},
KeyMsg{Type: KeyEscape},
},
seqTest{
[]byte{byte(keySOH)},
KeyMsg{Type: KeyCtrlA},
},
seqTest{
[]byte{'\x1b', byte(keySOH)},
KeyMsg{Type: KeyCtrlA, Alt: true},
},
seqTest{
[]byte{byte(keyNUL)},
KeyMsg{Type: KeyCtrlAt},
},
seqTest{
[]byte{'\x1b', byte(keyNUL)},
KeyMsg{Type: KeyCtrlAt, Alt: true},
},
// Invalid characters.
seqTest{
[]byte{'\x80'},
unknownInputByteMsg(0x80),
},
)
if runtime.GOOS != "windows" {
// Sadly, utf8.DecodeRune([]byte(0xfe)) returns a valid rune on windows.
// This is incorrect, but it makes our test fail if we try it out.
td = append(td, seqTest{
[]byte{'\xfe'},
unknownInputByteMsg(0xfe),
})
}
for _, tc := range td {
t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) {
width, msg := detectOneMsg(tc.seq)
if width != len(tc.seq) {
t.Errorf("parser did not consume the entire input: got %d, expected %d", width, len(tc.seq))
}
if !reflect.DeepEqual(tc.msg, msg) {
t.Errorf("expected event %#v (%T), got %#v (%T)", tc.msg, tc.msg, msg, msg)
}
})
}
}
func TestReadInput(t *testing.T) {
type test struct {
keyname string
in []byte
out []Msg
}
for i, td := range []test{
testData := []test{
{"a",
[]byte{'a'},
[]Msg{
@ -73,6 +232,21 @@ func TestReadInput(t *testing.T) {
},
},
},
{"a alt+a",
[]byte{'a', '\x1b', 'a'},
[]Msg{
KeyMsg{Type: KeyRunes, Runes: []rune{'a'}},
KeyMsg{Type: KeyRunes, Runes: []rune{'a'}, Alt: true},
},
},
{"a alt+a a",
[]byte{'a', '\x1b', 'a', 'a'},
[]Msg{
KeyMsg{Type: KeyRunes, Runes: []rune{'a'}},
KeyMsg{Type: KeyRunes, Runes: []rune{'a'}, Alt: true},
KeyMsg{Type: KeyRunes, Runes: []rune{'a'}},
},
},
{"ctrl+a",
[]byte{byte(keySOH)},
[]Msg{
@ -81,6 +255,13 @@ func TestReadInput(t *testing.T) {
},
},
},
{"ctrl+a ctrl+b",
[]byte{byte(keySOH), byte(keySTX)},
[]Msg{
KeyMsg{Type: KeyCtrlA},
KeyMsg{Type: KeyCtrlB},
},
},
{"alt+a",
[]byte{byte(0x1b), 'a'},
[]Msg{
@ -96,19 +277,7 @@ func TestReadInput(t *testing.T) {
[]Msg{
KeyMsg{
Type: KeyRunes,
Runes: []rune{'a'},
},
KeyMsg{
Type: KeyRunes,
Runes: []rune{'b'},
},
KeyMsg{
Type: KeyRunes,
Runes: []rune{'c'},
},
KeyMsg{
Type: KeyRunes,
Runes: []rune{'d'},
Runes: []rune{'a', 'b', 'c', 'd'},
},
},
},
@ -124,10 +293,30 @@ func TestReadInput(t *testing.T) {
[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
[]Msg{
MouseMsg{
X: 32,
Y: 16,
Type: MouseWheelUp,
},
},
},
{"left 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,
}),
MouseMsg(MouseEvent{
X: 64,
Y: 32,
Type: MouseRelease,
}),
},
},
{"shift+tab",
[]byte{'\x1b', '[', 'Z'},
[]Msg{
@ -136,6 +325,10 @@ func TestReadInput(t *testing.T) {
},
},
},
{"enter",
[]byte{'\r'},
[]Msg{KeyMsg{Type: KeyEnter}},
},
{"alt+enter",
[]byte{'\x1b', '\r'},
[]Msg{
@ -162,9 +355,9 @@ func TestReadInput(t *testing.T) {
},
},
},
{"unrecognized CSI",
{"?CSI[45 45 45 45 88]?",
[]byte{'\x1b', '[', '-', '-', '-', '-', 'X'},
[]Msg{},
[]Msg{unknownCSISequenceMsg([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'})},
},
// Powershell sequences.
{"up",
@ -191,34 +384,236 @@ func TestReadInput(t *testing.T) {
[]byte{'\x1b', '\x7f'},
[]Msg{KeyMsg{Type: KeyBackspace, Alt: true}},
},
} {
{"ctrl+@",
[]byte{'\x00'},
[]Msg{KeyMsg{Type: KeyCtrlAt}},
},
{"alt+ctrl+@",
[]byte{'\x1b', '\x00'},
[]Msg{KeyMsg{Type: KeyCtrlAt, Alt: true}},
},
{"esc",
[]byte{'\x1b'},
[]Msg{KeyMsg{Type: KeyEsc}},
},
{"alt+esc",
[]byte{'\x1b', '\x1b'},
[]Msg{KeyMsg{Type: KeyEsc, Alt: true}},
},
// Bracketed paste does not work yet.
{"?CSI[50 48 48 126]? a b ?CSI[50 48 49 126]?",
[]byte{
'\x1b', '[', '2', '0', '0', '~',
'a', ' ', 'b',
'\x1b', '[', '2', '0', '1', '~'},
[]Msg{
// What we expect once bracketed paste is recognized properly:
//
// KeyMsg{Type: KeyRunes, Runes: []rune("a b")},
//
// What we get instead (for now):
unknownCSISequenceMsg{0x1b, 0x5b, 0x32, 0x30, 0x30, 0x7e},
KeyMsg{Type: KeyRunes, Runes: []rune{'a'}},
KeyMsg{Type: KeySpace, Runes: []rune{' '}},
KeyMsg{Type: KeyRunes, Runes: []rune{'b'}},
unknownCSISequenceMsg{0x1b, 0x5b, 0x32, 0x30, 0x31, 0x7e},
},
},
}
if runtime.GOOS != "windows" {
// Sadly, utf8.DecodeRune([]byte(0xfe)) returns a valid rune on windows.
// This is incorrect, but it makes our test fail if we try it out.
testData = append(testData,
test{"?0xfe?",
[]byte{'\xfe'},
[]Msg{unknownInputByteMsg(0xfe)},
},
test{"a ?0xfe? b",
[]byte{'a', '\xfe', ' ', 'b'},
[]Msg{
KeyMsg{Type: KeyRunes, Runes: []rune{'a'}},
unknownInputByteMsg(0xfe),
KeyMsg{Type: KeySpace, Runes: []rune{' '}},
KeyMsg{Type: KeyRunes, Runes: []rune{'b'}},
},
},
)
}
for i, td := range testData {
t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) {
msgs, err := readInputs(bytes.NewReader(td.in))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Compute the title for the event sequence.
var buf strings.Builder
for i, msg := range msgs {
if i > 0 {
buf.WriteByte(' ')
}
if s, ok := msg.(fmt.Stringer); ok {
buf.WriteString(s.String())
} else {
fmt.Fprintf(&buf, "%#v:%T", msg, msg)
}
}
title := buf.String()
if title != td.keyname {
t.Errorf("expected message titles:\n %s\ngot:\n %s", td.keyname, title)
}
if len(msgs) != len(td.out) {
t.Fatalf("unexpected message list length")
t.Fatalf("unexpected message list length: got %d, expected %d\n%#v", len(msgs), len(td.out), msgs)
}
if len(msgs) == 1 {
if m, ok := msgs[0].(KeyMsg); ok && m.String() != td.keyname {
t.Fatalf(`expected a keymsg %q, got %q`, td.keyname, m)
if !reflect.DeepEqual(td.out, msgs) {
t.Fatalf("expected:\n%#v\ngot:\n%#v", td.out, msgs)
}
})
}
}
for i, v := range msgs {
if m, ok := v.(KeyMsg); ok &&
m.String() != td.out[i].(KeyMsg).String() {
t.Fatalf(`expected a keymsg %q, got %q`, td.out[i].(KeyMsg), m)
// randTest defines the test input and expected output for a sequence
// of interleaved control sequences and control characters.
type randTest struct {
data []byte
lengths []int
names []string
}
// seed is the random seed to randomize the input. This helps check
// that all the sequences get ultimately exercised.
var seed = flag.Int64("seed", 0, "random seed (0 to autoselect)")
// genRandomData generates a randomized test, with a random seed unless
// the seed flag was set.
func genRandomData(logfn func(int64), length int) randTest {
// We'll use a random source. However, we give the user the option
// to override it to a specific value for reproduceability.
s := *seed
if s == 0 {
s = time.Now().UnixNano()
}
// Inform the user so they know what to reuse to get the same data.
logfn(s)
return genRandomDataWithSeed(s, length)
}
// genRandomDataWithSeed generates a randomized test with a fixed seed.
func genRandomDataWithSeed(s int64, length int) randTest {
src := rand.NewSource(s)
r := rand.New(src)
// allseqs contains all the sequences, in sorted order. We sort
// to make the test deterministic (when the seed is also fixed).
type seqpair struct {
seq string
name string
}
var allseqs []seqpair
for seq, key := range sequences {
allseqs = append(allseqs, seqpair{seq, key.String()})
}
sort.Slice(allseqs, func(i, j int) bool { return allseqs[i].seq < allseqs[j].seq })
// res contains the computed test.
var res randTest
for len(res.data) < length {
alt := r.Intn(2)
prefix := ""
esclen := 0
if alt == 1 {
prefix = "alt+"
esclen = 1
}
kind := r.Intn(3)
switch kind {
case 0:
// A control character.
if alt == 1 {
res.data = append(res.data, '\x1b')
}
res.data = append(res.data, 1)
res.names = append(res.names, prefix+"ctrl+a")
res.lengths = append(res.lengths, 1+esclen)
case 1, 2:
// A sequence.
seqi := r.Intn(len(allseqs))
s := allseqs[seqi]
if strings.HasPrefix(s.name, "alt+") {
esclen = 0
prefix = ""
alt = 0
}
if alt == 1 {
res.data = append(res.data, '\x1b')
}
res.data = append(res.data, s.seq...)
res.names = append(res.names, prefix+s.name)
res.lengths = append(res.lengths, len(s.seq)+esclen)
}
}
return res
}
// TestDetectRandomSequencesLex checks that the lex-generated sequence
// detector works over concatenations of random sequences.
func TestDetectRandomSequencesLex(t *testing.T) {
runTestDetectSequence(t, detectSequence)
}
func runTestDetectSequence(
t *testing.T, detectSequence func(input []byte) (hasSeq bool, width int, msg Msg),
) {
for i := 0; i < 10; i++ {
t.Run("", func(t *testing.T) {
td := genRandomData(func(s int64) { t.Logf("using random seed: %d", s) }, 1000)
t.Logf("%#v", td)
// tn is the event number in td.
// i is the cursor in the input data.
// w is the length of the last sequence detected.
for tn, i, w := 0, 0, 0; i < len(td.data); tn, i = tn+1, i+w {
hasSequence, width, msg := detectSequence(td.data[i:])
if !hasSequence {
t.Fatalf("at %d (ev %d): failed to find sequence", i, tn)
}
if width != td.lengths[tn] {
t.Errorf("at %d (ev %d): expected width %d, got %d", i, tn, td.lengths[tn], width)
}
w = width
s, ok := msg.(fmt.Stringer)
if !ok {
t.Errorf("at %d (ev %d): expected stringer event, got %T", i, tn, msg)
} else {
if td.names[tn] != s.String() {
t.Errorf("at %d (ev %d): expected event %q, got %q", i, tn, td.names[tn], s.String())
}
if m, ok := v.(MouseMsg); ok &&
(mouseEventTypes[m.Type] != td.keyname || m.Type != td.out[i].(MouseMsg).Type) {
t.Fatalf(`expected a mousemsg %q, got %q`,
td.keyname,
mouseEventTypes[td.out[i].(MouseMsg).Type])
}
}
})
}
}
// TestDetectRandomSequencesLex checks that the map-based sequence
// detector works over concatenations of random sequences.
func TestDetectRandomSequencesMap(t *testing.T) {
runTestDetectSequence(t, detectSequence)
}
// BenchmarkDetectSequenceMap benchmarks the map-based sequence
// detector.
func BenchmarkDetectSequenceMap(b *testing.B) {
td := genRandomDataWithSeed(123, 10000)
for i := 0; i < b.N; i++ {
for j, w := 0, 0; j < len(td.data); j += w {
_, w, _ = detectSequence(td.data[j:])
}
}
}

View File

@ -1,15 +1,15 @@
package tea
import (
"bytes"
"errors"
)
// MouseMsg contains information about a mouse event and is sent to a program's
// 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
// be enabled in order for the mouse events to be received.
type MouseMsg MouseEvent
// String returns a string representation of a mouse event.
func (m MouseMsg) String() string {
return MouseEvent(m).String()
}
// MouseEvent represents a mouse event, which could be a click, a scroll wheel
// movement, a cursor movement, or a combination.
type MouseEvent struct {
@ -66,22 +66,8 @@ var mouseEventTypes = map[MouseEventType]string{
// ESC [M Cb Cx Cy
//
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
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")
}
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")
}
func parseX10MouseEvent(buf []byte) MouseEvent {
v := buf[3:6]
var m MouseEvent
const byteOffset = 32
e := v[0] - byteOffset
@ -142,8 +128,5 @@ func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) {
m.X = int(v[1]) - byteOffset - 1
m.Y = int(v[2]) - byteOffset - 1
r = append(r, m)
}
return r, nil
return m
}

View File

@ -122,172 +122,143 @@ 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{
{
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{
{
expected: MouseEvent{
X: 222,
Y: 222,
Type: MouseLeft,
},
},
},
// Simple.
{
name: "left",
buf: encode(0b0000_0000, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseLeft,
},
},
},
{
name: "left in motion",
buf: encode(0b0010_0000, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseLeft,
},
},
},
{
name: "middle",
buf: encode(0b0000_0001, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseMiddle,
},
},
},
{
name: "middle in motion",
buf: encode(0b0010_0001, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseMiddle,
},
},
},
{
name: "right",
buf: encode(0b0000_0010, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseRight,
},
},
},
{
name: "right in motion",
buf: encode(0b0010_0010, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseRight,
},
},
},
{
name: "motion",
buf: encode(0b0010_0011, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseMotion,
},
},
},
{
name: "wheel up",
buf: encode(0b0100_0000, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseWheelUp,
},
},
},
{
name: "wheel down",
buf: encode(0b0100_0001, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseWheelDown,
},
},
},
{
name: "release",
buf: encode(0b0000_0011, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseRelease,
},
},
},
// Combinations.
{
name: "alt+right",
buf: encode(0b0010_1010, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseRight,
Alt: true,
},
},
},
{
name: "ctrl+right",
buf: encode(0b0011_0010, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseRight,
Ctrl: true,
},
},
},
{
name: "ctrl+alt+right",
buf: encode(0b0011_1010, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseRight,
@ -295,36 +266,30 @@ func TestParseX10MouseEvent(t *testing.T) {
Ctrl: true,
},
},
},
{
name: "alt+wheel down",
buf: encode(0b0100_1001, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseWheelDown,
Alt: true,
},
},
},
{
name: "ctrl+wheel down",
buf: encode(0b0101_0001, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseWheelDown,
Ctrl: true,
},
},
},
{
name: "ctrl+alt+wheel down",
buf: encode(0b0101_1001, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseWheelDown,
@ -332,117 +297,50 @@ func TestParseX10MouseEvent(t *testing.T) {
Ctrl: true,
},
},
},
// Unknown.
{
name: "wheel with unknown bit",
buf: encode(0b0100_0010, 32, 16),
expected: []MouseEvent{
{
expected: MouseEvent{
X: 32,
Y: 16,
Type: MouseUnknown,
},
},
},
{
name: "unknown with modifier",
buf: encode(0b0100_1010, 32, 16),
expected: []MouseEvent{
{
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{
{
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,
},
},
},
}
for i := range tt {
tc := tt[i]
t.Run(tc.name, func(t *testing.T) {
actual, err := parseX10MouseEvents(tc.buf)
if err != nil {
t.Fatalf("unexpected error for test: %v",
err,
)
}
actual := parseX10MouseEvent(tc.buf)
for i := range tc.expected {
if tc.expected[i] != actual[i] {
if tc.expected != actual {
t.Fatalf("expected %#v but got %#v",
tc.expected[i],
actual[i],
tc.expected,
actual,
)
}
}
})
}
}
func TestParseX10MouseEvent_error(t *testing.T) {
tt := []struct {
name string
buf []byte
}{
{
name: "empty buf",
buf: nil,
},
{
name: "wrong high bit",
buf: []byte("\x1a[M@A1"),
},
{
name: "short buf",
buf: []byte("\x1b[M@A"),
},
{
name: "long buf",
buf: []byte("\x1b[M@A11"),
},
}
for i := range tt {
tc := tt[i]
t.Run(tc.name, func(t *testing.T) {
_, err := parseX10MouseEvents(tc.buf)
if err == nil {
t.Fatalf("expected error but got nil")
}
})
}
}