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 KeyCtrlB
45 KeyCtrlE
46 KeyCtrlW
47 KeyCtrlO
48 KeyPaste
49 KeyPasteClipboard
50 KeyMouseWheelUp
51 KeyMouseWheelDown
52 KeyUnknown
53)
54
55// Reader parses a byte stream into Key events. It understands basic
56// xterm escape sequences and bracketed paste.
57type Reader struct {
58 src func() (byte, error)
59 peek func(time.Duration) (byte, bool, error) // optional; may be nil
60
61 // pending contains bytes consumed while deciding whether an Esc was
62 // bare. In particular, a second Esc belongs to the next Read, not to
63 // the Alt+<char> case handled by readEscape.
64 pending []byte
65
66 // A Reader without a peek callback still needs to preserve the old
67 // NewReader API and parse Alt/CSI input. The pump lets its bounded
68 // lookahead wait without calling the blocking source on the Read caller's
69 // goroutine. There is never more than one outstanding source call: after a
70 // timeout that call is reused by the next Read rather than replaced.
71 //
72 // The source callback has no cancellation contract, so an implementation
73 // that blocks forever can keep this one goroutine alive forever. That
74 // limitation is intentional; starting another goroutine for each isolated
75 // Esc would leak an unbounded number of blocked goroutines and could also
76 // reorder bytes from an uncancellable source.
77 pump *readerPump
78 pumpErr error
79}
80
81type readerResult struct {
82 b byte
83 err error
84}
85
86type readerPump struct {
87 result chan readerResult
88}
89
90func newReaderPump(read func() (byte, error)) *readerPump {
91 p := &readerPump{result: make(chan readerResult, 1)}
92 go func() {
93 b, err := read()
94 p.result <- readerResult{b: b, err: err}
95 }()
96 return p
97}
98
99// NewReader returns a Reader that pulls bytes from read.
100func NewReader(read func() (byte, error)) *Reader { return &Reader{src: read} }
101
102// NewReaderWithPeek returns a Reader that pulls bytes from read and uses
103// peek to disambiguate bare Esc from the start of an escape sequence.
104func NewReaderWithPeek(read func() (byte, error), peek func(time.Duration) (byte, bool, error)) *Reader {
105 return &Reader{src: read, peek: peek}
106}
107
108// readByte returns the next source byte, including any byte held back after
109// an escape lookahead.
110func (r *Reader) readByte() (byte, error) {
111 if len(r.pending) != 0 {
112 b := r.pending[0]
113 r.pending = r.pending[1:]
114 return b, nil
115 }
116 if r.pumpErr != nil {
117 return 0, r.pumpErr
118 }
119 if r.pump == nil {
120 return r.src()
121 }
122 result := <-r.pump.result
123 r.pump = nil
124 if result.err != nil {
125 r.pumpErr = result.err
126 }
127 return result.b, result.err
128}
129
130// readByteTimeout returns one source byte without waiting longer than d. A
131// source call left outstanding after a timeout is consumed by the next Read.
132func (r *Reader) readByteTimeout(d time.Duration) (byte, bool, error) {
133 if r.pumpErr != nil {
134 return 0, false, r.pumpErr
135 }
136 if r.pump == nil {
137 r.pump = newReaderPump(r.src)
138 }
139
140 var result readerResult
141 if d <= 0 {
142 select {
143 case result = <-r.pump.result:
144 default:
145 return 0, false, nil
146 }
147 } else {
148 timer := time.NewTimer(d)
149 defer timer.Stop()
150 select {
151 case result = <-r.pump.result:
152 case <-timer.C:
153 return 0, false, nil
154 }
155 }
156 r.pump = nil
157 if result.err != nil {
158 r.pumpErr = result.err
159 return 0, false, result.err
160 }
161 return result.b, true, nil
162}
163
164// Read returns the next parsed Key.
165func (r *Reader) Read() (Key, error) {
166 b, err := r.readByte()
167 if err != nil {
168 return Key{}, err
169 }
170 switch {
171 case b == 0x03:
172 return Key{Kind: KeyCtrlC}, nil
173 case b == 0x04:
174 return Key{Kind: KeyCtrlD}, nil
175 case b == 0x0c:
176 return Key{Kind: KeyCtrlL}, nil
177 case b == 0x15:
178 return Key{Kind: KeyCtrlU}, nil
179 case b == 0x0b:
180 return Key{Kind: KeyCtrlK}, nil
181 case b == 0x01:
182 return Key{Kind: KeyCtrlA}, nil
183 case b == 0x02:
184 return Key{Kind: KeyCtrlB}, nil
185 case b == 0x05:
186 return Key{Kind: KeyCtrlE}, nil
187 case b == 0x17:
188 return Key{Kind: KeyCtrlW}, nil
189 case b == 0x0f:
190 return Key{Kind: KeyCtrlO}, nil
191 case b == 0x16:
192 return Key{Kind: KeyPasteClipboard, Ctrl: true}, nil
193 case b == '\r', b == '\n':
194 return Key{Kind: KeyEnter}, nil
195 case b == '\t':
196 return Key{Kind: KeyTab}, nil
197 case b == 0x7f, b == 0x08:
198 return Key{Kind: KeyBackspace}, nil
199 case b == 0x1b:
200 return r.readEscape()
201 case b < 0x20:
202 return Key{Kind: KeyUnknown}, nil
203 }
204 // UTF-8 multibyte?
205 if b < 0x80 {
206 return Key{Kind: KeyRune, Rune: rune(b)}, nil
207 }
208 // Decode UTF-8 (up to 4 bytes).
209 n := utf8Len(b)
210 buf := []byte{b}
211 for i := 1; i < n; i++ {
212 bb, err := r.readByte()
213 if err != nil {
214 return Key{}, err
215 }
216 buf = append(buf, bb)
217 }
218 rn, _ := decodeRune(buf)
219 return Key{Kind: KeyRune, Rune: rn}, nil
220}
221
222func utf8Len(b byte) int {
223 switch {
224 case b&0xe0 == 0xc0:
225 return 2
226 case b&0xf0 == 0xe0:
227 return 3
228 case b&0xf8 == 0xf0:
229 return 4
230 }
231 return 1
232}
233
234func decodeRune(b []byte) (rune, int) {
235 // Minimal decoder; invalid runes become U+FFFD.
236 if len(b) == 1 {
237 return rune(b[0]), 1
238 }
239 var r rune
240 switch len(b) {
241 case 2:
242 r = rune(b[0]&0x1f)<<6 | rune(b[1]&0x3f)
243 case 3:
244 r = rune(b[0]&0x0f)<<12 | rune(b[1]&0x3f)<<6 | rune(b[2]&0x3f)
245 case 4:
246 r = rune(b[0]&0x07)<<18 | rune(b[1]&0x3f)<<12 | rune(b[2]&0x3f)<<6 | rune(b[3]&0x3f)
247 default:
248 r = 0xFFFD
249 }
250 return r, len(b)
251}
252
253// readEscape handles sequences starting with 0x1b.
254func (r *Reader) readEscape() (Key, error) {
255 // Bare ESC: maybe followed by another byte within a short window.
256 b, have, err := r.readEscapeNext(50 * time.Millisecond)
257 if err != nil || !have {
258 return Key{Kind: KeyEsc}, nil
259 }
260 if b == 0x1b {
261 // A second raw ESC is a separate key, not Alt+ESC. Keep it for the
262 // next Read so Esc Esc produces two bare KeyEsc events. This is the
263 // one lookahead case that is not intentionally treated as Alt input.
264 r.pending = append(r.pending, b)
265 return Key{Kind: KeyEsc}, nil
266 }
267 switch b {
268 case '[':
269 return r.readCSI()
270 case 'O':
271 // SS3 sequences (function keys in some terminals).
272 c, err := r.readByte()
273 if err != nil {
274 return Key{}, err
275 }
276 switch c {
277 case 'H':
278 return Key{Kind: KeyHome}, nil
279 case 'F':
280 return Key{Kind: KeyEnd}, nil
281 }
282 return Key{Kind: KeyUnknown}, nil
283 case 0x7f, 0x08:
284 // Alt+Backspace (Option+Delete on macOS) — most terminals send
285 // ESC + DEL for this. Surface as a dedicated "alt backspace"
286 // so the editor can map it to delete-word.
287 return Key{Kind: KeyBackspace, Alt: true}, nil
288 case 'b':
289 // Emacs-style word-left, also emitted by some terminals for
290 // Option+LeftArrow.
291 return Key{Kind: KeyLeft, Alt: true}, nil
292 case 'f':
293 // Emacs-style word-right, also emitted for Option+RightArrow.
294 return Key{Kind: KeyRight, Alt: true}, nil
295 default:
296 // Alt+<char>
297 if b < 0x80 {
298 return Key{Kind: KeyRune, Rune: rune(b), Alt: true}, nil
299 }
300 }
301 return Key{Kind: KeyUnknown}, nil
302}
303
304// readEscapeNext tries to read one byte within d. If peek is available
305// we use it. NewReader's source pump supplies the same bounded behavior
306// when no peek callback is provided, so a bare Esc never waits forever for
307// a second byte. A follow-on printable byte, DEL, b, or f remains
308// intentionally ambiguous with Alt input and keeps the historical mapping
309// in readEscape.
310func (r *Reader) readEscapeNext(d time.Duration) (byte, bool, error) {
311 if r.peek != nil {
312 return r.peek(d)
313 }
314 return r.readByteTimeout(d)
315}
316
317// readCSI parses a CSI sequence after ESC [.
318func (r *Reader) readCSI() (Key, error) {
319 var params []byte
320 for {
321 c, err := r.readByte()
322 if err != nil {
323 return Key{}, err
324 }
325 if c >= 0x30 && c <= 0x3f {
326 params = append(params, c)
327 continue
328 }
329 // Final byte.
330 return r.dispatchCSI(string(params), c), nil
331 }
332}
333
334func (r *Reader) dispatchCSI(params string, final byte) Key {
335 // SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
336 // button codes 64 (up) and 65 (down). We ignore coordinates for
337 // now; the chat view only needs scroll direction.
338 if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
339 parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
340 if len(parts) >= 1 {
341 switch parts[0] {
342 case "64":
343 return Key{Kind: KeyMouseWheelUp}
344 case "65":
345 return Key{Kind: KeyMouseWheelDown}
346 }
347 }
348 return Key{Kind: KeyUnknown}
349 }
350
351 shift, alt, super := parseCSIModifiers(params)
352 if final == 'u' {
353 if key, ok := parseCSIU(params); ok {
354 return key
355 }
356 }
357 if final == '~' {
358 if key, ok := parseModifyOtherKeys(params); ok {
359 return key
360 }
361 }
362 switch final {
363 case 'A':
364 return Key{Kind: KeyUp, Alt: alt, Shift: shift, Super: super}
365 case 'B':
366 return Key{Kind: KeyDown, Alt: alt, Shift: shift, Super: super}
367 case 'C':
368 return Key{Kind: KeyRight, Alt: alt, Shift: shift, Super: super}
369 case 'D':
370 return Key{Kind: KeyLeft, Alt: alt, Shift: shift, Super: super}
371 case 'H':
372 return Key{Kind: KeyHome}
373 case 'F':
374 return Key{Kind: KeyEnd}
375 case 'Z':
376 return Key{Kind: KeyShiftTab}
377 case '~':
378 switch params {
379 case "3":
380 return Key{Kind: KeyDelete}
381 case "5":
382 return Key{Kind: KeyPageUp}
383 case "6":
384 return Key{Kind: KeyPageDown}
385 case "200":
386 // Start of bracketed paste.
387 return r.readPaste()
388 }
389 }
390 return Key{Kind: KeyUnknown}
391}
392
393func parseCSIModifiers(params string) (shift, alt, super bool) {
394 if params == "" {
395 return false, false, false
396 }
397 i := strings.LastIndexByte(params, ';')
398 if i < 0 || i+1 >= len(params) {
399 return false, false, false
400 }
401 mod, ok := parseModifierParam(params[i+1:])
402 if !ok {
403 return false, false, false
404 }
405 shift, alt, _, super = modifierBits(mod)
406 return shift, alt, super
407}
408
409func parseCSIU(params string) (Key, bool) {
410 parts := strings.Split(params, ";")
411 if len(parts) == 0 {
412 return Key{}, false
413 }
414 code, err := strconv.Atoi(parts[0])
415 if err != nil {
416 return Key{}, false
417 }
418 mod := 1
419 if len(parts) >= 2 {
420 var ok bool
421 mod, ok = parseModifierParam(parts[1])
422 if !ok {
423 return Key{}, false
424 }
425 }
426 return keyFromModifiedCode(code, mod)
427}
428
429func parseModifyOtherKeys(params string) (Key, bool) {
430 parts := strings.Split(params, ";")
431 if len(parts) != 3 || parts[0] != "27" {
432 return Key{}, false
433 }
434 mod, ok := parseModifierParam(parts[1])
435 if !ok {
436 return Key{}, false
437 }
438 code, err := strconv.Atoi(parts[2])
439 if err != nil {
440 return Key{}, false
441 }
442 return keyFromModifiedCode(code, mod)
443}
444
445func parseModifierParam(s string) (int, bool) {
446 if i := strings.IndexByte(s, ':'); i >= 0 {
447 s = s[:i]
448 }
449 mod, err := strconv.Atoi(s)
450 if err != nil {
451 return 0, false
452 }
453 return mod, true
454}
455
456func modifierBits(mod int) (shift, alt, ctrl, super bool) {
457 bits := mod - 1
458 return bits&1 != 0, bits&2 != 0, bits&4 != 0, bits&8 != 0 || bits&32 != 0
459}
460
461func keyFromModifiedCode(code, mod int) (Key, bool) {
462 shift, alt, ctrl, super := modifierBits(mod)
463 // Kitty keyboard protocol (CSI ... u) reports control keys as their
464 // codepoints: Esc=27, Enter=13, Tab=9, Backspace=127. Without the
465 // enhanced-mode handling these arrive as raw bytes; with it enabled
466 // they come through here, so map them back to their dedicated keys.
467 switch code {
468 case 13:
469 return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
470 case 27:
471 return Key{Kind: KeyEsc, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
472 case 9:
473 if shift {
474 return Key{Kind: KeyShiftTab, Alt: alt, Ctrl: ctrl, Super: super}, true
475 }
476 return Key{Kind: KeyTab, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
477 case 127, 8:
478 return Key{Kind: KeyBackspace, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
479 }
480 if ctrl {
481 switch code {
482 case 'c', 'C':
483 return Key{Kind: KeyCtrlC, Shift: shift, Alt: alt, Ctrl: true}, true
484 case 'd', 'D':
485 return Key{Kind: KeyCtrlD, Shift: shift, Alt: alt, Ctrl: true}, true
486 case 'l', 'L':
487 return Key{Kind: KeyCtrlL, Shift: shift, Alt: alt, Ctrl: true}, true
488 case 'u', 'U':
489 return Key{Kind: KeyCtrlU, Shift: shift, Alt: alt, Ctrl: true}, true
490 case 'k', 'K':
491 return Key{Kind: KeyCtrlK, Shift: shift, Alt: alt, Ctrl: true}, true
492 case 'a', 'A':
493 return Key{Kind: KeyCtrlA, Shift: shift, Alt: alt, Ctrl: true}, true
494 case 'b', 'B', 2:
495 return Key{Kind: KeyCtrlB, Shift: shift, Alt: alt, Ctrl: true}, true
496 case 'e', 'E':
497 return Key{Kind: KeyCtrlE, Shift: shift, Alt: alt, Ctrl: true}, true
498 case 'w', 'W':
499 return Key{Kind: KeyCtrlW, Shift: shift, Alt: alt, Ctrl: true}, true
500 case 'o', 'O':
501 return Key{Kind: KeyCtrlO, Shift: shift, Alt: alt, Ctrl: true}, true
502 case 'v', 'V':
503 return Key{Kind: KeyPasteClipboard, Shift: shift, Alt: alt, Ctrl: true}, true
504 }
505 }
506 if code >= '0' && code <= '9' {
507 return Key{Kind: KeyRune, Rune: rune(code), Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
508 }
509 return Key{}, false
510}
511
512// readPaste reads until ESC [ 2 0 1 ~ and returns the pasted text.
513func (r *Reader) readPaste() Key {
514 var sb strings.Builder
515 const end = "\x1b[201~"
516 tail := make([]byte, 0, len(end))
517 for {
518 b, err := r.readByte()
519 if err != nil {
520 break
521 }
522 tail = append(tail, b)
523 if len(tail) > len(end) {
524 sb.WriteByte(tail[0])
525 tail = tail[1:]
526 }
527 if string(tail) == end {
528 break
529 }
530 }
531 return Key{Kind: KeyPaste, Paste: sb.String()}
532}