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 >= 0x01 && b <= 0x1a:
104 // Legacy terminals encode Ctrl+A..Ctrl+Z as the control bytes
105 // 0x01..0x1a. The bindings above have dedicated KeyKinds; the
106 // remaining letters surface as Ctrl-modified runes so keymaps
107 // can bind them. Legacy encodings cannot carry Shift, so
108 // Ctrl+Shift+letter arrives identically; only enhanced keyboard
109 // protocols distinguish the two. Consumers must treat a Ctrl
110 // rune as a chord, never as typed text.
111 return Key{Kind: KeyRune, Rune: rune('a' + b - 1), Ctrl: true}, nil
112 case b == 0x1b:
113 return r.readEscape()
114 case b < 0x20:
115 return Key{Kind: KeyUnknown}, nil
116 }
117 // UTF-8 multibyte?
118 if b < 0x80 {
119 return Key{Kind: KeyRune, Rune: rune(b)}, nil
120 }
121 // Decode UTF-8 (up to 4 bytes).
122 n := utf8Len(b)
123 buf := []byte{b}
124 for i := 1; i < n; i++ {
125 bb, err := r.src()
126 if err != nil {
127 return Key{}, err
128 }
129 buf = append(buf, bb)
130 }
131 rn, _ := decodeRune(buf)
132 return Key{Kind: KeyRune, Rune: rn}, nil
133}
134
135func utf8Len(b byte) int {
136 switch {
137 case b&0xe0 == 0xc0:
138 return 2
139 case b&0xf0 == 0xe0:
140 return 3
141 case b&0xf8 == 0xf0:
142 return 4
143 }
144 return 1
145}
146
147func decodeRune(b []byte) (rune, int) {
148 // Minimal decoder; invalid runes become U+FFFD.
149 if len(b) == 1 {
150 return rune(b[0]), 1
151 }
152 var r rune
153 switch len(b) {
154 case 2:
155 r = rune(b[0]&0x1f)<<6 | rune(b[1]&0x3f)
156 case 3:
157 r = rune(b[0]&0x0f)<<12 | rune(b[1]&0x3f)<<6 | rune(b[2]&0x3f)
158 case 4:
159 r = rune(b[0]&0x07)<<18 | rune(b[1]&0x3f)<<12 | rune(b[2]&0x3f)<<6 | rune(b[3]&0x3f)
160 default:
161 r = 0xFFFD
162 }
163 return r, len(b)
164}
165
166// readEscape handles sequences starting with 0x1b.
167func (r *Reader) readEscape() (Key, error) {
168 // Bare ESC: maybe followed by another byte within a short window.
169 b, have, err := r.readEscapeNext(50 * time.Millisecond)
170 if err != nil || !have {
171 return Key{Kind: KeyEsc}, nil
172 }
173 switch b {
174 case '[':
175 return r.readCSI()
176 case 'O':
177 // SS3 sequences. Terminals in application cursor mode use these
178 // for arrows, while some terminals also use them for Home and End.
179 c, err := r.src()
180 if err != nil {
181 return Key{}, err
182 }
183 switch c {
184 case 'A':
185 return Key{Kind: KeyUp}, nil
186 case 'B':
187 return Key{Kind: KeyDown}, nil
188 case 'C':
189 return Key{Kind: KeyRight}, nil
190 case 'D':
191 return Key{Kind: KeyLeft}, nil
192 case 'H':
193 return Key{Kind: KeyHome}, nil
194 case 'F':
195 return Key{Kind: KeyEnd}, nil
196 }
197 return Key{Kind: KeyUnknown}, nil
198 case 0x7f, 0x08:
199 // Alt+Backspace (Option+Delete on macOS) — most terminals send
200 // ESC + DEL for this. Surface as a dedicated "alt backspace"
201 // so the editor can map it to delete-word.
202 return Key{Kind: KeyBackspace, Alt: true}, nil
203 case 'b':
204 // Emacs-style word-left, also emitted by some terminals for
205 // Option+LeftArrow.
206 return Key{Kind: KeyLeft, Alt: true}, nil
207 case 'f':
208 // Emacs-style word-right, also emitted for Option+RightArrow.
209 return Key{Kind: KeyRight, Alt: true}, nil
210 default:
211 // Alt+<char>
212 if b < 0x80 {
213 return Key{Kind: KeyRune, Rune: rune(b), Alt: true}, nil
214 }
215 }
216 return Key{Kind: KeyUnknown}, nil
217}
218
219// readEscapeNext tries to read one byte within d. If peek is available
220// we use it (true non-blocking). Otherwise we fall back to a blocking
221// read, which means bare Esc is only detected after the next keystroke.
222func (r *Reader) readEscapeNext(d time.Duration) (byte, bool, error) {
223 if r.peek != nil {
224 return r.peek(d)
225 }
226 b, err := r.src()
227 if err != nil {
228 return 0, false, err
229 }
230 return b, true, nil
231}
232
233// readCSI parses a CSI sequence after ESC [.
234func (r *Reader) readCSI() (Key, error) {
235 var params []byte
236 for {
237 c, err := r.src()
238 if err != nil {
239 return Key{}, err
240 }
241 if c >= 0x30 && c <= 0x3f {
242 params = append(params, c)
243 continue
244 }
245 // Final byte.
246 return r.dispatchCSI(string(params), c), nil
247 }
248}
249
250func (r *Reader) dispatchCSI(params string, final byte) Key {
251 // SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
252 // button codes 64 (up) and 65 (down). We ignore coordinates for
253 // now; the chat view only needs scroll direction.
254 if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
255 parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
256 if len(parts) >= 1 {
257 switch parts[0] {
258 case "64":
259 return Key{Kind: KeyMouseWheelUp}
260 case "65":
261 return Key{Kind: KeyMouseWheelDown}
262 }
263 }
264 return Key{Kind: KeyUnknown}
265 }
266
267 shift, alt, super := parseCSIModifiers(params)
268 if final == 'u' {
269 if key, ok := parseCSIU(params); ok {
270 return key
271 }
272 }
273 if final == '~' {
274 if key, ok := parseModifyOtherKeys(params); ok {
275 return key
276 }
277 }
278 switch final {
279 case 'A':
280 return Key{Kind: KeyUp, Alt: alt, Shift: shift, Super: super}
281 case 'B':
282 return Key{Kind: KeyDown, Alt: alt, Shift: shift, Super: super}
283 case 'C':
284 return Key{Kind: KeyRight, Alt: alt, Shift: shift, Super: super}
285 case 'D':
286 return Key{Kind: KeyLeft, Alt: alt, Shift: shift, Super: super}
287 case 'H':
288 return Key{Kind: KeyHome}
289 case 'F':
290 return Key{Kind: KeyEnd}
291 case 'Z':
292 return Key{Kind: KeyShiftTab}
293 case '~':
294 switch params {
295 case "3":
296 return Key{Kind: KeyDelete}
297 case "5":
298 return Key{Kind: KeyPageUp}
299 case "6":
300 return Key{Kind: KeyPageDown}
301 case "200":
302 // Start of bracketed paste.
303 return r.readPaste()
304 }
305 }
306 return Key{Kind: KeyUnknown}
307}
308
309func parseCSIModifiers(params string) (shift, alt, super bool) {
310 if params == "" {
311 return false, false, false
312 }
313 i := strings.LastIndexByte(params, ';')
314 if i < 0 || i+1 >= len(params) {
315 return false, false, false
316 }
317 mod, ok := parseModifierParam(params[i+1:])
318 if !ok {
319 return false, false, false
320 }
321 shift, alt, _, super = modifierBits(mod)
322 return shift, alt, super
323}
324
325func parseCSIU(params string) (Key, bool) {
326 parts := strings.Split(params, ";")
327 if len(parts) == 0 {
328 return Key{}, false
329 }
330 code, err := strconv.Atoi(parts[0])
331 if err != nil {
332 return Key{}, false
333 }
334 mod := 1
335 if len(parts) >= 2 {
336 var ok bool
337 mod, ok = parseModifierParam(parts[1])
338 if !ok {
339 return Key{}, false
340 }
341 }
342 return keyFromModifiedCode(code, mod)
343}
344
345func parseModifyOtherKeys(params string) (Key, bool) {
346 parts := strings.Split(params, ";")
347 if len(parts) != 3 || parts[0] != "27" {
348 return Key{}, false
349 }
350 mod, ok := parseModifierParam(parts[1])
351 if !ok {
352 return Key{}, false
353 }
354 code, err := strconv.Atoi(parts[2])
355 if err != nil {
356 return Key{}, false
357 }
358 return keyFromModifiedCode(code, mod)
359}
360
361func parseModifierParam(s string) (int, bool) {
362 if i := strings.IndexByte(s, ':'); i >= 0 {
363 s = s[:i]
364 }
365 mod, err := strconv.Atoi(s)
366 if err != nil {
367 return 0, false
368 }
369 return mod, true
370}
371
372func modifierBits(mod int) (shift, alt, ctrl, super bool) {
373 bits := mod - 1
374 return bits&1 != 0, bits&2 != 0, bits&4 != 0, bits&8 != 0 || bits&32 != 0
375}
376
377func keyFromModifiedCode(code, mod int) (Key, bool) {
378 shift, alt, ctrl, super := modifierBits(mod)
379 // Kitty keyboard protocol (CSI ... u) reports control keys as their
380 // codepoints: Esc=27, Enter=13, Tab=9, Backspace=127. Without the
381 // enhanced-mode handling these arrive as raw bytes; with it enabled
382 // they come through here, so map them back to their dedicated keys.
383 switch code {
384 case 13:
385 return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
386 case 27:
387 return Key{Kind: KeyEsc, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
388 case 9:
389 if shift {
390 return Key{Kind: KeyShiftTab, Alt: alt, Ctrl: ctrl, Super: super}, true
391 }
392 return Key{Kind: KeyTab, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
393 case 127, 8:
394 return Key{Kind: KeyBackspace, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
395 }
396 if ctrl {
397 switch code {
398 case 'c', 'C':
399 return Key{Kind: KeyCtrlC, Shift: shift, Alt: alt, Ctrl: true}, true
400 case 'd', 'D':
401 return Key{Kind: KeyCtrlD, Shift: shift, Alt: alt, Ctrl: true}, true
402 case 'l', 'L':
403 return Key{Kind: KeyCtrlL, Shift: shift, Alt: alt, Ctrl: true}, true
404 case 'u', 'U':
405 return Key{Kind: KeyCtrlU, Shift: shift, Alt: alt, Ctrl: true}, true
406 case 'k', 'K':
407 return Key{Kind: KeyCtrlK, Shift: shift, Alt: alt, Ctrl: true}, true
408 case 'a', 'A':
409 return Key{Kind: KeyCtrlA, Shift: shift, Alt: alt, Ctrl: true}, true
410 case 'e', 'E':
411 return Key{Kind: KeyCtrlE, Shift: shift, Alt: alt, Ctrl: true}, true
412 case 'w', 'W':
413 return Key{Kind: KeyCtrlW, Shift: shift, Alt: alt, Ctrl: true}, true
414 case 'o', 'O':
415 return Key{Kind: KeyCtrlO, Shift: shift, Alt: alt, Ctrl: true}, true
416 case 'v', 'V':
417 return Key{Kind: KeyPasteClipboard, Shift: shift, Alt: alt, Ctrl: true}, true
418 }
419 // Enhanced keyboard protocols preserve Ctrl for letters that do
420 // not have a dedicated KeyKind, such as Ctrl+H and Ctrl+S.
421 if (code >= 'a' && code <= 'z') || (code >= 'A' && code <= 'Z') || (code >= 0x20 && code <= 0x7e) {
422 return Key{Kind: KeyRune, Rune: rune(code), Shift: shift, Alt: alt, Ctrl: true, Super: super}, true
423 }
424 }
425 if code >= '0' && code <= '9' {
426 return Key{Kind: KeyRune, Rune: rune(code), Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
427 }
428 if code >= 0x20 && code <= 0x7e && (shift || alt || super) {
429 return Key{Kind: KeyRune, Rune: rune(code), Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
430 }
431 return Key{}, false
432}
433
434// readPaste reads until ESC [ 2 0 1 ~ and returns the pasted text.
435func (r *Reader) readPaste() Key {
436 var sb strings.Builder
437 const end = "\x1b[201~"
438 tail := make([]byte, 0, len(end))
439 for {
440 b, err := r.src()
441 if err != nil {
442 break
443 }
444 tail = append(tail, b)
445 if len(tail) > len(end) {
446 sb.WriteByte(tail[0])
447 tail = tail[1:]
448 }
449 if string(tail) == end {
450 break
451 }
452 }
453 return Key{Kind: KeyPaste, Paste: sb.String()}
454}