package input import ( "bytes" "context" "errors" "flag" "fmt" "image/color" "io" "math/rand" "reflect" "regexp" "runtime" "sort" "strings" "sync" "testing" "time" "github.com/charmbracelet/x/ansi" "github.com/charmbracelet/x/ansi/kitty" ) var sequences = buildKeysTable(FlagTerminfo, "dumb") func TestKeyString(t *testing.T) { t.Run("alt+space", func(t *testing.T) { k := KeyPressEvent{Code: KeySpace, Mod: ModAlt} if got := k.String(); got != "alt+space" { t.Fatalf(`expected a "alt+space", got %q`, got) } }) t.Run("runes", func(t *testing.T) { k := KeyPressEvent{Code: 'a', Text: "a"} if got := k.String(); got != "a" { t.Fatalf(`expected an "a", got %q`, got) } }) t.Run("invalid", func(t *testing.T) { k := KeyPressEvent{Code: 99999} if got := k.String(); got != "𘚟" { t.Fatalf(`expected a "unknown", got %q`, got) } }) t.Run("space", func(t *testing.T) { k := KeyPressEvent{Code: KeySpace, Text: " "} if got := k.String(); got != "space" { t.Fatalf(`expected a "space", got %q`, got) } }) t.Run("shift+space", func(t *testing.T) { k := KeyPressEvent{Code: KeySpace, Mod: ModShift} if got := k.String(); got != "shift+space" { t.Fatalf(`expected a "shift+space", got %q`, got) } }) t.Run("?", func(t *testing.T) { k := KeyPressEvent{Code: '/', Mod: ModShift, Text: "?"} if got := k.String(); got != "?" { t.Fatalf(`expected a "?", got %q`, got) } }) } type seqTest struct { seq []byte Events []Event } var f3CurPosRegexp = regexp.MustCompile(`\x1b\[1;(\d+)R`) // buildBaseSeqTests returns sequence tests that are valid for the // detectSequence() function. func buildBaseSeqTests() []seqTest { td := []seqTest{} for seq, key := range sequences { k := KeyPressEvent(key) st := seqTest{seq: []byte(seq), Events: []Event{k}} // XXX: This is a special case to handle F3 key sequence and cursor // position report having the same sequence. See [parseCsi] for more // information. if f3CurPosRegexp.MatchString(seq) { st.Events = []Event{k, CursorPositionEvent{Y: 0, X: int(key.Mod)}} } td = append(td, st) } // Additional special cases. td = append(td, // Unrecognized CSI sequence. seqTest{ []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, []Event{ UnknownEvent([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'}), }, }, // A lone space character. seqTest{ []byte{' '}, []Event{ KeyPressEvent{Code: KeySpace, Text: " "}, }, }, // An escape character with the alt modifier. seqTest{ []byte{'\x1b', ' '}, []Event{ KeyPressEvent{Code: KeySpace, Mod: ModAlt}, }, }, ) return td } func TestParseSequence(t *testing.T) { td := buildBaseSeqTests() td = append(td, // Background color. seqTest{ []byte("\x1b]11;rgb:1234/1234/1234\x07"), []Event{BackgroundColorEvent{ Color: color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}, }}, }, seqTest{ []byte("\x1b]11;rgb:1234/1234/1234\x1b\\"), []Event{BackgroundColorEvent{ Color: color.RGBA{R: 0x12, G: 0x12, B: 0x12, A: 0xff}, }}, }, seqTest{ []byte("\x1b]11;rgb:1234/1234/1234\x1b"), // Incomplete sequences are ignored. []Event{ UnknownEvent("\x1b]11;rgb:1234/1234/1234\x1b"), }, }, // Kitty Graphics response. seqTest{ []byte("\x1b_Ga=t;OK\x1b\\"), []Event{KittyGraphicsEvent{ Options: kitty.Options{Action: kitty.Transmit}, Payload: []byte("OK"), }}, }, seqTest{ []byte("\x1b_Gi=99,I=13;OK\x1b\\"), []Event{KittyGraphicsEvent{ Options: kitty.Options{ID: 99, Number: 13}, Payload: []byte("OK"), }}, }, seqTest{ []byte("\x1b_Gi=1337,q=1;EINVAL:your face\x1b\\"), []Event{KittyGraphicsEvent{ Options: kitty.Options{ID: 1337, Quite: 1}, Payload: []byte("EINVAL:your face"), }}, }, // Xterm modifyOtherKeys CSI 27 ; ; ~ seqTest{ []byte("\x1b[27;3;20320~"), []Event{KeyPressEvent{Code: '你', Mod: ModAlt}}, }, seqTest{ []byte("\x1b[27;3;65~"), []Event{KeyPressEvent{Code: 'A', Mod: ModAlt}}, }, seqTest{ []byte("\x1b[27;3;8~"), []Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}}, }, seqTest{ []byte("\x1b[27;3;27~"), []Event{KeyPressEvent{Code: KeyEscape, Mod: ModAlt}}, }, seqTest{ []byte("\x1b[27;3;127~"), []Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}}, }, // Xterm report window text area size. seqTest{ []byte("\x1b[4;24;80t"), []Event{ WindowOpEvent{Op: 4, Args: []int{24, 80}}, }, }, // Kitty keyboard / CSI u (fixterms) seqTest{ []byte("\x1b[1B"), []Event{KeyPressEvent{Code: KeyDown}}, }, seqTest{ []byte("\x1b[1;B"), []Event{KeyPressEvent{Code: KeyDown}}, }, seqTest{ []byte("\x1b[1;4B"), []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown}}, }, seqTest{ []byte("\x1b[1;4:1B"), []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown}}, }, seqTest{ []byte("\x1b[1;4:2B"), []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyDown, IsRepeat: true}}, }, seqTest{ []byte("\x1b[1;4:3B"), []Event{KeyReleaseEvent{Mod: ModShift | ModAlt, Code: KeyDown}}, }, seqTest{ []byte("\x1b[8~"), []Event{KeyPressEvent{Code: KeyEnd}}, }, seqTest{ []byte("\x1b[8;~"), []Event{KeyPressEvent{Code: KeyEnd}}, }, seqTest{ []byte("\x1b[8;10~"), []Event{KeyPressEvent{Mod: ModShift | ModMeta, Code: KeyEnd}}, }, seqTest{ []byte("\x1b[27;4u"), []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyEscape}}, }, seqTest{ []byte("\x1b[127;4u"), []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyBackspace}}, }, seqTest{ []byte("\x1b[57358;4u"), []Event{KeyPressEvent{Mod: ModShift | ModAlt, Code: KeyCapsLock}}, }, seqTest{ []byte("\x1b[9;2u"), []Event{KeyPressEvent{Mod: ModShift, Code: KeyTab}}, }, seqTest{ []byte("\x1b[195;u"), []Event{KeyPressEvent{Text: "Ã", Code: 'Ã'}}, }, seqTest{ []byte("\x1b[20320;2u"), []Event{KeyPressEvent{Text: "你", Mod: ModShift, Code: '你'}}, }, seqTest{ []byte("\x1b[195;:1u"), []Event{KeyPressEvent{Text: "Ã", Code: 'Ã'}}, }, seqTest{ []byte("\x1b[195;2:3u"), []Event{KeyReleaseEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}}, }, seqTest{ []byte("\x1b[195;2:2u"), []Event{KeyPressEvent{Code: 'Ã', Text: "Ã", IsRepeat: true, Mod: ModShift}}, }, seqTest{ []byte("\x1b[195;2:1u"), []Event{KeyPressEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}}, }, seqTest{ []byte("\x1b[195;2:3u"), []Event{KeyReleaseEvent{Code: 'Ã', Text: "Ã", Mod: ModShift}}, }, seqTest{ []byte("\x1b[97;2;65u"), []Event{KeyPressEvent{Code: 'a', Text: "A", Mod: ModShift}}, }, seqTest{ []byte("\x1b[97;;229u"), []Event{KeyPressEvent{Code: 'a', Text: "å"}}, }, // focus/blur seqTest{ []byte{'\x1b', '[', 'I'}, []Event{ FocusEvent{}, }, }, seqTest{ []byte{'\x1b', '[', 'O'}, []Event{ BlurEvent{}, }, }, // Mouse event. seqTest{ []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, []Event{ MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelUp}, }, }, // SGR Mouse event. seqTest{ []byte("\x1b[<0;33;17M"), []Event{ MouseClickEvent{X: 32, Y: 16, Button: MouseLeft}, }, }, // Runes. seqTest{ []byte{'a'}, []Event{ KeyPressEvent{Code: 'a', Text: "a"}, }, }, seqTest{ []byte{'\x1b', 'a'}, []Event{ KeyPressEvent{Code: 'a', Mod: ModAlt}, }, }, seqTest{ []byte{'a', 'a', 'a'}, []Event{ KeyPressEvent{Code: 'a', Text: "a"}, KeyPressEvent{Code: 'a', Text: "a"}, KeyPressEvent{Code: 'a', Text: "a"}, }, }, // Multi-byte rune. seqTest{ []byte("☃"), []Event{ KeyPressEvent{Code: '☃', Text: "☃"}, }, }, seqTest{ []byte("\x1b☃"), []Event{ KeyPressEvent{Code: '☃', Mod: ModAlt}, }, }, // Standalone control characters. seqTest{ []byte{'\x1b'}, []Event{ KeyPressEvent{Code: KeyEscape}, }, }, seqTest{ []byte{ansi.SOH}, []Event{ KeyPressEvent{Code: 'a', Mod: ModCtrl}, }, }, seqTest{ []byte{'\x1b', ansi.SOH}, []Event{ KeyPressEvent{Code: 'a', Mod: ModCtrl | ModAlt}, }, }, seqTest{ []byte{ansi.NUL}, []Event{ KeyPressEvent{Code: KeySpace, Mod: ModCtrl}, }, }, seqTest{ []byte{'\x1b', ansi.NUL}, []Event{ KeyPressEvent{Code: KeySpace, Mod: ModCtrl | ModAlt}, }, }, // C1 control characters. seqTest{ []byte{'\x80'}, []Event{ KeyPressEvent{Code: rune(0x80 - '@'), Mod: ModCtrl | ModAlt}, }, }, ) 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'}, []Event{ UnknownEvent(rune(0xfe)), }, }) } var p Parser for _, tc := range td { t.Run(fmt.Sprintf("%q", string(tc.seq)), func(t *testing.T) { var events []Event buf := tc.seq for len(buf) > 0 { width, Event := p.parseSequence(buf) switch Event := Event.(type) { case MultiEvent: events = append(events, Event...) default: events = append(events, Event) } buf = buf[width:] } if !reflect.DeepEqual(tc.Events, events) { t.Errorf("\nexpected event for %q:\n %#v\ngot:\n %#v", tc.seq, tc.Events, events) } }) } } func TestReadLongInput(t *testing.T) { expect := make([]Event, 1000) for i := range 1000 { expect[i] = KeyPressEvent{Code: 'a', Text: "a"} } input := strings.Repeat("a", 1000) drv, err := NewReader(strings.NewReader(input), "dumb", 0) if err != nil { t.Fatalf("unexpected input driver error: %v", err) } var Events []Event for { events, err := drv.ReadEvents() if err == io.EOF { break } if err != nil { t.Fatalf("unexpected input error: %v", err) } Events = append(Events, events...) } if !reflect.DeepEqual(expect, Events) { t.Errorf("unexpected messages, expected:\n %+v\ngot:\n %+v", expect, Events) } } func TestReadInput(t *testing.T) { type test struct { keyname string in []byte out []Event } testData := []test{ { "a", []byte{'a'}, []Event{ KeyPressEvent{Code: 'a', Text: "a"}, }, }, { "space", []byte{' '}, []Event{ KeyPressEvent{Code: KeySpace, Text: " "}, }, }, { "a alt+a", []byte{'a', '\x1b', 'a'}, []Event{ KeyPressEvent{Code: 'a', Text: "a"}, KeyPressEvent{Code: 'a', Mod: ModAlt}, }, }, { "a alt+a a", []byte{'a', '\x1b', 'a', 'a'}, []Event{ KeyPressEvent{Code: 'a', Text: "a"}, KeyPressEvent{Code: 'a', Mod: ModAlt}, KeyPressEvent{Code: 'a', Text: "a"}, }, }, { "ctrl+a", []byte{byte(ansi.SOH)}, []Event{ KeyPressEvent{Code: 'a', Mod: ModCtrl}, }, }, { "ctrl+a ctrl+b", []byte{byte(ansi.SOH), byte(ansi.STX)}, []Event{ KeyPressEvent{Code: 'a', Mod: ModCtrl}, KeyPressEvent{Code: 'b', Mod: ModCtrl}, }, }, { "alt+a", []byte{byte(0x1b), 'a'}, []Event{ KeyPressEvent{Code: 'a', Mod: ModAlt}, }, }, { "a b c d", []byte{'a', 'b', 'c', 'd'}, []Event{ KeyPressEvent{Code: 'a', Text: "a"}, KeyPressEvent{Code: 'b', Text: "b"}, KeyPressEvent{Code: 'c', Text: "c"}, KeyPressEvent{Code: 'd', Text: "d"}, }, }, { "up", []byte("\x1b[A"), []Event{ KeyPressEvent{Code: KeyUp}, }, }, { "wheel up", []byte{'\x1b', '[', 'M', byte(32) + 0b0100_0000, byte(65), byte(49)}, []Event{ MouseWheelEvent{X: 32, Y: 16, Button: MouseWheelUp}, }, }, { "left motion 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), }, []Event{ MouseMotionEvent{X: 32, Y: 16, Button: MouseLeft}, MouseReleaseEvent{X: 64, Y: 32, Button: MouseNone}, }, }, { "shift+tab", []byte{'\x1b', '[', 'Z'}, []Event{ KeyPressEvent{Code: KeyTab, Mod: ModShift}, }, }, { "enter", []byte{'\r'}, []Event{KeyPressEvent{Code: KeyEnter}}, }, { "alt+enter", []byte{'\x1b', '\r'}, []Event{ KeyPressEvent{Code: KeyEnter, Mod: ModAlt}, }, }, { "insert", []byte{'\x1b', '[', '2', '~'}, []Event{ KeyPressEvent{Code: KeyInsert}, }, }, { "ctrl+alt+a", []byte{'\x1b', byte(ansi.SOH)}, []Event{ KeyPressEvent{Code: 'a', Mod: ModCtrl | ModAlt}, }, }, { "CSI?----X?", []byte{'\x1b', '[', '-', '-', '-', '-', 'X'}, []Event{UnknownEvent([]byte{'\x1b', '[', '-', '-', '-', '-', 'X'})}, }, // Powershell sequences. { "up", []byte{'\x1b', 'O', 'A'}, []Event{KeyPressEvent{Code: KeyUp}}, }, { "down", []byte{'\x1b', 'O', 'B'}, []Event{KeyPressEvent{Code: KeyDown}}, }, { "right", []byte{'\x1b', 'O', 'C'}, []Event{KeyPressEvent{Code: KeyRight}}, }, { "left", []byte{'\x1b', 'O', 'D'}, []Event{KeyPressEvent{Code: KeyLeft}}, }, { "alt+enter", []byte{'\x1b', '\x0d'}, []Event{KeyPressEvent{Code: KeyEnter, Mod: ModAlt}}, }, { "alt+backspace", []byte{'\x1b', '\x7f'}, []Event{KeyPressEvent{Code: KeyBackspace, Mod: ModAlt}}, }, { "ctrl+space", []byte{'\x00'}, []Event{KeyPressEvent{Code: KeySpace, Mod: ModCtrl}}, }, { "ctrl+alt+space", []byte{'\x1b', '\x00'}, []Event{KeyPressEvent{Code: KeySpace, Mod: ModCtrl | ModAlt}}, }, { "esc", []byte{'\x1b'}, []Event{KeyPressEvent{Code: KeyEscape}}, }, { "alt+esc", []byte{'\x1b', '\x1b'}, []Event{KeyPressEvent{Code: KeyEscape, Mod: ModAlt}}, }, { "a b o", []byte{ '\x1b', '[', '2', '0', '0', '~', 'a', ' ', 'b', '\x1b', '[', '2', '0', '1', '~', 'o', }, []Event{ PasteStartEvent{}, PasteEvent("a b"), PasteEndEvent{}, KeyPressEvent{Code: 'o', Text: "o"}, }, }, { "a\x03\nb", []byte{ '\x1b', '[', '2', '0', '0', '~', 'a', '\x03', '\n', 'b', '\x1b', '[', '2', '0', '1', '~', }, []Event{ PasteStartEvent{}, PasteEvent("a\x03\nb"), PasteEndEvent{}, }, }, { "?0xfe?", []byte{'\xfe'}, []Event{ UnknownEvent(rune(0xfe)), }, }, { "a ?0xfe? b", []byte{'a', '\xfe', ' ', 'b'}, []Event{ KeyPressEvent{Code: 'a', Text: "a"}, UnknownEvent(rune(0xfe)), KeyPressEvent{Code: KeySpace, Text: " "}, KeyPressEvent{Code: 'b', Text: "b"}, }, }, } for i, td := range testData { t.Run(fmt.Sprintf("%d: %s", i, td.keyname), func(t *testing.T) { Events := testReadInputs(t, bytes.NewReader(td.in)) var buf strings.Builder for i, Event := range Events { if i > 0 { buf.WriteByte(' ') } if s, ok := Event.(fmt.Stringer); ok { buf.WriteString(s.String()) } else { fmt.Fprintf(&buf, "%#v:%T", Event, Event) } } if len(Events) != len(td.out) { t.Fatalf("unexpected message list length: got %d, expected %d\n got: %#v\n expected: %#v\n", len(Events), len(td.out), Events, td.out) } if !reflect.DeepEqual(td.out, Events) { t.Fatalf("expected:\n%#v\ngot:\n%#v", td.out, Events) } }) } } func testReadInputs(t *testing.T, input io.Reader) []Event { // We'll check that the input reader finishes at the end // without error. var wg sync.WaitGroup var inputErr error ctx, cancel := context.WithCancel(context.Background()) defer func() { cancel() wg.Wait() if inputErr != nil && !errors.Is(inputErr, io.EOF) { t.Fatalf("unexpected input error: %v", inputErr) } }() dr, err := NewReader(input, "dumb", 0) if err != nil { t.Fatalf("unexpected input driver error: %v", err) } // The messages we're consuming. EventsC := make(chan Event) // Start the reader in the background. wg.Add(1) go func() { defer wg.Done() var events []Event events, inputErr = dr.ReadEvents() out: for _, ev := range events { select { case EventsC <- ev: case <-ctx.Done(): break out } } EventsC <- nil }() var Events []Event loop: for { select { case Event := <-EventsC: if Event == nil { // end of input marker for the test. break loop } Events = append(Events, Event) case <-time.After(2 * time.Second): t.Errorf("timeout waiting for input event") break loop } } return Events } // 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 reproducibility. 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, "ctrl+"+prefix+"a") res.lengths = append(res.lengths, 1+esclen) case 1, 2: // A sequence. seqi := r.Intn(len(allseqs)) s := allseqs[seqi] if strings.Contains(s.name, "alt+") || strings.Contains(s.name, "meta+") { esclen = 0 prefix = "" alt = 0 } if alt == 1 { res.data = append(res.data, '\x1b') } res.data = append(res.data, s.seq...) if strings.HasPrefix(s.name, "ctrl+") { prefix = "ctrl+" + prefix } name := prefix + strings.TrimPrefix(s.name, "ctrl+") res.names = append(res.names, name) res.lengths = append(res.lengths, len(s.seq)+esclen) } } return res } func FuzzParseSequence(f *testing.F) { var p Parser for seq := range sequences { f.Add(seq) } f.Add("\x1b]52;?\x07") // OSC 52 f.Add("\x1b]11;rgb:0000/0000/0000\x1b\\") // OSC 11 f.Add("\x1bP>|charm terminal(0.1.2)\x1b\\") // DCS (XTVERSION) f.Add("\x1b_Gi=123\x1b\\") // APC f.Fuzz(func(t *testing.T, seq string) { n, _ := p.parseSequence([]byte(seq)) if n == 0 && seq != "" { t.Errorf("expected a non-zero width for %q", seq) } }) } // BenchmarkDetectSequenceMap benchmarks the map-based sequence // detector. func BenchmarkDetectSequenceMap(b *testing.B) { var p Parser td := genRandomDataWithSeed(123, 10000) for i := 0; i < b.N; i++ { for j, w := 0, 0; j < len(td.data); j += w { w, _ = p.parseSequence(td.data[j:]) } } }