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