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 TestReaderParsesSGRMouseWheel(t *testing.T) {
247 cases := []struct {
248 seq string
249 want KeyKind
250 }{
251 {"\x1b[<64;10;20M", KeyMouseWheelUp},
252 {"\x1b[<65;10;20M", KeyMouseWheelDown},
253 }
254 for _, tc := range cases {
255 k := readKey(t, tc.seq)
256 if k.Kind != tc.want {
257 t.Fatalf("Read(%q) kind=%v, want %v", tc.seq, k.Kind, tc.want)
258 }
259 }
260}
261
262func readKey(t *testing.T, seq string) Key {
263 t.Helper()
264 return readReaderKey(t, NewReader(func() (byte, error) {
265 if len(seq) == 0 {
266 return 0, io.EOF
267 }
268 b := seq[0]
269 seq = seq[1:]
270 return b, nil
271 }))
272}
273
274func newPeekReader(seq string) *Reader {
275 idx := 0
276 read := func() (byte, error) {
277 if idx >= len(seq) {
278 return 0, io.EOF
279 }
280 b := seq[idx]
281 idx++
282 return b, nil
283 }
284 peek := func(time.Duration) (byte, bool, error) {
285 if idx >= len(seq) {
286 return 0, false, nil
287 }
288 b := seq[idx]
289 idx++
290 return b, true, nil
291 }
292 return NewReaderWithPeek(read, peek)
293}
294
295func readReaderKey(t *testing.T, r *Reader) Key {
296 t.Helper()
297 k, err := r.Read()
298 if err != nil {
299 t.Fatalf("Read: %v", err)
300 }
301 return k
302}