forked from Mirrors/bubbletea
fix(key),test: simplify the input analysis code
This commit is contained in:
parent
c0cc6aa1fb
commit
d9c675138c
368
key.go
368
key.go
|
@ -1,8 +1,9 @@
|
||||||
package tea
|
package tea
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"regexp"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/mattn/go-localereader"
|
"github.com/mattn/go-localereader"
|
||||||
|
@ -338,85 +339,73 @@ var keyNames = map[KeyType]string{
|
||||||
// Sequence mappings.
|
// Sequence mappings.
|
||||||
var sequences = map[string]Key{
|
var sequences = map[string]Key{
|
||||||
// Arrow keys
|
// Arrow keys
|
||||||
"\x1b[A": {Type: KeyUp},
|
"\x1b[A": {Type: KeyUp},
|
||||||
"\x1b[B": {Type: KeyDown},
|
"\x1b[B": {Type: KeyDown},
|
||||||
"\x1b[C": {Type: KeyRight},
|
"\x1b[C": {Type: KeyRight},
|
||||||
"\x1b[D": {Type: KeyLeft},
|
"\x1b[D": {Type: KeyLeft},
|
||||||
"\x1b[1;2A": {Type: KeyShiftUp},
|
"\x1b[1;2A": {Type: KeyShiftUp},
|
||||||
"\x1b[1;2B": {Type: KeyShiftDown},
|
"\x1b[1;2B": {Type: KeyShiftDown},
|
||||||
"\x1b[1;2C": {Type: KeyShiftRight},
|
"\x1b[1;2C": {Type: KeyShiftRight},
|
||||||
"\x1b[1;2D": {Type: KeyShiftLeft},
|
"\x1b[1;2D": {Type: KeyShiftLeft},
|
||||||
"\x1b[OA": {Type: KeyShiftUp}, // DECCKM
|
"\x1b[OA": {Type: KeyShiftUp}, // DECCKM
|
||||||
"\x1b[OB": {Type: KeyShiftDown}, // DECCKM
|
"\x1b[OB": {Type: KeyShiftDown}, // DECCKM
|
||||||
"\x1b[OC": {Type: KeyShiftRight}, // DECCKM
|
"\x1b[OC": {Type: KeyShiftRight}, // DECCKM
|
||||||
"\x1b[OD": {Type: KeyShiftLeft}, // DECCKM
|
"\x1b[OD": {Type: KeyShiftLeft}, // DECCKM
|
||||||
"\x1b[a": {Type: KeyShiftUp}, // urxvt
|
"\x1b[a": {Type: KeyShiftUp}, // urxvt
|
||||||
"\x1b[b": {Type: KeyShiftDown}, // urxvt
|
"\x1b[b": {Type: KeyShiftDown}, // urxvt
|
||||||
"\x1b[c": {Type: KeyShiftRight}, // urxvt
|
"\x1b[c": {Type: KeyShiftRight}, // urxvt
|
||||||
"\x1b[d": {Type: KeyShiftLeft}, // urxvt
|
"\x1b[d": {Type: KeyShiftLeft}, // urxvt
|
||||||
"\x1b[1;3A": {Type: KeyUp, Alt: true},
|
"\x1b[1;3A": {Type: KeyUp, Alt: true},
|
||||||
"\x1b[1;3B": {Type: KeyDown, Alt: true},
|
"\x1b[1;3B": {Type: KeyDown, Alt: true},
|
||||||
"\x1b[1;3C": {Type: KeyRight, Alt: true},
|
"\x1b[1;3C": {Type: KeyRight, Alt: true},
|
||||||
"\x1b[1;3D": {Type: KeyLeft, 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[1;4A": {Type: KeyShiftUp, Alt: true},
|
||||||
"\x1b\x1b[C": {Type: KeyRight, Alt: true}, // urxvt
|
"\x1b[1;4B": {Type: KeyShiftDown, Alt: true},
|
||||||
"\x1b\x1b[D": {Type: KeyLeft, Alt: true}, // urxvt
|
"\x1b[1;4C": {Type: KeyShiftRight, Alt: true},
|
||||||
"\x1b[1;4A": {Type: KeyShiftUp, Alt: true},
|
"\x1b[1;4D": {Type: KeyShiftLeft, Alt: true},
|
||||||
"\x1b[1;4B": {Type: KeyShiftDown, Alt: true},
|
|
||||||
"\x1b[1;4C": {Type: KeyShiftRight, Alt: true},
|
"\x1b[1;5A": {Type: KeyCtrlUp},
|
||||||
"\x1b[1;4D": {Type: KeyShiftLeft, Alt: true},
|
"\x1b[1;5B": {Type: KeyCtrlDown},
|
||||||
"\x1b\x1b[a": {Type: KeyShiftUp, Alt: true}, // urxvt
|
"\x1b[1;5C": {Type: KeyCtrlRight},
|
||||||
"\x1b\x1b[b": {Type: KeyShiftDown, Alt: true}, // urxvt
|
"\x1b[1;5D": {Type: KeyCtrlLeft},
|
||||||
"\x1b\x1b[c": {Type: KeyShiftRight, Alt: true}, // urxvt
|
"\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt
|
||||||
"\x1b\x1b[d": {Type: KeyShiftLeft, Alt: true}, // urxvt
|
"\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt
|
||||||
"\x1b[1;5A": {Type: KeyCtrlUp},
|
"\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt
|
||||||
"\x1b[1;5B": {Type: KeyCtrlDown},
|
"\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt
|
||||||
"\x1b[1;5C": {Type: KeyCtrlRight},
|
"\x1b[1;6A": {Type: KeyCtrlShiftUp},
|
||||||
"\x1b[1;5D": {Type: KeyCtrlLeft},
|
"\x1b[1;6B": {Type: KeyCtrlShiftDown},
|
||||||
"\x1b[Oa": {Type: KeyCtrlUp, Alt: true}, // urxvt
|
"\x1b[1;6C": {Type: KeyCtrlShiftRight},
|
||||||
"\x1b[Ob": {Type: KeyCtrlDown, Alt: true}, // urxvt
|
"\x1b[1;6D": {Type: KeyCtrlShiftLeft},
|
||||||
"\x1b[Oc": {Type: KeyCtrlRight, Alt: true}, // urxvt
|
"\x1b[1;7A": {Type: KeyCtrlUp, Alt: true},
|
||||||
"\x1b[Od": {Type: KeyCtrlLeft, Alt: true}, // urxvt
|
"\x1b[1;7B": {Type: KeyCtrlDown, Alt: true},
|
||||||
"\x1b[1;6A": {Type: KeyCtrlShiftUp},
|
"\x1b[1;7C": {Type: KeyCtrlRight, Alt: true},
|
||||||
"\x1b[1;6B": {Type: KeyCtrlShiftDown},
|
"\x1b[1;7D": {Type: KeyCtrlLeft, Alt: true},
|
||||||
"\x1b[1;6C": {Type: KeyCtrlShiftRight},
|
"\x1b[1;8A": {Type: KeyCtrlShiftUp, Alt: true},
|
||||||
"\x1b[1;6D": {Type: KeyCtrlShiftLeft},
|
"\x1b[1;8B": {Type: KeyCtrlShiftDown, Alt: true},
|
||||||
"\x1b[1;7A": {Type: KeyCtrlUp, Alt: true},
|
"\x1b[1;8C": {Type: KeyCtrlShiftRight, Alt: true},
|
||||||
"\x1b[1;7B": {Type: KeyCtrlDown, Alt: true},
|
"\x1b[1;8D": {Type: KeyCtrlShiftLeft, 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
|
// Miscellaneous keys
|
||||||
"\x1b[Z": {Type: KeyShiftTab},
|
"\x1b[Z": {Type: KeyShiftTab},
|
||||||
|
|
||||||
"\x1b[2~": {Type: KeyInsert},
|
"\x1b[2~": {Type: KeyInsert},
|
||||||
"\x1b[3;2~": {Type: KeyInsert, Alt: true},
|
"\x1b[3;2~": {Type: KeyInsert, Alt: true},
|
||||||
"\x1b\x1b[2~": {Type: KeyInsert, Alt: true}, // urxvt
|
|
||||||
|
|
||||||
"\x1b[3~": {Type: KeyDelete},
|
"\x1b[3~": {Type: KeyDelete},
|
||||||
"\x1b[3;3~": {Type: KeyDelete, Alt: true},
|
"\x1b[3;3~": {Type: KeyDelete, Alt: true},
|
||||||
"\x1b\x1b[3~": {Type: KeyDelete, Alt: true}, // urxvt
|
|
||||||
|
|
||||||
"\x1b[5~": {Type: KeyPgUp},
|
"\x1b[5~": {Type: KeyPgUp},
|
||||||
"\x1b[5;3~": {Type: KeyPgUp, Alt: true},
|
"\x1b[5;3~": {Type: KeyPgUp, Alt: true},
|
||||||
"\x1b\x1b[5~": {Type: KeyPgUp, Alt: true}, // urxvt
|
"\x1b[5;5~": {Type: KeyCtrlPgUp},
|
||||||
"\x1b[5;5~": {Type: KeyCtrlPgUp},
|
"\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt
|
||||||
"\x1b[5^": {Type: KeyCtrlPgUp}, // urxvt
|
"\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true},
|
||||||
"\x1b[5;7~": {Type: KeyCtrlPgUp, Alt: true},
|
|
||||||
"\x1b\x1b[5^": {Type: KeyCtrlPgUp, Alt: true}, // urxvt
|
|
||||||
|
|
||||||
"\x1b[6~": {Type: KeyPgDown},
|
"\x1b[6~": {Type: KeyPgDown},
|
||||||
"\x1b[6;3~": {Type: KeyPgDown, Alt: true},
|
"\x1b[6;3~": {Type: KeyPgDown, Alt: true},
|
||||||
"\x1b\x1b[6~": {Type: KeyPgDown, Alt: true}, // urxvt
|
"\x1b[6;5~": {Type: KeyCtrlPgDown},
|
||||||
"\x1b[6;5~": {Type: KeyCtrlPgDown},
|
"\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt
|
||||||
"\x1b[6^": {Type: KeyCtrlPgDown}, // urxvt
|
"\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true},
|
||||||
"\x1b[6;7~": {Type: KeyCtrlPgDown, Alt: true},
|
|
||||||
"\x1b\x1b[6^": {Type: KeyCtrlPgDown, Alt: true}, // urxvt
|
|
||||||
|
|
||||||
"\x1b[1~": {Type: KeyHome},
|
"\x1b[1~": {Type: KeyHome},
|
||||||
"\x1b[H": {Type: KeyHome}, // xterm, lxterm
|
"\x1b[H": {Type: KeyHome}, // xterm, lxterm
|
||||||
|
@ -438,23 +427,15 @@ var sequences = map[string]Key{
|
||||||
"\x1b[1;6F": {Type: KeyCtrlShiftEnd}, // xterm, lxterm
|
"\x1b[1;6F": {Type: KeyCtrlShiftEnd}, // xterm, lxterm
|
||||||
"\x1b[1;8F": {Type: KeyCtrlShiftEnd, Alt: true}, // xterm, lxterm
|
"\x1b[1;8F": {Type: KeyCtrlShiftEnd, Alt: true}, // xterm, lxterm
|
||||||
|
|
||||||
"\x1b[7~": {Type: KeyHome}, // urxvt
|
"\x1b[7~": {Type: KeyHome}, // urxvt
|
||||||
"\x1b\x1b[7~": {Type: KeyHome, Alt: true}, // urxvt
|
"\x1b[7^": {Type: KeyCtrlHome}, // urxvt
|
||||||
"\x1b[7^": {Type: KeyCtrlHome}, // urxvt
|
"\x1b[7$": {Type: KeyShiftHome}, // urxvt
|
||||||
"\x1b\x1b[7^": {Type: KeyCtrlHome, Alt: true}, // urxvt
|
"\x1b[7@": {Type: KeyCtrlShiftHome}, // 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[8~": {Type: KeyEnd}, // urxvt
|
||||||
"\x1b\x1b[8~": {Type: KeyEnd, Alt: true}, // urxvt
|
"\x1b[8^": {Type: KeyCtrlEnd}, // urxvt
|
||||||
"\x1b[8^": {Type: KeyCtrlEnd}, // urxvt
|
"\x1b[8$": {Type: KeyShiftEnd}, // urxvt
|
||||||
"\x1b\x1b[8^": {Type: KeyCtrlEnd, Alt: true}, // urxvt
|
"\x1b[8@": {Type: KeyCtrlShiftEnd}, // 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
|
// Function keys, Linux console
|
||||||
"\x1b[[A": {Type: KeyF1}, // linux console
|
"\x1b[[A": {Type: KeyF1}, // linux console
|
||||||
|
@ -479,29 +460,16 @@ var sequences = map[string]Key{
|
||||||
"\x1b[13~": {Type: KeyF3}, // urxvt
|
"\x1b[13~": {Type: KeyF3}, // urxvt
|
||||||
"\x1b[14~": {Type: KeyF4}, // 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~": {Type: KeyF5}, // vt100, xterm, also urxvt
|
||||||
|
|
||||||
"\x1b[15;3~": {Type: KeyF5, Alt: true}, // 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[17~": {Type: KeyF6}, // vt100, xterm, also urxvt
|
||||||
"\x1b[18~": {Type: KeyF7}, // vt100, xterm, also urxvt
|
"\x1b[18~": {Type: KeyF7}, // vt100, xterm, also urxvt
|
||||||
"\x1b[19~": {Type: KeyF8}, // vt100, xterm, also urxvt
|
"\x1b[19~": {Type: KeyF8}, // vt100, xterm, also urxvt
|
||||||
"\x1b[20~": {Type: KeyF9}, // vt100, xterm, also urxvt
|
"\x1b[20~": {Type: KeyF9}, // vt100, xterm, also urxvt
|
||||||
"\x1b[21~": {Type: KeyF10}, // 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[17;3~": {Type: KeyF6, Alt: true}, // vt100, xterm
|
||||||
"\x1b[18;3~": {Type: KeyF7, Alt: true}, // vt100, xterm
|
"\x1b[18;3~": {Type: KeyF7, Alt: true}, // vt100, xterm
|
||||||
"\x1b[19;3~": {Type: KeyF8, 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[23;3~": {Type: KeyF11, Alt: true}, // vt100, xterm
|
||||||
"\x1b[24;3~": {Type: KeyF12, 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;2P": {Type: KeyF13},
|
||||||
"\x1b[1;2Q": {Type: KeyF14},
|
"\x1b[1;2Q": {Type: KeyF14},
|
||||||
|
|
||||||
|
@ -526,9 +491,6 @@ var sequences = map[string]Key{
|
||||||
"\x1b[25;3~": {Type: KeyF13, Alt: true}, // vt100, xterm
|
"\x1b[25;3~": {Type: KeyF13, Alt: true}, // vt100, xterm
|
||||||
"\x1b[26;3~": {Type: KeyF14, 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;2R": {Type: KeyF15},
|
||||||
"\x1b[1;2S": {Type: KeyF16},
|
"\x1b[1;2S": {Type: KeyF16},
|
||||||
|
|
||||||
|
@ -538,9 +500,6 @@ var sequences = map[string]Key{
|
||||||
"\x1b[28;3~": {Type: KeyF15, Alt: true}, // vt100, xterm
|
"\x1b[28;3~": {Type: KeyF15, Alt: true}, // vt100, xterm
|
||||||
"\x1b[29;3~": {Type: KeyF16, 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[15;2~": {Type: KeyF17},
|
||||||
"\x1b[17;2~": {Type: KeyF18},
|
"\x1b[17;2~": {Type: KeyF18},
|
||||||
"\x1b[18;2~": {Type: KeyF19},
|
"\x1b[18;2~": {Type: KeyF19},
|
||||||
|
@ -551,11 +510,6 @@ var sequences = map[string]Key{
|
||||||
"\x1b[33~": {Type: KeyF19},
|
"\x1b[33~": {Type: KeyF19},
|
||||||
"\x1b[34~": {Type: KeyF20},
|
"\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.
|
// Powershell sequences.
|
||||||
"\x1bOA": {Type: KeyUp, Alt: false},
|
"\x1bOA": {Type: KeyUp, Alt: false},
|
||||||
"\x1bOB": {Type: KeyDown, Alt: false},
|
"\x1bOB": {Type: KeyDown, Alt: false},
|
||||||
|
@ -563,102 +517,118 @@ var sequences = map[string]Key{
|
||||||
"\x1bOD": {Type: KeyLeft, Alt: false},
|
"\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
|
// readInputs reads keypress and mouse inputs from a TTY and returns messages
|
||||||
// containing information about the key or mouse events accordingly.
|
// containing information about the key or mouse events accordingly.
|
||||||
func readInputs(input io.Reader) ([]Msg, error) {
|
func readInputs(input io.Reader) ([]Msg, error) {
|
||||||
var buf [256]byte
|
var buf [256]byte
|
||||||
|
|
||||||
|
input = localereader.NewReader(input)
|
||||||
|
|
||||||
// Read and block
|
// Read and block
|
||||||
numBytes, err := input.Read(buf[:])
|
numBytes, err := input.Read(buf[:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
b := buf[:numBytes]
|
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
|
var msgs []Msg
|
||||||
for _, runes := range runeSets {
|
for i, w := 0, 0; i < len(b); i += w {
|
||||||
// Is it a sequence, like an arrow key?
|
var msg Msg
|
||||||
if k, ok := sequences[string(runes)]; ok {
|
w, msg = detectOneMsg(b[i:])
|
||||||
msgs = append(msgs, KeyMsg(k))
|
msgs = append(msgs, msg)
|
||||||
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}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return msgs, nil
|
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])
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
457
key_test.go
457
key_test.go
|
@ -2,8 +2,15 @@ package tea
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestKeyString(t *testing.T) {
|
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) {
|
func TestReadInput(t *testing.T) {
|
||||||
type test struct {
|
type test struct {
|
||||||
keyname string
|
keyname string
|
||||||
in []byte
|
in []byte
|
||||||
out []Msg
|
out []Msg
|
||||||
}
|
}
|
||||||
for i, td := range []test{
|
testData := []test{
|
||||||
{"a",
|
{"a",
|
||||||
[]byte{'a'},
|
[]byte{'a'},
|
||||||
[]Msg{
|
[]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",
|
{"ctrl+a",
|
||||||
[]byte{byte(keySOH)},
|
[]byte{byte(keySOH)},
|
||||||
[]Msg{
|
[]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",
|
{"alt+a",
|
||||||
[]byte{byte(0x1b), 'a'},
|
[]byte{byte(0x1b), 'a'},
|
||||||
[]Msg{
|
[]Msg{
|
||||||
|
@ -96,19 +277,7 @@ func TestReadInput(t *testing.T) {
|
||||||
[]Msg{
|
[]Msg{
|
||||||
KeyMsg{
|
KeyMsg{
|
||||||
Type: KeyRunes,
|
Type: KeyRunes,
|
||||||
Runes: []rune{'a'},
|
Runes: []rune{'a', 'b', 'c', 'd'},
|
||||||
},
|
|
||||||
KeyMsg{
|
|
||||||
Type: KeyRunes,
|
|
||||||
Runes: []rune{'b'},
|
|
||||||
},
|
|
||||||
KeyMsg{
|
|
||||||
Type: KeyRunes,
|
|
||||||
Runes: []rune{'c'},
|
|
||||||
},
|
|
||||||
KeyMsg{
|
|
||||||
Type: KeyRunes,
|
|
||||||
Runes: []rune{'d'},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -124,10 +293,30 @@ func TestReadInput(t *testing.T) {
|
||||||
[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
|
[]byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)},
|
||||||
[]Msg{
|
[]Msg{
|
||||||
MouseMsg{
|
MouseMsg{
|
||||||
|
X: 32,
|
||||||
|
Y: 16,
|
||||||
Type: MouseWheelUp,
|
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",
|
{"shift+tab",
|
||||||
[]byte{'\x1b', '[', 'Z'},
|
[]byte{'\x1b', '[', 'Z'},
|
||||||
[]Msg{
|
[]Msg{
|
||||||
|
@ -136,6 +325,10 @@ func TestReadInput(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{"enter",
|
||||||
|
[]byte{'\r'},
|
||||||
|
[]Msg{KeyMsg{Type: KeyEnter}},
|
||||||
|
},
|
||||||
{"alt+enter",
|
{"alt+enter",
|
||||||
[]byte{'\x1b', '\r'},
|
[]byte{'\x1b', '\r'},
|
||||||
[]Msg{
|
[]Msg{
|
||||||
|
@ -162,9 +355,9 @@ func TestReadInput(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{"unrecognized CSI",
|
{"?CSI[45 45 45 45 88]?",
|
||||||
[]byte{'\x1b', '[', '-', '-', '-', '-', 'X'},
|
[]byte{'\x1b', '[', '-', '-', '-', '-', 'X'},
|
||||||
[]Msg{},
|
[]Msg{unknownCSISequenceMsg([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'})},
|
||||||
},
|
},
|
||||||
// Powershell sequences.
|
// Powershell sequences.
|
||||||
{"up",
|
{"up",
|
||||||
|
@ -191,34 +384,236 @@ func TestReadInput(t *testing.T) {
|
||||||
[]byte{'\x1b', '\x7f'},
|
[]byte{'\x1b', '\x7f'},
|
||||||
[]Msg{KeyMsg{Type: KeyBackspace, Alt: true}},
|
[]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) {
|
t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) {
|
||||||
msgs, err := readInputs(bytes.NewReader(td.in))
|
msgs, err := readInputs(bytes.NewReader(td.in))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
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) {
|
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 !reflect.DeepEqual(td.out, msgs) {
|
||||||
if m, ok := msgs[0].(KeyMsg); ok && m.String() != td.keyname {
|
t.Fatalf("expected:\n%#v\ngot:\n%#v", td.out, msgs)
|
||||||
t.Fatalf(`expected a keymsg %q, got %q`, td.keyname, m)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i, v := range msgs {
|
// randTest defines the test input and expected output for a sequence
|
||||||
if m, ok := v.(KeyMsg); ok &&
|
// of interleaved control sequences and control characters.
|
||||||
m.String() != td.out[i].(KeyMsg).String() {
|
type randTest struct {
|
||||||
t.Fatalf(`expected a keymsg %q, got %q`, td.out[i].(KeyMsg), m)
|
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 &&
|
if width != td.lengths[tn] {
|
||||||
(mouseEventTypes[m.Type] != td.keyname || m.Type != td.out[i].(MouseMsg).Type) {
|
t.Errorf("at %d (ev %d): expected width %d, got %d", i, tn, td.lengths[tn], width)
|
||||||
t.Fatalf(`expected a mousemsg %q, got %q`,
|
}
|
||||||
td.keyname,
|
w = width
|
||||||
mouseEventTypes[td.out[i].(MouseMsg).Type])
|
|
||||||
|
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:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
141
mouse.go
141
mouse.go
|
@ -1,15 +1,15 @@
|
||||||
package tea
|
package tea
|
||||||
|
|
||||||
import (
|
// MouseMsg contains information about a mouse event and are sent to a programs
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MouseMsg contains information about a mouse event and is sent to a program's
|
|
||||||
// update function when mouse activity occurs. Note that the mouse must first
|
// update function when mouse activity occurs. Note that the mouse must first
|
||||||
// be enabled in order for the mouse events to be received.
|
// be enabled in order for the mouse events to be received.
|
||||||
type MouseMsg MouseEvent
|
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
|
// MouseEvent represents a mouse event, which could be a click, a scroll wheel
|
||||||
// movement, a cursor movement, or a combination.
|
// movement, a cursor movement, or a combination.
|
||||||
type MouseEvent struct {
|
type MouseEvent struct {
|
||||||
|
@ -66,84 +66,67 @@ var mouseEventTypes = map[MouseEventType]string{
|
||||||
// ESC [M Cb Cx Cy
|
// ESC [M Cb Cx Cy
|
||||||
//
|
//
|
||||||
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
|
// See: http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
|
||||||
func parseX10MouseEvents(buf []byte) ([]MouseEvent, error) {
|
func parseX10MouseEvent(buf []byte) MouseEvent {
|
||||||
var r []MouseEvent
|
v := buf[3:6]
|
||||||
|
var m MouseEvent
|
||||||
|
const byteOffset = 32
|
||||||
|
e := v[0] - byteOffset
|
||||||
|
|
||||||
seq := []byte("\x1b[M")
|
const (
|
||||||
if !bytes.Contains(buf, seq) {
|
bitShift = 0b0000_0100
|
||||||
return r, errors.New("not an X10 mouse event")
|
bitAlt = 0b0000_1000
|
||||||
}
|
bitCtrl = 0b0001_0000
|
||||||
|
bitMotion = 0b0010_0000
|
||||||
|
bitWheel = 0b0100_0000
|
||||||
|
|
||||||
for _, v := range bytes.Split(buf, seq) {
|
bitsMask = 0b0000_0011
|
||||||
if len(v) == 0 {
|
|
||||||
continue
|
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 {
|
} else {
|
||||||
return r, errors.New("not an X10 mouse event")
|
// Check the low two bits.
|
||||||
}
|
// We do not separate clicking and dragging.
|
||||||
|
switch e & bitsMask {
|
||||||
var m MouseEvent
|
case bitsLeft:
|
||||||
const byteOffset = 32
|
m.Type = MouseLeft
|
||||||
e := v[0] - byteOffset
|
case bitsMiddle:
|
||||||
|
m.Type = MouseMiddle
|
||||||
const (
|
case bitsRight:
|
||||||
bitShift = 0b0000_0100
|
m.Type = MouseRight
|
||||||
bitAlt = 0b0000_1000
|
case bitsRelease:
|
||||||
bitCtrl = 0b0001_0000
|
if e&bitMotion != 0 {
|
||||||
bitMotion = 0b0010_0000
|
m.Type = MouseMotion
|
||||||
bitWheel = 0b0100_0000
|
} else {
|
||||||
|
m.Type = MouseRelease
|
||||||
bitsMask = 0b0000_0011
|
|
||||||
|
|
||||||
bitsLeft = 0b0000_0000
|
|
||||||
bitsMiddle = 0b0000_0001
|
|
||||||
bitsRight = 0b0000_0010
|
|
||||||
bitsRelease = 0b0000_0011
|
|
||||||
|
|
||||||
bitsWheelUp = 0b0000_0000
|
|
||||||
bitsWheelDown = 0b0000_0001
|
|
||||||
)
|
|
||||||
|
|
||||||
if e&bitWheel != 0 {
|
|
||||||
// Check the low two bits.
|
|
||||||
switch e & bitsMask {
|
|
||||||
case bitsWheelUp:
|
|
||||||
m.Type = MouseWheelUp
|
|
||||||
case bitsWheelDown:
|
|
||||||
m.Type = MouseWheelDown
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Check the low two bits.
|
|
||||||
// We do not separate clicking and dragging.
|
|
||||||
switch e & bitsMask {
|
|
||||||
case bitsLeft:
|
|
||||||
m.Type = MouseLeft
|
|
||||||
case bitsMiddle:
|
|
||||||
m.Type = MouseMiddle
|
|
||||||
case bitsRight:
|
|
||||||
m.Type = MouseRight
|
|
||||||
case bitsRelease:
|
|
||||||
if e&bitMotion != 0 {
|
|
||||||
m.Type = MouseMotion
|
|
||||||
} else {
|
|
||||||
m.Type = MouseRelease
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if e&bitAlt != 0 {
|
|
||||||
m.Alt = true
|
|
||||||
}
|
|
||||||
if e&bitCtrl != 0 {
|
|
||||||
m.Ctrl = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// (1,1) is the upper left. We subtract 1 to normalize it to (0,0).
|
|
||||||
m.X = int(v[1]) - byteOffset - 1
|
|
||||||
m.Y = int(v[2]) - byteOffset - 1
|
|
||||||
|
|
||||||
r = append(r, m)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
302
mouse_test.go
302
mouse_test.go
|
@ -122,268 +122,209 @@ func TestParseX10MouseEvent(t *testing.T) {
|
||||||
tt := []struct {
|
tt := []struct {
|
||||||
name string
|
name string
|
||||||
buf []byte
|
buf []byte
|
||||||
expected []MouseEvent
|
expected MouseEvent
|
||||||
}{
|
}{
|
||||||
// Position.
|
// Position.
|
||||||
{
|
{
|
||||||
name: "zero position",
|
name: "zero position",
|
||||||
buf: encode(0b0010_0000, 0, 0),
|
buf: encode(0b0010_0000, 0, 0),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 0,
|
||||||
X: 0,
|
Y: 0,
|
||||||
Y: 0,
|
Type: MouseLeft,
|
||||||
Type: MouseLeft,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "max position",
|
name: "max position",
|
||||||
buf: encode(0b0010_0000, 222, 222), // Because 255 (max int8) - 32 - 1.
|
buf: encode(0b0010_0000, 222, 222), // Because 255 (max int8) - 32 - 1.
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 222,
|
||||||
X: 222,
|
Y: 222,
|
||||||
Y: 222,
|
Type: MouseLeft,
|
||||||
Type: MouseLeft,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Simple.
|
// Simple.
|
||||||
{
|
{
|
||||||
name: "left",
|
name: "left",
|
||||||
buf: encode(0b0000_0000, 32, 16),
|
buf: encode(0b0000_0000, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseLeft,
|
||||||
Type: MouseLeft,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "left in motion",
|
name: "left in motion",
|
||||||
buf: encode(0b0010_0000, 32, 16),
|
buf: encode(0b0010_0000, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseLeft,
|
||||||
Type: MouseLeft,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "middle",
|
name: "middle",
|
||||||
buf: encode(0b0000_0001, 32, 16),
|
buf: encode(0b0000_0001, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseMiddle,
|
||||||
Type: MouseMiddle,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "middle in motion",
|
name: "middle in motion",
|
||||||
buf: encode(0b0010_0001, 32, 16),
|
buf: encode(0b0010_0001, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseMiddle,
|
||||||
Type: MouseMiddle,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "right",
|
name: "right",
|
||||||
buf: encode(0b0000_0010, 32, 16),
|
buf: encode(0b0000_0010, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseRight,
|
||||||
Type: MouseRight,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "right in motion",
|
name: "right in motion",
|
||||||
buf: encode(0b0010_0010, 32, 16),
|
buf: encode(0b0010_0010, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseRight,
|
||||||
Type: MouseRight,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "motion",
|
name: "motion",
|
||||||
buf: encode(0b0010_0011, 32, 16),
|
buf: encode(0b0010_0011, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseMotion,
|
||||||
Type: MouseMotion,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wheel up",
|
name: "wheel up",
|
||||||
buf: encode(0b0100_0000, 32, 16),
|
buf: encode(0b0100_0000, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseWheelUp,
|
||||||
Type: MouseWheelUp,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "wheel down",
|
name: "wheel down",
|
||||||
buf: encode(0b0100_0001, 32, 16),
|
buf: encode(0b0100_0001, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseWheelDown,
|
||||||
Type: MouseWheelDown,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "release",
|
name: "release",
|
||||||
buf: encode(0b0000_0011, 32, 16),
|
buf: encode(0b0000_0011, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseRelease,
|
||||||
Type: MouseRelease,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Combinations.
|
// Combinations.
|
||||||
{
|
{
|
||||||
name: "alt+right",
|
name: "alt+right",
|
||||||
buf: encode(0b0010_1010, 32, 16),
|
buf: encode(0b0010_1010, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseRight,
|
||||||
Type: MouseRight,
|
Alt: true,
|
||||||
Alt: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ctrl+right",
|
name: "ctrl+right",
|
||||||
buf: encode(0b0011_0010, 32, 16),
|
buf: encode(0b0011_0010, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseRight,
|
||||||
Type: MouseRight,
|
Ctrl: true,
|
||||||
Ctrl: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ctrl+alt+right",
|
name: "ctrl+alt+right",
|
||||||
buf: encode(0b0011_1010, 32, 16),
|
buf: encode(0b0011_1010, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseRight,
|
||||||
Type: MouseRight,
|
Alt: true,
|
||||||
Alt: true,
|
Ctrl: true,
|
||||||
Ctrl: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "alt+wheel down",
|
name: "alt+wheel down",
|
||||||
buf: encode(0b0100_1001, 32, 16),
|
buf: encode(0b0100_1001, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseWheelDown,
|
||||||
Type: MouseWheelDown,
|
Alt: true,
|
||||||
Alt: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ctrl+wheel down",
|
name: "ctrl+wheel down",
|
||||||
buf: encode(0b0101_0001, 32, 16),
|
buf: encode(0b0101_0001, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseWheelDown,
|
||||||
Type: MouseWheelDown,
|
Ctrl: true,
|
||||||
Ctrl: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ctrl+alt+wheel down",
|
name: "ctrl+alt+wheel down",
|
||||||
buf: encode(0b0101_1001, 32, 16),
|
buf: encode(0b0101_1001, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseWheelDown,
|
||||||
Type: MouseWheelDown,
|
Alt: true,
|
||||||
Alt: true,
|
Ctrl: true,
|
||||||
Ctrl: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Unknown.
|
// Unknown.
|
||||||
{
|
{
|
||||||
name: "wheel with unknown bit",
|
name: "wheel with unknown bit",
|
||||||
buf: encode(0b0100_0010, 32, 16),
|
buf: encode(0b0100_0010, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseUnknown,
|
||||||
Type: MouseUnknown,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unknown with modifier",
|
name: "unknown with modifier",
|
||||||
buf: encode(0b0100_1010, 32, 16),
|
buf: encode(0b0100_1010, 32, 16),
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: 32,
|
||||||
X: 32,
|
Y: 16,
|
||||||
Y: 16,
|
Type: MouseUnknown,
|
||||||
Type: MouseUnknown,
|
Alt: true,
|
||||||
Alt: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Overflow position.
|
// Overflow position.
|
||||||
{
|
{
|
||||||
name: "overflow position",
|
name: "overflow position",
|
||||||
buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1.
|
buf: encode(0b0010_0000, 250, 223), // Because 255 (max int8) - 32 - 1.
|
||||||
expected: []MouseEvent{
|
expected: MouseEvent{
|
||||||
{
|
X: -6,
|
||||||
X: -6,
|
Y: -33,
|
||||||
Y: -33,
|
Type: MouseLeft,
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -392,57 +333,14 @@ func TestParseX10MouseEvent(t *testing.T) {
|
||||||
tc := tt[i]
|
tc := tt[i]
|
||||||
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
actual, err := parseX10MouseEvents(tc.buf)
|
actual := parseX10MouseEvent(tc.buf)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unexpected error for test: %v",
|
if tc.expected != actual {
|
||||||
err,
|
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")
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue