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