1package tui
2
3import (
4 "strconv"
5 "strings"
6 "time"
7)
8
9// Key is a parsed keypress.
10type Key struct {
11 Kind KeyKind
12 Rune rune // for KeyRune
13 Paste string // for KeyPaste
14 Ctrl bool
15 Alt bool
16 Shift bool
17 Super bool
18}
19
20type KeyKind int
21
22const (
23 KeyRune KeyKind = iota
24 KeyEnter
25 KeyBackspace
26 KeyTab
27 KeyShiftTab
28 KeyEsc
29 KeyUp
30 KeyDown
31 KeyLeft
32 KeyRight
33 KeyHome
34 KeyEnd
35 KeyPageUp
36 KeyPageDown
37 KeyDelete
38 KeyCtrlC
39 KeyCtrlD
40 KeyCtrlL
41 KeyCtrlU
42 KeyCtrlK
43 KeyCtrlA
44 KeyCtrlE
45 KeyCtrlW
46 KeyCtrlO
47 KeyPaste
48 KeyPasteClipboard
49 KeyMouseWheelUp
50 KeyMouseWheelDown
51 KeyUnknown
52)
53
54// Reader parses a byte stream into Key events. It understands basic
55// xterm escape sequences and bracketed paste.
56type Reader struct {
57 src func() (byte, error)
58 peek func(time.Duration) (byte, bool, error) // optional; may be nil
59}
60
61// NewReader returns a Reader that pulls bytes from read.
62func NewReader(read func() (byte, error)) *Reader { return &Reader{src: read} }
63
64// NewReaderWithPeek returns a Reader that pulls bytes from read and uses
65// peek to disambiguate bare Esc from the start of an escape sequence.
66func NewReaderWithPeek(read func() (byte, error), peek func(time.Duration) (byte, bool, error)) *Reader {
67 return &Reader{src: read, peek: peek}
68}
69
70// Read returns the next parsed Key.
71func (r *Reader) Read() (Key, error) {
72 b, err := r.src()
73 if err != nil {
74 return Key{}, err
75 }
76 switch {
77 case b == 0x03:
78 return Key{Kind: KeyCtrlC}, nil
79 case b == 0x04:
80 return Key{Kind: KeyCtrlD}, nil
81 case b == 0x0c:
82 return Key{Kind: KeyCtrlL}, nil
83 case b == 0x15:
84 return Key{Kind: KeyCtrlU}, nil
85 case b == 0x0b:
86 return Key{Kind: KeyCtrlK}, nil
87 case b == 0x01:
88 return Key{Kind: KeyCtrlA}, nil
89 case b == 0x05:
90 return Key{Kind: KeyCtrlE}, nil
91 case b == 0x17:
92 return Key{Kind: KeyCtrlW}, nil
93 case b == 0x0f:
94 return Key{Kind: KeyCtrlO}, nil
95 case b == 0x16:
96 return Key{Kind: KeyPasteClipboard, Ctrl: true}, nil
97 case b == '\r', b == '\n':
98 return Key{Kind: KeyEnter}, nil
99 case b == '\t':
100 return Key{Kind: KeyTab}, nil
101 case b == 0x7f, b == 0x08:
102 return Key{Kind: KeyBackspace}, nil
103 case b == 0x1b:
104 return r.readEscape()
105 case b < 0x20:
106 return Key{Kind: KeyUnknown}, nil
107 }
108 // UTF-8 multibyte?
109 if b < 0x80 {
110 return Key{Kind: KeyRune, Rune: rune(b)}, nil
111 }
112 // Decode UTF-8 (up to 4 bytes).
113 n := utf8Len(b)
114 buf := []byte{b}
115 for i := 1; i < n; i++ {
116 bb, err := r.src()
117 if err != nil {
118 return Key{}, err
119 }
120 buf = append(buf, bb)
121 }
122 rn, _ := decodeRune(buf)
123 return Key{Kind: KeyRune, Rune: rn}, nil
124}
125
126func utf8Len(b byte) int {
127 switch {
128 case b&0xe0 == 0xc0:
129 return 2
130 case b&0xf0 == 0xe0:
131 return 3
132 case b&0xf8 == 0xf0:
133 return 4
134 }
135 return 1
136}
137
138func decodeRune(b []byte) (rune, int) {
139 // Minimal decoder; invalid runes become U+FFFD.
140 if len(b) == 1 {
141 return rune(b[0]), 1
142 }
143 var r rune
144 switch len(b) {
145 case 2:
146 r = rune(b[0]&0x1f)<<6 | rune(b[1]&0x3f)
147 case 3:
148 r = rune(b[0]&0x0f)<<12 | rune(b[1]&0x3f)<<6 | rune(b[2]&0x3f)
149 case 4:
150 r = rune(b[0]&0x07)<<18 | rune(b[1]&0x3f)<<12 | rune(b[2]&0x3f)<<6 | rune(b[3]&0x3f)
151 default:
152 r = 0xFFFD
153 }
154 return r, len(b)
155}
156
157// readEscape handles sequences starting with 0x1b.
158func (r *Reader) readEscape() (Key, error) {
159 // Bare ESC: maybe followed by another byte within a short window.
160 b, have, err := r.readEscapeNext(50 * time.Millisecond)
161 if err != nil || !have {
162 return Key{Kind: KeyEsc}, nil
163 }
164 switch b {
165 case '[':
166 return r.readCSI()
167 case 'O':
168 // SS3 sequences. Terminals in application cursor mode use these
169 // for arrows, while some terminals also use them for Home and End.
170 c, err := r.src()
171 if err != nil {
172 return Key{}, err
173 }
174 switch c {
175 case 'A':
176 return Key{Kind: KeyUp}, nil
177 case 'B':
178 return Key{Kind: KeyDown}, nil
179 case 'C':
180 return Key{Kind: KeyRight}, nil
181 case 'D':
182 return Key{Kind: KeyLeft}, nil
183 case 'H':
184 return Key{Kind: KeyHome}, nil
185 case 'F':
186 return Key{Kind: KeyEnd}, nil
187 }
188 return Key{Kind: KeyUnknown}, nil
189 case 0x7f, 0x08:
190 // Alt+Backspace (Option+Delete on macOS) — most terminals send
191 // ESC + DEL for this. Surface as a dedicated "alt backspace"
192 // so the editor can map it to delete-word.
193 return Key{Kind: KeyBackspace, Alt: true}, nil
194 case 'b':
195 // Emacs-style word-left, also emitted by some terminals for
196 // Option+LeftArrow.
197 return Key{Kind: KeyLeft, Alt: true}, nil
198 case 'f':
199 // Emacs-style word-right, also emitted for Option+RightArrow.
200 return Key{Kind: KeyRight, Alt: true}, nil
201 default:
202 // Alt+<char>
203 if b < 0x80 {
204 return Key{Kind: KeyRune, Rune: rune(b), Alt: true}, nil
205 }
206 }
207 return Key{Kind: KeyUnknown}, nil
208}
209
210// readEscapeNext tries to read one byte within d. If peek is available
211// we use it (true non-blocking). Otherwise we fall back to a blocking
212// read, which means bare Esc is only detected after the next keystroke.
213func (r *Reader) readEscapeNext(d time.Duration) (byte, bool, error) {
214 if r.peek != nil {
215 return r.peek(d)
216 }
217 b, err := r.src()
218 if err != nil {
219 return 0, false, err
220 }
221 return b, true, nil
222}
223
224// readCSI parses a CSI sequence after ESC [.
225func (r *Reader) readCSI() (Key, error) {
226 var params []byte
227 for {
228 c, err := r.src()
229 if err != nil {
230 return Key{}, err
231 }
232 if c >= 0x30 && c <= 0x3f {
233 params = append(params, c)
234 continue
235 }
236 // Final byte.
237 return r.dispatchCSI(string(params), c), nil
238 }
239}
240
241func (r *Reader) dispatchCSI(params string, final byte) Key {
242 // SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
243 // button codes 64 (up) and 65 (down). We ignore coordinates for
244 // now; the chat view only needs scroll direction.
245 if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
246 parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
247 if len(parts) >= 1 {
248 switch parts[0] {
249 case "64":
250 return Key{Kind: KeyMouseWheelUp}
251 case "65":
252 return Key{Kind: KeyMouseWheelDown}
253 }
254 }
255 return Key{Kind: KeyUnknown}
256 }
257
258 shift, alt, super := parseCSIModifiers(params)
259 if final == 'u' {
260 if key, ok := parseCSIU(params); ok {
261 return key
262 }
263 }
264 if final == '~' {
265 if key, ok := parseModifyOtherKeys(params); ok {
266 return key
267 }
268 }
269 switch final {
270 case 'A':
271 return Key{Kind: KeyUp, Alt: alt, Shift: shift, Super: super}
272 case 'B':
273 return Key{Kind: KeyDown, Alt: alt, Shift: shift, Super: super}
274 case 'C':
275 return Key{Kind: KeyRight, Alt: alt, Shift: shift, Super: super}
276 case 'D':
277 return Key{Kind: KeyLeft, Alt: alt, Shift: shift, Super: super}
278 case 'H':
279 return Key{Kind: KeyHome}
280 case 'F':
281 return Key{Kind: KeyEnd}
282 case 'Z':
283 return Key{Kind: KeyShiftTab}
284 case '~':
285 switch params {
286 case "3":
287 return Key{Kind: KeyDelete}
288 case "5":
289 return Key{Kind: KeyPageUp}
290 case "6":
291 return Key{Kind: KeyPageDown}
292 case "200":
293 // Start of bracketed paste.
294 return r.readPaste()
295 }
296 }
297 return Key{Kind: KeyUnknown}
298}
299
300func parseCSIModifiers(params string) (shift, alt, super bool) {
301 if params == "" {
302 return false, false, false
303 }
304 i := strings.LastIndexByte(params, ';')
305 if i < 0 || i+1 >= len(params) {
306 return false, false, false
307 }
308 mod, ok := parseModifierParam(params[i+1:])
309 if !ok {
310 return false, false, false
311 }
312 shift, alt, _, super = modifierBits(mod)
313 return shift, alt, super
314}
315
316func parseCSIU(params string) (Key, bool) {
317 parts := strings.Split(params, ";")
318 if len(parts) == 0 {
319 return Key{}, false
320 }
321 code, err := strconv.Atoi(parts[0])
322 if err != nil {
323 return Key{}, false
324 }
325 mod := 1
326 if len(parts) >= 2 {
327 var ok bool
328 mod, ok = parseModifierParam(parts[1])
329 if !ok {
330 return Key{}, false
331 }
332 }
333 return keyFromModifiedCode(code, mod)
334}
335
336func parseModifyOtherKeys(params string) (Key, bool) {
337 parts := strings.Split(params, ";")
338 if len(parts) != 3 || parts[0] != "27" {
339 return Key{}, false
340 }
341 mod, ok := parseModifierParam(parts[1])
342 if !ok {
343 return Key{}, false
344 }
345 code, err := strconv.Atoi(parts[2])
346 if err != nil {
347 return Key{}, false
348 }
349 return keyFromModifiedCode(code, mod)
350}
351
352func parseModifierParam(s string) (int, bool) {
353 if i := strings.IndexByte(s, ':'); i >= 0 {
354 s = s[:i]
355 }
356 mod, err := strconv.Atoi(s)
357 if err != nil {
358 return 0, false
359 }
360 return mod, true
361}
362
363func modifierBits(mod int) (shift, alt, ctrl, super bool) {
364 bits := mod - 1
365 return bits&1 != 0, bits&2 != 0, bits&4 != 0, bits&8 != 0 || bits&32 != 0
366}
367
368func keyFromModifiedCode(code, mod int) (Key, bool) {
369 shift, alt, ctrl, super := modifierBits(mod)
370 // Kitty keyboard protocol (CSI ... u) reports control keys as their
371 // codepoints: Esc=27, Enter=13, Tab=9, Backspace=127. Without the
372 // enhanced-mode handling these arrive as raw bytes; with it enabled
373 // they come through here, so map them back to their dedicated keys.
374 switch code {
375 case 13:
376 return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
377 case 27:
378 return Key{Kind: KeyEsc, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
379 case 9:
380 if shift {
381 return Key{Kind: KeyShiftTab, Alt: alt, Ctrl: ctrl, Super: super}, true
382 }
383 return Key{Kind: KeyTab, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
384 case 127, 8:
385 return Key{Kind: KeyBackspace, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
386 }
387 if ctrl {
388 switch code {
389 case 'c', 'C':
390 return Key{Kind: KeyCtrlC, Shift: shift, Alt: alt, Ctrl: true}, true
391 case 'd', 'D':
392 return Key{Kind: KeyCtrlD, Shift: shift, Alt: alt, Ctrl: true}, true
393 case 'l', 'L':
394 return Key{Kind: KeyCtrlL, Shift: shift, Alt: alt, Ctrl: true}, true
395 case 'u', 'U':
396 return Key{Kind: KeyCtrlU, Shift: shift, Alt: alt, Ctrl: true}, true
397 case 'k', 'K':
398 return Key{Kind: KeyCtrlK, Shift: shift, Alt: alt, Ctrl: true}, true
399 case 'a', 'A':
400 return Key{Kind: KeyCtrlA, Shift: shift, Alt: alt, Ctrl: true}, true
401 case 'e', 'E':
402 return Key{Kind: KeyCtrlE, Shift: shift, Alt: alt, Ctrl: true}, true
403 case 'w', 'W':
404 return Key{Kind: KeyCtrlW, Shift: shift, Alt: alt, Ctrl: true}, true
405 case 'o', 'O':
406 return Key{Kind: KeyCtrlO, Shift: shift, Alt: alt, Ctrl: true}, true
407 case 'v', 'V':
408 return Key{Kind: KeyPasteClipboard, Shift: shift, Alt: alt, Ctrl: true}, true
409 }
410 }
411 if code >= '0' && code <= '9' {
412 return Key{Kind: KeyRune, Rune: rune(code), Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
413 }
414 return Key{}, false
415}
416
417// readPaste reads until ESC [ 2 0 1 ~ and returns the pasted text.
418func (r *Reader) readPaste() Key {
419 var sb strings.Builder
420 const end = "\x1b[201~"
421 tail := make([]byte, 0, len(end))
422 for {
423 b, err := r.src()
424 if err != nil {
425 break
426 }
427 tail = append(tail, b)
428 if len(tail) > len(end) {
429 sb.WriteByte(tail[0])
430 tail = tail[1:]
431 }
432 if string(tail) == end {
433 break
434 }
435 }
436 return Key{Kind: KeyPaste, Paste: sb.String()}
437}