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 TestReaderParsesCSIUCtrlVAsClipboardPaste(t *testing.T) {
81 k := readKey(t, "\x1b[118;5u")
82 if k.Kind != KeyPasteClipboard || !k.Ctrl {
83 t.Fatalf("Read kind=%v ctrl=%v, want enhanced ctrl+v clipboard paste", k.Kind, k.Ctrl)
84 }
85}
86
87func TestReaderParsesModifyOtherKeysCtrlC(t *testing.T) {
88 k := readKey(t, "\x1b[27;5;99~")
89 if k.Kind != KeyCtrlC || !k.Ctrl {
90 t.Fatalf("Read kind=%v ctrl=%v, want ctrl+c", k.Kind, k.Ctrl)
91 }
92}
93
94func TestReaderParsesCSIUEsc(t *testing.T) {
95 k := readKey(t, "\x1b[27u")
96 if k.Kind != KeyEsc {
97 t.Fatalf("Read kind=%v, want esc", k.Kind)
98 }
99}
100
101func TestReaderParsesCSIUTabAndBackspace(t *testing.T) {
102 if k := readKey(t, "\x1b[9u"); k.Kind != KeyTab {
103 t.Fatalf("Read kind=%v, want tab", k.Kind)
104 }
105 if k := readKey(t, "\x1b[9;2u"); k.Kind != KeyShiftTab {
106 t.Fatalf("Read kind=%v, want shift-tab", k.Kind)
107 }
108 if k := readKey(t, "\x1b[127u"); k.Kind != KeyBackspace {
109 t.Fatalf("Read kind=%v, want backspace", k.Kind)
110 }
111}
112
113func TestReaderParsesRawEscapeEscapeAsTwoEscapes(t *testing.T) {
114 r := newPeekReader("\x1b\x1b")
115 if k := readReaderKey(t, r); k.Kind != KeyEsc {
116 t.Fatalf("first Read kind=%v, want esc", k.Kind)
117 }
118 if k := readReaderKey(t, r); k.Kind != KeyEsc {
119 t.Fatalf("second Read kind=%v, want esc", k.Kind)
120 }
121}
122
123func TestReaderParsesRawEscapeEscapeCSIAsEscapeThenUp(t *testing.T) {
124 r := newPeekReader("\x1b\x1b[A")
125 if k := readReaderKey(t, r); k.Kind != KeyEsc {
126 t.Fatalf("first Read kind=%v, want esc", k.Kind)
127 }
128 if k := readReaderKey(t, r); k.Kind != KeyUp {
129 t.Fatalf("second Read kind=%v, want up", k.Kind)
130 }
131}
132
133func TestReaderPreservesDoubleEscapePrintableAmbiguity(t *testing.T) {
134 r := newPeekReader("\x1b\x1bx")
135 if got := readReaderKey(t, r); got.Kind != KeyEsc {
136 t.Fatalf("first Read = %+v, want bare escape", got)
137 }
138 if got := readReaderKey(t, r); got.Kind != KeyRune || got.Rune != 'x' || !got.Alt {
139 t.Fatalf("second Read = %+v, want Alt+x after the documented ambiguity", got)
140 }
141}
142
143func TestReaderPreservesAmbiguousAltEscapeMappings(t *testing.T) {
144 cases := []struct {
145 name string
146 seq string
147 want Key
148 }{
149 {name: "printable", seq: "\x1bx", want: Key{Kind: KeyRune, Rune: 'x', Alt: true}},
150 {name: "delete", seq: "\x1b\x7f", want: Key{Kind: KeyBackspace, Alt: true}},
151 {name: "word-left", seq: "\x1bb", want: Key{Kind: KeyLeft, Alt: true}},
152 {name: "word-right", seq: "\x1bf", want: Key{Kind: KeyRight, Alt: true}},
153 }
154 for _, tc := range cases {
155 t.Run(tc.name, func(t *testing.T) {
156 got := readReaderKey(t, newPeekReader(tc.seq))
157 if got != tc.want {
158 t.Fatalf("Read(%q) = %+v, want %+v", tc.seq, got, tc.want)
159 }
160 })
161 }
162}
163
164func TestReaderWithoutPeekDoesNotBlockBareEscapeOrStartAnotherPump(t *testing.T) {
165 release := make(chan struct{})
166 lookaheadStarted := make(chan struct{})
167 calls := 0
168 r := NewReader(func() (byte, error) {
169 calls++
170 if calls == 1 {
171 return 0x1b, nil
172 }
173 if calls == 2 {
174 close(lookaheadStarted)
175 <-release
176 return 'x', nil
177 }
178 return 0, io.EOF
179 })
180
181 readDone := make(chan struct{})
182 var got Key
183 var err error
184 go func() {
185 got, err = r.Read()
186 close(readDone)
187 }()
188 select {
189 case <-lookaheadStarted:
190 case <-time.After(500 * time.Millisecond):
191 t.Fatal("lookahead source was not started")
192 }
193 select {
194 case <-readDone:
195 case <-time.After(500 * time.Millisecond):
196 t.Fatal("Read blocked while disambiguating bare escape")
197 }
198 if err != nil {
199 t.Fatalf("Read: %v", err)
200 }
201 if got.Kind != KeyEsc {
202 t.Fatalf("Read kind=%v, want esc", got.Kind)
203 }
204
205 // The timed-out source call is still the sole owner of the next byte.
206 // Releasing it must make that byte available to the next Read without
207 // starting a second blocked callback.
208 close(release)
209 got, err = r.Read()
210 if err != nil {
211 t.Fatalf("Read after lookahead: %v", err)
212 }
213 if got.Kind != KeyRune || got.Rune != 'x' || calls != 2 {
214 t.Fatalf("Read after lookahead = %+v with %d source calls, want x with 2 calls", got, calls)
215 }
216}
217
218func TestReaderParsesSGRMouseWheel(t *testing.T) {
219 cases := []struct {
220 seq string
221 want KeyKind
222 }{
223 {"\x1b[<64;10;20M", KeyMouseWheelUp},
224 {"\x1b[<65;10;20M", KeyMouseWheelDown},
225 }
226 for _, tc := range cases {
227 k := readKey(t, tc.seq)
228 if k.Kind != tc.want {
229 t.Fatalf("Read(%q) kind=%v, want %v", tc.seq, k.Kind, tc.want)
230 }
231 }
232}
233
234func readKey(t *testing.T, seq string) Key {
235 t.Helper()
236 return readReaderKey(t, NewReader(func() (byte, error) {
237 if len(seq) == 0 {
238 return 0, io.EOF
239 }
240 b := seq[0]
241 seq = seq[1:]
242 return b, nil
243 }))
244}
245
246func newPeekReader(seq string) *Reader {
247 idx := 0
248 read := func() (byte, error) {
249 if idx >= len(seq) {
250 return 0, io.EOF
251 }
252 b := seq[idx]
253 idx++
254 return b, nil
255 }
256 peek := func(time.Duration) (byte, bool, error) {
257 if idx >= len(seq) {
258 return 0, false, nil
259 }
260 b := seq[idx]
261 idx++
262 return b, true, nil
263 }
264 return NewReaderWithPeek(read, peek)
265}
266
267func readReaderKey(t *testing.T, r *Reader) Key {
268 t.Helper()
269 k, err := r.Read()
270 if err != nil {
271 t.Fatalf("Read: %v", err)
272 }
273 return k
274}