1package tui
2
3import (
4 "io"
5 "testing"
6 "time"
7)
8
9func TestReaderParsesCSIUShiftEnter(t *testing.T) {
10 k := readKey(t, "\x1b[13;2u")
11 if k.Kind != KeyEnter || !k.Shift || k.Alt {
12 t.Fatalf("Read kind=%v shift=%v alt=%v, want shift+enter", k.Kind, k.Shift, k.Alt)
13 }
14}
15
16func TestReaderParsesModifyOtherKeysShiftEnter(t *testing.T) {
17 k := readKey(t, "\x1b[27;2;13~")
18 if k.Kind != KeyEnter || !k.Shift || k.Alt {
19 t.Fatalf("Read kind=%v shift=%v alt=%v, want shift+enter", k.Kind, k.Shift, k.Alt)
20 }
21}
22
23func TestReaderParsesCSIUCtrlC(t *testing.T) {
24 k := readKey(t, "\x1b[99;5u")
25 if k.Kind != KeyCtrlC || !k.Ctrl {
26 t.Fatalf("Read kind=%v ctrl=%v, want ctrl+c", k.Kind, k.Ctrl)
27 }
28}
29
30func TestReaderParsesCtrlR(t *testing.T) {
31 for name, sequence := range map[string]string{
32 "raw": "\x12",
33 "kitty protocol": "\x1b[114;5u",
34 "modify other keys": "\x1b[27;5;114~",
35 } {
36 t.Run(name, func(t *testing.T) {
37 k := readKey(t, sequence)
38 if k.Kind != KeyCtrlR {
39 t.Fatalf("Read kind=%v, want ctrl+r", k.Kind)
40 }
41 })
42 }
43}
44
45func TestReaderParsesCSIUCtrlNumber(t *testing.T) {
46 k := readKey(t, "\x1b[49;5u")
47 if k.Kind != KeyRune || k.Rune != '1' || !k.Ctrl {
48 t.Fatalf("Read kind=%v rune=%q ctrl=%v, want ctrl+1", k.Kind, k.Rune, k.Ctrl)
49 }
50}
51
52func TestReaderParsesCSIUSuperNumber(t *testing.T) {
53 k := readKey(t, "\x1b[50;9u")
54 if k.Kind != KeyRune || k.Rune != '2' || !k.Super {
55 t.Fatalf("Read kind=%v rune=%q super=%v, want super+2", k.Kind, k.Rune, k.Super)
56 }
57}
58
59func TestReaderParsesCSIUSuperNumberWithEventType(t *testing.T) {
60 k := readKey(t, "\x1b[51;9:3u")
61 if k.Kind != KeyRune || k.Rune != '3' || !k.Super {
62 t.Fatalf("Read kind=%v rune=%q super=%v, want super+3", k.Kind, k.Rune, k.Super)
63 }
64}
65
66func TestReaderParsesCSIUHyperNumberAsSuper(t *testing.T) {
67 k := readKey(t, "\x1b[52;33u")
68 if k.Kind != KeyRune || k.Rune != '4' || !k.Super {
69 t.Fatalf("Read kind=%v rune=%q super=%v, want hyper+4 as super", k.Kind, k.Rune, k.Super)
70 }
71}
72
73func TestReaderParsesRawCtrlVAsClipboardPaste(t *testing.T) {
74 k := readKey(t, "\x16")
75 if k.Kind != KeyPasteClipboard || !k.Ctrl {
76 t.Fatalf("Read kind=%v ctrl=%v, want ctrl+v clipboard paste", k.Kind, k.Ctrl)
77 }
78}
79
80func TestReaderParsesRawAltC(t *testing.T) {
81 k := readKey(t, "\x1bc")
82 if k.Kind != KeyRune || k.Rune != 'c' || !k.Alt {
83 t.Fatalf("Read = %+v, want alt+c", k)
84 }
85}
86
87func TestReaderParsesEnhancedAltC(t *testing.T) {
88 for name, sequence := range map[string]string{
89 "kitty protocol": "\x1b[99;3u",
90 "modify other keys": "\x1b[27;3;99~",
91 } {
92 t.Run(name, func(t *testing.T) {
93 k := readKey(t, sequence)
94 if k.Kind != KeyRune || k.Rune != 'c' || !k.Alt {
95 t.Fatalf("Read = %+v, want alt+c", k)
96 }
97 })
98 }
99}
100
101func TestReaderKeepsUnknownEnhancedCtrlLetterUnknown(t *testing.T) {
102 k := readKey(t, "\x1b[122;5u")
103 if k.Kind != KeyUnknown {
104 t.Fatalf("Read = %+v, want unknown ctrl+z", k)
105 }
106}
107
108func TestReaderParsesCSIUCtrlVAsClipboardPaste(t *testing.T) {
109 k := readKey(t, "\x1b[118;5u")
110 if k.Kind != KeyPasteClipboard || !k.Ctrl {
111 t.Fatalf("Read kind=%v ctrl=%v, want enhanced ctrl+v clipboard paste", k.Kind, k.Ctrl)
112 }
113}
114
115func TestReaderParsesModifyOtherKeysCtrlC(t *testing.T) {
116 k := readKey(t, "\x1b[27;5;99~")
117 if k.Kind != KeyCtrlC || !k.Ctrl {
118 t.Fatalf("Read kind=%v ctrl=%v, want ctrl+c", k.Kind, k.Ctrl)
119 }
120}
121
122func TestReaderParsesCSIUEsc(t *testing.T) {
123 k := readKey(t, "\x1b[27u")
124 if k.Kind != KeyEsc {
125 t.Fatalf("Read kind=%v, want esc", k.Kind)
126 }
127}
128
129func TestReaderParsesCSIUTabAndBackspace(t *testing.T) {
130 if k := readKey(t, "\x1b[9u"); k.Kind != KeyTab {
131 t.Fatalf("Read kind=%v, want tab", k.Kind)
132 }
133 if k := readKey(t, "\x1b[9;2u"); k.Kind != KeyShiftTab {
134 t.Fatalf("Read kind=%v, want shift-tab", k.Kind)
135 }
136 if k := readKey(t, "\x1b[127u"); k.Kind != KeyBackspace {
137 t.Fatalf("Read kind=%v, want backspace", k.Kind)
138 }
139}
140
141func TestReaderParsesRawEscapeEscapeAsTwoEscapes(t *testing.T) {
142 r := newPeekReader("\x1b\x1b")
143 if k := readReaderKey(t, r); k.Kind != KeyEsc {
144 t.Fatalf("first Read kind=%v, want esc", k.Kind)
145 }
146 if k := readReaderKey(t, r); k.Kind != KeyEsc {
147 t.Fatalf("second Read kind=%v, want esc", k.Kind)
148 }
149}
150
151func TestReaderParsesRawEscapeEscapeCSIAsEscapeThenUp(t *testing.T) {
152 r := newPeekReader("\x1b\x1b[A")
153 if k := readReaderKey(t, r); k.Kind != KeyEsc {
154 t.Fatalf("first Read kind=%v, want esc", k.Kind)
155 }
156 if k := readReaderKey(t, r); k.Kind != KeyUp {
157 t.Fatalf("second Read kind=%v, want up", k.Kind)
158 }
159}
160
161func TestReaderPreservesDoubleEscapePrintableAmbiguity(t *testing.T) {
162 r := newPeekReader("\x1b\x1bx")
163 if got := readReaderKey(t, r); got.Kind != KeyEsc {
164 t.Fatalf("first Read = %+v, want bare escape", got)
165 }
166 if got := readReaderKey(t, r); got.Kind != KeyRune || got.Rune != 'x' || !got.Alt {
167 t.Fatalf("second Read = %+v, want Alt+x after the documented ambiguity", got)
168 }
169}
170
171func TestReaderPreservesAmbiguousAltEscapeMappings(t *testing.T) {
172 cases := []struct {
173 name string
174 seq string
175 want Key
176 }{
177 {name: "printable", seq: "\x1bx", want: Key{Kind: KeyRune, Rune: 'x', Alt: true}},
178 {name: "delete", seq: "\x1b\x7f", want: Key{Kind: KeyBackspace, Alt: true}},
179 {name: "word-left", seq: "\x1bb", want: Key{Kind: KeyLeft, Alt: true}},
180 {name: "word-right", seq: "\x1bf", want: Key{Kind: KeyRight, Alt: true}},
181 }
182 for _, tc := range cases {
183 t.Run(tc.name, func(t *testing.T) {
184 got := readReaderKey(t, newPeekReader(tc.seq))
185 if got != tc.want {
186 t.Fatalf("Read(%q) = %+v, want %+v", tc.seq, got, tc.want)
187 }
188 })
189 }
190}
191
192func TestReaderWithoutPeekDoesNotBlockBareEscapeOrStartAnotherPump(t *testing.T) {
193 release := make(chan struct{})
194 lookaheadStarted := make(chan struct{})
195 calls := 0
196 r := NewReader(func() (byte, error) {
197 calls++
198 if calls == 1 {
199 return 0x1b, nil
200 }
201 if calls == 2 {
202 close(lookaheadStarted)
203 <-release
204 return 'x', nil
205 }
206 return 0, io.EOF
207 })
208
209 readDone := make(chan struct{})
210 var got Key
211 var err error
212 go func() {
213 got, err = r.Read()
214 close(readDone)
215 }()
216 select {
217 case <-lookaheadStarted:
218 case <-time.After(500 * time.Millisecond):
219 t.Fatal("lookahead source was not started")
220 }
221 select {
222 case <-readDone:
223 case <-time.After(500 * time.Millisecond):
224 t.Fatal("Read blocked while disambiguating bare escape")
225 }
226 if err != nil {
227 t.Fatalf("Read: %v", err)
228 }
229 if got.Kind != KeyEsc {
230 t.Fatalf("Read kind=%v, want esc", got.Kind)
231 }
232
233 // The timed-out source call is still the sole owner of the next byte.
234 // Releasing it must make that byte available to the next Read without
235 // starting a second blocked callback.
236 close(release)
237 got, err = r.Read()
238 if err != nil {
239 t.Fatalf("Read after lookahead: %v", err)
240 }
241 if got.Kind != KeyRune || got.Rune != 'x' || calls != 2 {
242 t.Fatalf("Read after lookahead = %+v with %d source calls, want x with 2 calls", got, calls)
243 }
244}
245
246func TestReaderParsesSS3CursorKeys(t *testing.T) {
247 cases := []struct {
248 seq string
249 want KeyKind
250 }{
251 {"\x1bOA", KeyUp},
252 {"\x1bOB", KeyDown},
253 {"\x1bOC", KeyRight},
254 {"\x1bOD", KeyLeft},
255 {"\x1bOH", KeyHome},
256 {"\x1bOF", KeyEnd},
257 }
258 for _, tc := range cases {
259 if k := readKey(t, tc.seq); k.Kind != tc.want {
260 t.Fatalf("Read(%q) kind=%v, want %v", tc.seq, k.Kind, tc.want)
261 }
262 }
263}
264
265func TestReaderParsesSGRMouseWheel(t *testing.T) {
266 cases := []struct {
267 seq string
268 want KeyKind
269 }{
270 {"\x1b[<64;10;20M", KeyMouseWheelUp},
271 {"\x1b[<65;10;20M", KeyMouseWheelDown},
272 }
273 for _, tc := range cases {
274 k := readKey(t, tc.seq)
275 if k.Kind != tc.want {
276 t.Fatalf("Read(%q) kind=%v, want %v", tc.seq, k.Kind, tc.want)
277 }
278 }
279}
280
281func readKey(t *testing.T, seq string) Key {
282 t.Helper()
283 return readReaderKey(t, NewReader(func() (byte, error) {
284 if len(seq) == 0 {
285 return 0, io.EOF
286 }
287 b := seq[0]
288 seq = seq[1:]
289 return b, nil
290 }))
291}
292
293func newPeekReader(seq string) *Reader {
294 idx := 0
295 read := func() (byte, error) {
296 if idx >= len(seq) {
297 return 0, io.EOF
298 }
299 b := seq[idx]
300 idx++
301 return b, nil
302 }
303 peek := func(time.Duration) (byte, bool, error) {
304 if idx >= len(seq) {
305 return 0, false, nil
306 }
307 b := seq[idx]
308 idx++
309 return b, true, nil
310 }
311 return NewReaderWithPeek(read, peek)
312}
313
314func readReaderKey(t *testing.T, r *Reader) Key {
315 t.Helper()
316 k, err := r.Read()
317 if err != nil {
318 t.Fatalf("Read: %v", err)
319 }
320 return k
321}