mirror of
https://github.com/sst/opencode.git
synced 2025-08-23 14:34:08 +00:00
880 lines
19 KiB
Go
880 lines
19 KiB
Go
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 ; <modifier> ; <code> ~
|
|
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:])
|
|
}
|
|
}
|
|
}
|