diff --git a/key.go b/key.go index c2e5e3a..bbd533c 100644 --- a/key.go +++ b/key.go @@ -1,8 +1,9 @@ package tea import ( - "errors" + "fmt" "io" + "regexp" "unicode/utf8" "github.com/mattn/go-localereader" @@ -338,85 +339,73 @@ var keyNames = map[KeyType]string{ // Sequence mappings. var sequences = map[string]Key{ // Arrow keys - "\x1b[A": {Type: KeyUp}, - "\x1b[B": {Type: KeyDown}, - "\x1b[C": {Type: KeyRight}, - "\x1b[D": {Type: KeyLeft}, - "\x1b[1;2A": {Type: KeyShiftUp}, - "\x1b[1;2B": {Type: KeyShiftDown}, - "\x1b[1;2C": {Type: KeyShiftRight}, - "\x1b[1;2D": {Type: KeyShiftLeft}, - "\x1b[OA": {Type: KeyShiftUp}, // DECCKM - "\x1b[OB": {Type: KeyShiftDown}, // DECCKM - "\x1b[OC": {Type: KeyShiftRight}, // DECCKM - "\x1b[OD": {Type: KeyShiftLeft}, // DECCKM - "\x1b[a": {Type: KeyShiftUp}, // urxvt - "\x1b[b": {Type: KeyShiftDown}, // urxvt - "\x1b[c": {Type: KeyShiftRight}, // urxvt - "\x1b[d": {Type: KeyShiftLeft}, // urxvt - "\x1b[1;3A": {Type: KeyUp, Alt: true}, - "\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}, - "\x1b[1;5D": {Type: KeyCtrlLeft}, - "\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt - "\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt - "\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt - "\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt - "\x1b[1;6A": {Type: KeyCtrlShiftUp}, - "\x1b[1;6B": {Type: KeyCtrlShiftDown}, - "\x1b[1;6C": {Type: KeyCtrlShiftRight}, - "\x1b[1;6D": {Type: KeyCtrlShiftLeft}, - "\x1b[1;7A": {Type: KeyCtrlUp, Alt: true}, - "\x1b[1;7B": {Type: KeyCtrlDown, Alt: true}, - "\x1b[1;7C": {Type: KeyCtrlRight, Alt: true}, - "\x1b[1;7D": {Type: KeyCtrlLeft, Alt: true}, - "\x1b[1;8A": {Type: KeyCtrlShiftUp, Alt: true}, - "\x1b[1;8B": {Type: KeyCtrlShiftDown, Alt: true}, - "\x1b[1;8C": {Type: KeyCtrlShiftRight, Alt: true}, - "\x1b[1;8D": {Type: KeyCtrlShiftLeft, Alt: true}, + "\x1b[A": {Type: KeyUp}, + "\x1b[B": {Type: KeyDown}, + "\x1b[C": {Type: KeyRight}, + "\x1b[D": {Type: KeyLeft}, + "\x1b[1;2A": {Type: KeyShiftUp}, + "\x1b[1;2B": {Type: KeyShiftDown}, + "\x1b[1;2C": {Type: KeyShiftRight}, + "\x1b[1;2D": {Type: KeyShiftLeft}, + "\x1b[OA": {Type: KeyShiftUp}, // DECCKM + "\x1b[OB": {Type: KeyShiftDown}, // DECCKM + "\x1b[OC": {Type: KeyShiftRight}, // DECCKM + "\x1b[OD": {Type: KeyShiftLeft}, // DECCKM + "\x1b[a": {Type: KeyShiftUp}, // urxvt + "\x1b[b": {Type: KeyShiftDown}, // urxvt + "\x1b[c": {Type: KeyShiftRight}, // urxvt + "\x1b[d": {Type: KeyShiftLeft}, // urxvt + "\x1b[1;3A": {Type: KeyUp, Alt: true}, + "\x1b[1;3B": {Type: KeyDown, Alt: true}, + "\x1b[1;3C": {Type: KeyRight, Alt: true}, + "\x1b[1;3D": {Type: KeyLeft, Alt: true}, + + "\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[1;5A": {Type: KeyCtrlUp}, + "\x1b[1;5B": {Type: KeyCtrlDown}, + "\x1b[1;5C": {Type: KeyCtrlRight}, + "\x1b[1;5D": {Type: KeyCtrlLeft}, + "\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt + "\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt + "\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt + "\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt + "\x1b[1;6A": {Type: KeyCtrlShiftUp}, + "\x1b[1;6B": {Type: KeyCtrlShiftDown}, + "\x1b[1;6C": {Type: KeyCtrlShiftRight}, + "\x1b[1;6D": {Type: KeyCtrlShiftLeft}, + "\x1b[1;7A": {Type: KeyCtrlUp, Alt: true}, + "\x1b[1;7B": {Type: KeyCtrlDown, Alt: true}, + "\x1b[1;7C": {Type: KeyCtrlRight, Alt: true}, + "\x1b[1;7D": {Type: KeyCtrlLeft, Alt: true}, + "\x1b[1;8A": {Type: KeyCtrlShiftUp, Alt: true}, + "\x1b[1;8B": {Type: KeyCtrlShiftDown, Alt: true}, + "\x1b[1;8C": {Type: KeyCtrlShiftRight, Alt: true}, + "\x1b[1;8D": {Type: KeyCtrlShiftLeft, Alt: true}, // Miscellaneous keys "\x1b[Z": {Type: KeyShiftTab}, - "\x1b[2~": {Type: KeyInsert}, - "\x1b[3;2~": {Type: KeyInsert, Alt: true}, - "\x1b\x1b[2~": {Type: KeyInsert, Alt: true}, // urxvt + "\x1b[2~": {Type: KeyInsert}, + "\x1b[3;2~": {Type: KeyInsert, Alt: true}, - "\x1b[3~": {Type: KeyDelete}, - "\x1b[3;3~": {Type: KeyDelete, Alt: true}, - "\x1b\x1b[3~": {Type: KeyDelete, Alt: true}, // urxvt + "\x1b[3~": {Type: KeyDelete}, + "\x1b[3;3~": {Type: KeyDelete, Alt: true}, - "\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[5~": {Type: KeyPgUp}, + "\x1b[5;3~": {Type: KeyPgUp, Alt: true}, + "\x1b[5;5~": {Type: KeyCtrlPgUp}, + "\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt + "\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true}, - "\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[6~": {Type: KeyPgDown}, + "\x1b[6;3~": {Type: KeyPgDown, Alt: true}, + "\x1b[6;5~": {Type: KeyCtrlPgDown}, + "\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt + "\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true}, "\x1b[1~": {Type: KeyHome}, "\x1b[H": {Type: KeyHome}, // xterm, lxterm @@ -438,23 +427,15 @@ var sequences = map[string]Key{ "\x1b[1;6F": {Type: KeyCtrlShiftEnd}, // xterm, lxterm "\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[7~": {Type: KeyHome}, // urxvt + "\x1b[7^": {Type: KeyCtrlHome}, // urxvt + "\x1b[7$": {Type: KeyShiftHome}, // urxvt + "\x1b[7@": {Type: KeyCtrlShiftHome}, // 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 + "\x1b[8~": {Type: KeyEnd}, // urxvt + "\x1b[8^": {Type: KeyCtrlEnd}, // urxvt + "\x1b[8$": {Type: KeyShiftEnd}, // urxvt + "\x1b[8@": {Type: KeyCtrlShiftEnd}, // 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 - } - - // 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})) - } + for i, w := 0, 0; i < len(b); i += w { + var msg Msg + w, msg = detectOneMsg(b[i:]) + msgs = append(msgs, msg) } - 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]) +} diff --git a/key_sequences.go b/key_sequences.go new file mode 100644 index 0000000..cc200f8 --- /dev/null +++ b/key_sequences.go @@ -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 +} diff --git a/key_test.go b/key_test.go index 07c2743..1af6a3e 100644 --- a/key_test.go +++ b/key_test.go @@ -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 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]) + 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()) + } } } }) } } + +// 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:]) + } + } +} diff --git a/mouse.go b/mouse.go index f918d20..fc66691 100644 --- a/mouse.go +++ b/mouse.go @@ -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,84 +66,67 @@ 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 +func parseX10MouseEvent(buf []byte) MouseEvent { + v := buf[3:6] + var m MouseEvent + const byteOffset = 32 + e := v[0] - byteOffset - seq := []byte("\x1b[M") - if !bytes.Contains(buf, seq) { - return r, errors.New("not an X10 mouse event") - } + const ( + bitShift = 0b0000_0100 + bitAlt = 0b0000_1000 + bitCtrl = 0b0001_0000 + bitMotion = 0b0010_0000 + bitWheel = 0b0100_0000 - for _, v := range bytes.Split(buf, seq) { - if len(v) == 0 { - continue + 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 } - 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 - } + } 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) } - return r, nil + 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 + + return m } diff --git a/mouse_test.go b/mouse_test.go index d9c108f..a64a2e3 100644 --- a/mouse_test.go +++ b/mouse_test.go @@ -122,268 +122,209 @@ 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, - }, - }, - }, - // 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, - }, + expected: MouseEvent{ + X: -6, + Y: -33, + Type: MouseLeft, }, }, } @@ -392,57 +333,14 @@ func TestParseX10MouseEvent(t *testing.T) { 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) + + 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], - ) - } - } - }) - } -} - -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") - } }) } }