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 // Terminals in application cursor mode send arrows as SS3.
279 case 'A':
280 return Key{Kind: KeyUp}, nil
281 case 'B':
282 return Key{Kind: KeyDown}, nil
283 case 'C':
284 return Key{Kind: KeyRight}, nil
285 case 'D':
286 return Key{Kind: KeyLeft}, nil
287 case 'H':
288 return Key{Kind: KeyHome}, nil
289 case 'F':
290 return Key{Kind: KeyEnd}, nil
291 }
292 return Key{Kind: KeyUnknown}, nil
293 case 0x7f, 0x08:
294 // Alt+Backspace (Option+Delete on macOS) — most terminals send
295 // ESC + DEL for this. Surface as a dedicated "alt backspace"
296 // so the editor can map it to delete-word.
297 return Key{Kind: KeyBackspace, Alt: true}, nil
298 case 'b':
299 // Emacs-style word-left, also emitted by some terminals for
300 // Option+LeftArrow.
301 return Key{Kind: KeyLeft, Alt: true}, nil
302 case 'f':
303 // Emacs-style word-right, also emitted for Option+RightArrow.
304 return Key{Kind: KeyRight, Alt: true}, nil
305 default:
306 // Alt+<char>
307 if b < 0x80 {
308 return Key{Kind: KeyRune, Rune: rune(b), Alt: true}, nil
309 }
310 }
311 return Key{Kind: KeyUnknown}, nil
312}
313
314// readEscapeNext tries to read one byte within d. If peek is available
315// we use it. NewReader's source pump supplies the same bounded behavior
316// when no peek callback is provided, so a bare Esc never waits forever for
317// a second byte. A follow-on printable byte, DEL, b, or f remains
318// intentionally ambiguous with Alt input and keeps the historical mapping
319// in readEscape.
320func (r *Reader) readEscapeNext(d time.Duration) (byte, bool, error) {
321 if r.peek != nil {
322 return r.peek(d)
323 }
324 return r.readByteTimeout(d)
325}
326
327// readCSI parses a CSI sequence after ESC [.
328func (r *Reader) readCSI() (Key, error) {
329 var params []byte
330 for {
331 c, err := r.readByte()
332 if err != nil {
333 return Key{}, err
334 }
335 if c >= 0x30 && c <= 0x3f {
336 params = append(params, c)
337 continue
338 }
339 // Final byte.
340 return r.dispatchCSI(string(params), c), nil
341 }
342}
343
344func (r *Reader) dispatchCSI(params string, final byte) Key {
345 // SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
346 // button codes 64 (up) and 65 (down). We ignore coordinates for
347 // now; the chat view only needs scroll direction.
348 if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
349 parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
350 if len(parts) >= 1 {
351 switch parts[0] {
352 case "64":
353 return Key{Kind: KeyMouseWheelUp}
354 case "65":
355 return Key{Kind: KeyMouseWheelDown}
356 }
357 }
358 return Key{Kind: KeyUnknown}
359 }
360
361 shift, alt, super := parseCSIModifiers(params)
362 if final == 'u' {
363 if key, ok := parseCSIU(params); ok {
364 return key
365 }
366 }
367 if final == '~' {
368 if key, ok := parseModifyOtherKeys(params); ok {
369 return key
370 }
371 }
372 switch final {
373 case 'A':
374 return Key{Kind: KeyUp, Alt: alt, Shift: shift, Super: super}
375 case 'B':
376 return Key{Kind: KeyDown, Alt: alt, Shift: shift, Super: super}
377 case 'C':
378 return Key{Kind: KeyRight, Alt: alt, Shift: shift, Super: super}
379 case 'D':
380 return Key{Kind: KeyLeft, Alt: alt, Shift: shift, Super: super}
381 case 'H':
382 return Key{Kind: KeyHome}
383 case 'F':
384 return Key{Kind: KeyEnd}
385 case 'Z':
386 return Key{Kind: KeyShiftTab}
387 case '~':
388 switch params {
389 case "3":
390 return Key{Kind: KeyDelete}
391 case "5":
392 return Key{Kind: KeyPageUp}
393 case "6":
394 return Key{Kind: KeyPageDown}
395 case "200":
396 // Start of bracketed paste.
397 return r.readPaste()
398 }
399 }
400 return Key{Kind: KeyUnknown}
401}
402
403func parseCSIModifiers(params string) (shift, alt, super bool) {
404 if params == "" {
405 return false, false, false
406 }
407 i := strings.LastIndexByte(params, ';')
408 if i < 0 || i+1 >= len(params) {
409 return false, false, false
410 }
411 mod, ok := parseModifierParam(params[i+1:])
412 if !ok {
413 return false, false, false
414 }
415 shift, alt, _, super = modifierBits(mod)
416 return shift, alt, super
417}
418
419func parseCSIU(params string) (Key, bool) {
420 parts := strings.Split(params, ";")
421 if len(parts) == 0 {
422 return Key{}, false
423 }
424 // Kitty may include shifted and base-layout alternatives (code:shifted:base).
425 // The primary code preserves the user's layout, including AZERTY punctuation.
426 primary, _, _ := strings.Cut(parts[0], ":")
427 code, err := strconv.Atoi(primary)
428 if err != nil {
429 return Key{}, false
430 }
431 mod := 1
432 if len(parts) >= 2 {
433 var ok bool
434 mod, ok = parseModifierParam(parts[1])
435 if !ok {
436 return Key{}, false
437 }
438 }
439 return keyFromModifiedCode(code, mod)
440}
441
442func parseModifyOtherKeys(params string) (Key, bool) {
443 parts := strings.Split(params, ";")
444 if len(parts) != 3 || parts[0] != "27" {
445 return Key{}, false
446 }
447 mod, ok := parseModifierParam(parts[1])
448 if !ok {
449 return Key{}, false
450 }
451 code, err := strconv.Atoi(parts[2])
452 if err != nil {
453 return Key{}, false
454 }
455 return keyFromModifiedCode(code, mod)
456}
457
458func parseModifierParam(s string) (int, bool) {
459 if i := strings.IndexByte(s, ':'); i >= 0 {
460 s = s[:i]
461 }
462 mod, err := strconv.Atoi(s)
463 if err != nil {
464 return 0, false
465 }
466 return mod, true
467}
468
469func modifierBits(mod int) (shift, alt, ctrl, super bool) {
470 bits := mod - 1
471 return bits&1 != 0, bits&2 != 0, bits&4 != 0, bits&8 != 0 || bits&32 != 0
472}
473
474func keyFromModifiedCode(code, mod int) (Key, bool) {
475 shift, alt, ctrl, super := modifierBits(mod)
476 // Kitty keyboard protocol (CSI ... u) reports control keys as their
477 // codepoints: Esc=27, Enter=13, Tab=9, Backspace=127. Without the
478 // enhanced-mode handling these arrive as raw bytes; with it enabled
479 // they come through here, so map them back to their dedicated keys.
480 switch code {
481 case 13:
482 return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
483 case 27:
484 return Key{Kind: KeyEsc, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
485 case 9:
486 if shift {
487 return Key{Kind: KeyShiftTab, Alt: alt, Ctrl: ctrl, Super: super}, true
488 }
489 return Key{Kind: KeyTab, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
490 case 127, 8:
491 return Key{Kind: KeyBackspace, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
492 }
493 if ctrl {
494 switch code {
495 case 'c', 'C':
496 return Key{Kind: KeyCtrlC, Shift: shift, Alt: alt, Ctrl: true}, true
497 case 'd', 'D':
498 return Key{Kind: KeyCtrlD, Shift: shift, Alt: alt, Ctrl: true}, true
499 case 'l', 'L':
500 return Key{Kind: KeyCtrlL, Shift: shift, Alt: alt, Ctrl: true}, true
501 case 'u', 'U':
502 return Key{Kind: KeyCtrlU, Shift: shift, Alt: alt, Ctrl: true}, true
503 case 'k', 'K':
504 return Key{Kind: KeyCtrlK, Shift: shift, Alt: alt, Ctrl: true}, true
505 case 'a', 'A':
506 return Key{Kind: KeyCtrlA, Shift: shift, Alt: alt, Ctrl: true}, true
507 case 'e', 'E':
508 return Key{Kind: KeyCtrlE, Shift: shift, Alt: alt, Ctrl: true}, true
509 case 'w', 'W':
510 return Key{Kind: KeyCtrlW, Shift: shift, Alt: alt, Ctrl: true}, true
511 case 'o', 'O':
512 return Key{Kind: KeyCtrlO, Shift: shift, Alt: alt, Ctrl: true}, true
513 case 'r', 'R':
514 return Key{Kind: KeyCtrlR, Shift: shift, Alt: alt, Ctrl: true}, true
515 case 'v', 'V':
516 return Key{Kind: KeyPasteClipboard, Shift: shift, Alt: alt, Ctrl: true}, true
517 }
518 }
519 if code >= '0' && code <= '9' {
520 return Key{Kind: KeyRune, Rune: rune(code), Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
521 }
522 // Only the AZERTY profile shortcuts are printable Ctrl exceptions. Other
523 // Ctrl chords must remain unknown rather than becoming editor text.
524 azertyProfileKey := !alt && strings.ContainsRune("&é\"'(-è_ç", rune(code))
525 if code >= 0x20 && code <= unicode.MaxRune && unicode.IsPrint(rune(code)) && (!ctrl || azertyProfileKey) {
526 return Key{Kind: KeyRune, Rune: rune(code), Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
527 }
528 return Key{}, false
529}
530
531// readPaste reads until ESC [ 2 0 1 ~ and returns the pasted text.
532func (r *Reader) readPaste() Key {
533 var sb strings.Builder
534 const end = "\x1b[201~"
535 tail := make([]byte, 0, len(end))
536 for {
537 b, err := r.readByte()
538 if err != nil {
539 break
540 }
541 tail = append(tail, b)
542 if len(tail) > len(end) {
543 sb.WriteByte(tail[0])
544 tail = tail[1:]
545 }
546 if string(tail) == end {
547 break
548 }
549 }
550 return Key{Kind: KeyPaste, Paste: sb.String()}
551}