1package kittykey
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/stretchr/testify/require"
8)
9
10func TestNormalizerRewritesRequestFlagsToOuterFlags(t *testing.T) {
11 tests := []struct {
12 name string
13 in string
14 want string
15 }{
16 {name: "legacy passthrough", in: "hello\x1b[A\x1bOP\r", want: "hello\x1b[A\x1bOP\r"},
17 {name: "outer csi u passthrough", in: "\x1b[97;5u\x1b[38::49;5u", want: "\x1b[97;5u\x1b[38::49;5u"},
18 {name: "text key", in: "\x1b[97;;97u", want: "a"},
19 {name: "text key without text field", in: "\x1b[97u", want: "a"},
20 {name: "shift text", in: "\x1b[97:65;2;65u", want: "A"},
21 {name: "shift without text uses shifted", in: "\x1b[97:65;2u", want: "A"},
22 {name: "composed text event", in: "\x1b[0;;229u", want: "å"},
23 {name: "multi codepoint text", in: "\x1b[101;;101:769u", want: "e\u0301"},
24 {name: "control codes dropped from text", in: "\x1b[0;;7:97u", want: "a"},
25 {name: "repeat is press", in: "\x1b[97;1:2;97u", want: "a"},
26 {name: "release dropped", in: "\x1b[97;1:3u", want: ""},
27 {name: "space", in: "\x1b[32;;32u", want: " "},
28 {name: "enter", in: "\x1b[13u", want: "\r"},
29 {name: "tab", in: "\x1b[9u", want: "\t"},
30 {name: "backspace", in: "\x1b[127u", want: "\x7f"},
31 {name: "shift enter stays csi u", in: "\x1b[13;2u", want: "\x1b[13;2u"},
32 {name: "escape", in: "\x1b[27u", want: "\x1b[27u"},
33 {name: "ctrl letter", in: "\x1b[99;5u", want: "\x1b[99;5u"},
34 {name: "ctrl digit with base", in: "\x1b[38::49;5u", want: "\x1b[38::49;5u"},
35 {name: "alt letter without text", in: "\x1b[97;3u", want: "\x1b[97;3u"},
36 {name: "altgr text wins over modifiers", in: "\x1b[113;7;64u", want: "@"},
37 {name: "long ime commit", in: "\x1b[0;;" + strings.Repeat("19968:", 11) + "19968u", want: strings.Repeat("一", 12)},
38 {name: "caps lock dropped from text key", in: "\x1b[97;65;65u", want: "A"},
39 {name: "caps lock kept on ctrl letter", in: "\x1b[97;69u", want: "\x1b[97;69u"},
40 {name: "both locks kept on alt letter", in: "\x1b[97;195u", want: "\x1b[97;195u"},
41 {name: "num lock kept on escape", in: "\x1b[27;129u", want: "\x1b[27;129u"},
42 {name: "caps lock kept on arrow", in: "\x1b[1;65A", want: "\x1b[1;65A"},
43 {name: "caps lock arrow repeat", in: "\x1b[1;65:2A", want: "\x1b[1;65A"},
44 {name: "caps lock delete", in: "\x1b[3;65~", want: "\x1b[3;65~"},
45 {name: "ctrl press dropped", in: "\x1b[57442;5u", want: ""},
46 {name: "ctrl release dropped", in: "\x1b[57442;1:3u", want: ""},
47 {name: "shift key dropped", in: "\x1b[57441;2u", want: ""},
48 {name: "caps lock dropped", in: "\x1b[57358;65u", want: ""},
49 {name: "arrow unmodified passthrough", in: "\x1b[A", want: "\x1b[A"},
50 {name: "arrow repeat", in: "\x1b[1;1:2A", want: "\x1b[A"},
51 {name: "arrow release dropped", in: "\x1b[1;1:3A", want: ""},
52 {name: "ctrl arrow", in: "\x1b[1;5C", want: "\x1b[1;5C"},
53 {name: "ctrl arrow repeat", in: "\x1b[1;5:2C", want: "\x1b[1;5C"},
54 {name: "delete release dropped", in: "\x1b[3;1:3~", want: ""},
55 {name: "delete repeat", in: "\x1b[3;1:2~", want: "\x1b[3~"},
56 {name: "ctrl delete", in: "\x1b[3;5~", want: "\x1b[3;5~"},
57 {name: "mouse untouched", in: "\x1b[<0;1;1M", want: "\x1b[<0;1;1M"},
58 {name: "keyboard reply untouched", in: "\x1b[?5u", want: "\x1b[?5u"},
59 {name: "cursor report untouched", in: "\x1b[12;40R", want: "\x1b[12;40R"},
60 {name: "paste body untouched", in: "\x1b[200~\x1b[97;1:3u\x1b[201~\x1b[97;1:3u", want: "\x1b[200~\x1b[97;1:3u\x1b[201~"},
61 {name: "lone esc passes before any kitty key", in: "x\x1b", want: "x\x1b"},
62 {name: "lone esc held after a kitty key", in: "\x1b[97;;97u\x1b", want: "a"},
63 {name: "trailing garbage", in: "\x1b[97;;97u\x1b[zz", want: "a\x1b[zz"},
64 {name: "bad event type kept", in: "\x1b[97;1:4u", want: "\x1b[97;1:4u"},
65 }
66 for _, tt := range tests {
67 t.Run(tt.name, func(t *testing.T) {
68 var n Normalizer
69 require.Equal(t, tt.want, string(n.Normalize([]byte(tt.in))))
70 })
71 }
72}
73
74func TestNormalizerHoldsSplitSequences(t *testing.T) {
75 tests := []struct {
76 name string
77 reads []string
78 want []string
79 }{
80 {name: "split csi u", reads: []string{"a\x1b[97;", ";98u"}, want: []string{"a", "b"}},
81 {name: "split csi prefix", reads: []string{"\x1b[", "57442;1:3u"}, want: []string{"", ""}},
82 {name: "split paste close", reads: []string{"\x1b[200~x\x1b[20", "1~\x1b[97;;97u"}, want: []string{"\x1b[200~x", "\x1b[201~a"}},
83 {name: "split after esc", reads: []string{"\x1b[27u\x1b", "[57442;5u", "\x1b", "[97;;97u"}, want: []string{"\x1b[27u", "", "", "a"}},
84 {name: "legacy escape before the switch", reads: []string{"\x1b", "\x1b[97;;97u"}, want: []string{"\x1b", "a"}},
85 {name: "split paste open", reads: []string{"\x1b[20", "0~\x1b[97;1:3u\x1b[201~"}, want: []string{"", "\x1b[200~\x1b[97;1:3u\x1b[201~"}},
86 }
87 for _, tt := range tests {
88 t.Run(tt.name, func(t *testing.T) {
89 var n Normalizer
90 for i, read := range tt.reads {
91 require.Equal(t, tt.want[i], string(n.Normalize([]byte(read))), "read %d", i)
92 }
93 })
94 }
95}
96
97// TestNormalizerEverySplitMatchesOneRead feeds each input split at every byte
98// and requires the same output and Ctrl state as one read.
99func TestNormalizerEverySplitMatchesOneRead(t *testing.T) {
100 // Each input starts with a kitty key, as a terminal in RequestFlags mode
101 // sends before any split can matter.
102 inputs := []string{
103 "\x1b[57442;5u\x1b[49;5u\x1b[49;5:3u",
104 "a\x1b[97;;97u\x1b[57442;1:3u",
105 "\x1b[200~\x1b[97;1:3u\x1b[201~\x1b[1;65:2A",
106 "\x1b[0;;19968:19968u\x1b[27u",
107 }
108 for _, in := range inputs {
109 var whole Normalizer
110 whole.Normalize([]byte("\x1b[27u"))
111 want := string(whole.Normalize([]byte(in)))
112 for cut := 1; cut < len(in); cut++ {
113 var n Normalizer
114 n.Normalize([]byte("\x1b[27u"))
115 got := string(n.Normalize([]byte(in[:cut]))) + string(n.Normalize([]byte(in[cut:])))
116 require.Equal(t, want, got, "input %q cut %d", in, cut)
117 require.Equal(t, whole.CtrlHeld(), n.CtrlHeld(), "input %q cut %d", in, cut)
118 }
119 }
120}
121
122func TestNormalizerTracksCtrl(t *testing.T) {
123 tests := []struct {
124 name string
125 reads []string
126 held []bool
127 }{
128 {name: "left ctrl press and release", reads: []string{"\x1b[57442;5u", "\x1b[57442;1:3u"}, held: []bool{true, false}},
129 {name: "right ctrl", reads: []string{"\x1b[57448;5u", "\x1b[57448;1:3u"}, held: []bool{true, false}},
130 {name: "ctrl repeat keeps held", reads: []string{"\x1b[57442;5u", "\x1b[57442;5:2u"}, held: []bool{true, true}},
131 {name: "one of two ctrl released", reads: []string{"\x1b[57442;5u", "\x1b[57448;5u", "\x1b[57448;5:3u"}, held: []bool{true, true, true}},
132 {name: "ctrl digit keeps held", reads: []string{"\x1b[57442;5u", "\x1b[49;5u", "\x1b[49;5:3u"}, held: []bool{true, true, true}},
133 {name: "key without ctrl clears", reads: []string{"\x1b[57442;5u", "\x1b[97;;97u"}, held: []bool{true, false}},
134 {name: "arrow without ctrl clears", reads: []string{"\x1b[57442;5u", "\x1b[1;1:2A"}, held: []bool{true, false}},
135 {name: "ctrl shortcut clears", reads: []string{"\x1b[57442;5u", "\x1b[114;5u", "\x1b[114;5:3u"}, held: []bool{true, false, false}},
136 {name: "ctrl arrow clears", reads: []string{"\x1b[57442;5u", "\x1b[1;5C"}, held: []bool{true, false}},
137 {name: "azerty ctrl digit keeps held", reads: []string{"\x1b[57442;5u", "\x1b[38::49;5u"}, held: []bool{true, true}},
138 {name: "ctrl shift keeps held", reads: []string{"\x1b[57442;5u", "\x1b[57441;6u", "\x1b[57441;5:3u"}, held: []bool{true, true, true}},
139 {name: "composed text keeps held", reads: []string{"\x1b[57442;5u", "\x1b[0;;229u"}, held: []bool{true, true}},
140 {name: "legacy text never sets", reads: []string{"\x1b[97;5u"}, held: []bool{false}},
141 {name: "shift does not set", reads: []string{"\x1b[57441;2u"}, held: []bool{false}},
142 {name: "ctrl in paste ignored", reads: []string{"\x1b[200~\x1b[57442;5u\x1b[201~"}, held: []bool{false}},
143 }
144 for _, tt := range tests {
145 t.Run(tt.name, func(t *testing.T) {
146 var n Normalizer
147 for i, read := range tt.reads {
148 n.Normalize([]byte(read))
149 require.Equal(t, tt.held[i], n.CtrlHeld(), "read %d", i)
150 }
151 })
152 }
153 var n Normalizer
154 n.Normalize([]byte("\x1b[57442;5u"))
155 n.ReleaseCtrl()
156 require.False(t, n.CtrlHeld())
157}
158
159func FuzzNormalize(f *testing.F) {
160 for _, seed := range []string{"\x1b[97;;97u", "\x1b[57442;5u", "\x1b[200~x\x1b[201~", "\x1b[1;5:3A", "a\x1b[", "\x1b[0;;229:7u"} {
161 f.Add([]byte(seed))
162 }
163 f.Fuzz(func(t *testing.T, data []byte) {
164 var n Normalizer
165 out := n.Normalize(data)
166 out = append(out, n.Normalize(nil)...)
167 require.LessOrEqual(t, len(out), 4*len(data)+16)
168 })
169}