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 (function keys in some terminals).
169 c, err := r.src()
170 if err != nil {
171 return Key{}, err
172 }
173 switch c {
174 case 'H':
175 return Key{Kind: KeyHome}, nil
176 case 'F':
177 return Key{Kind: KeyEnd}, nil
178 }
179 return Key{Kind: KeyUnknown}, nil
180 case 0x7f, 0x08:
181 // Alt+Backspace (Option+Delete on macOS) — most terminals send
182 // ESC + DEL for this. Surface as a dedicated "alt backspace"
183 // so the editor can map it to delete-word.
184 return Key{Kind: KeyBackspace, Alt: true}, nil
185 case 'b':
186 // Emacs-style word-left, also emitted by some terminals for
187 // Option+LeftArrow.
188 return Key{Kind: KeyLeft, Alt: true}, nil
189 case 'f':
190 // Emacs-style word-right, also emitted for Option+RightArrow.
191 return Key{Kind: KeyRight, Alt: true}, nil
192 default:
193 // Alt+<char>
194 if b < 0x80 {
195 return Key{Kind: KeyRune, Rune: rune(b), Alt: true}, nil
196 }
197 }
198 return Key{Kind: KeyUnknown}, nil
199}
200
201// readEscapeNext tries to read one byte within d. If peek is available
202// we use it (true non-blocking). Otherwise we fall back to a blocking
203// read, which means bare Esc is only detected after the next keystroke.
204func (r *Reader) readEscapeNext(d time.Duration) (byte, bool, error) {
205 if r.peek != nil {
206 return r.peek(d)
207 }
208 b, err := r.src()
209 if err != nil {
210 return 0, false, err
211 }
212 return b, true, nil
213}
214
215// readCSI parses a CSI sequence after ESC [.
216func (r *Reader) readCSI() (Key, error) {
217 var params []byte
218 for {
219 c, err := r.src()
220 if err != nil {
221 return Key{}, err
222 }
223 if c >= 0x30 && c <= 0x3f {
224 params = append(params, c)
225 continue
226 }
227 // Final byte.
228 return r.dispatchCSI(string(params), c), nil
229 }
230}
231
232func (r *Reader) dispatchCSI(params string, final byte) Key {
233 // SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
234 // button codes 64 (up) and 65 (down). We ignore coordinates for
235 // now; the chat view only needs scroll direction.
236 if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
237 parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
238 if len(parts) >= 1 {
239 switch parts[0] {
240 case "64":
241 return Key{Kind: KeyMouseWheelUp}
242 case "65":
243 return Key{Kind: KeyMouseWheelDown}
244 }
245 }
246 return Key{Kind: KeyUnknown}
247 }
248
249 shift, alt, super := parseCSIModifiers(params)
250 if final == 'u' {
251 if key, ok := parseCSIU(params); ok {
252 return key
253 }
254 }
255 if final == '~' {
256 if key, ok := parseModifyOtherKeys(params); ok {
257 return key
258 }
259 }
260 switch final {
261 case 'A':
262 return Key{Kind: KeyUp, Alt: alt, Shift: shift, Super: super}
263 case 'B':
264 return Key{Kind: KeyDown, Alt: alt, Shift: shift, Super: super}
265 case 'C':
266 return Key{Kind: KeyRight, Alt: alt, Shift: shift, Super: super}
267 case 'D':
268 return Key{Kind: KeyLeft, Alt: alt, Shift: shift, Super: super}
269 case 'H':
270 return Key{Kind: KeyHome}
271 case 'F':
272 return Key{Kind: KeyEnd}
273 case 'Z':
274 return Key{Kind: KeyShiftTab}
275 case '~':
276 switch params {
277 case "3":
278 return Key{Kind: KeyDelete}
279 case "5":
280 return Key{Kind: KeyPageUp}
281 case "6":
282 return Key{Kind: KeyPageDown}
283 case "200":
284 // Start of bracketed paste.
285 return r.readPaste()
286 }
287 }
288 return Key{Kind: KeyUnknown}
289}
290
291func parseCSIModifiers(params string) (shift, alt, super bool) {
292 if params == "" {
293 return false, false, false
294 }
295 i := strings.LastIndexByte(params, ';')
296 if i < 0 || i+1 >= len(params) {
297 return false, false, false
298 }
299 mod, ok := parseModifierParam(params[i+1:])
300 if !ok {
301 return false, false, false
302 }
303 shift, alt, _, super = modifierBits(mod)
304 return shift, alt, super
305}
306
307func parseCSIU(params string) (Key, bool) {
308 parts := strings.Split(params, ";")
309 if len(parts) == 0 {
310 return Key{}, false
311 }
312 code, err := strconv.Atoi(parts[0])
313 if err != nil {
314 return Key{}, false
315 }
316 mod := 1
317 if len(parts) >= 2 {
318 var ok bool
319 mod, ok = parseModifierParam(parts[1])
320 if !ok {
321 return Key{}, false
322 }
323 }
324 return keyFromModifiedCode(code, mod)
325}
326
327func parseModifyOtherKeys(params string) (Key, bool) {
328 parts := strings.Split(params, ";")
329 if len(parts) != 3 || parts[0] != "27" {
330 return Key{}, false
331 }
332 mod, ok := parseModifierParam(parts[1])
333 if !ok {
334 return Key{}, false
335 }
336 code, err := strconv.Atoi(parts[2])
337 if err != nil {
338 return Key{}, false
339 }
340 return keyFromModifiedCode(code, mod)
341}
342
343func parseModifierParam(s string) (int, bool) {
344 if i := strings.IndexByte(s, ':'); i >= 0 {
345 s = s[:i]
346 }
347 mod, err := strconv.Atoi(s)
348 if err != nil {
349 return 0, false
350 }
351 return mod, true
352}
353
354func modifierBits(mod int) (shift, alt, ctrl, super bool) {
355 bits := mod - 1
356 return bits&1 != 0, bits&2 != 0, bits&4 != 0, bits&8 != 0 || bits&32 != 0
357}
358
359func keyFromModifiedCode(code, mod int) (Key, bool) {
360 shift, alt, ctrl, super := modifierBits(mod)
361 // Kitty keyboard protocol (CSI ... u) reports control keys as their
362 // codepoints: Esc=27, Enter=13, Tab=9, Backspace=127. Without the
363 // enhanced-mode handling these arrive as raw bytes; with it enabled
364 // they come through here, so map them back to their dedicated keys.
365 switch code {
366 case 13:
367 return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
368 case 27:
369 return Key{Kind: KeyEsc, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
370 case 9:
371 if shift {
372 return Key{Kind: KeyShiftTab, Alt: alt, Ctrl: ctrl, Super: super}, true
373 }
374 return Key{Kind: KeyTab, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
375 case 127, 8:
376 return Key{Kind: KeyBackspace, Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
377 }
378 if ctrl {
379 switch code {
380 case 'c', 'C':
381 return Key{Kind: KeyCtrlC, Shift: shift, Alt: alt, Ctrl: true}, true
382 case 'd', 'D':
383 return Key{Kind: KeyCtrlD, Shift: shift, Alt: alt, Ctrl: true}, true
384 case 'l', 'L':
385 return Key{Kind: KeyCtrlL, Shift: shift, Alt: alt, Ctrl: true}, true
386 case 'u', 'U':
387 return Key{Kind: KeyCtrlU, Shift: shift, Alt: alt, Ctrl: true}, true
388 case 'k', 'K':
389 return Key{Kind: KeyCtrlK, Shift: shift, Alt: alt, Ctrl: true}, true
390 case 'a', 'A':
391 return Key{Kind: KeyCtrlA, Shift: shift, Alt: alt, Ctrl: true}, true
392 case 'e', 'E':
393 return Key{Kind: KeyCtrlE, Shift: shift, Alt: alt, Ctrl: true}, true
394 case 'w', 'W':
395 return Key{Kind: KeyCtrlW, Shift: shift, Alt: alt, Ctrl: true}, true
396 case 'o', 'O':
397 return Key{Kind: KeyCtrlO, Shift: shift, Alt: alt, Ctrl: true}, true
398 case 'v', 'V':
399 return Key{Kind: KeyPasteClipboard, Shift: shift, Alt: alt, Ctrl: true}, true
400 }
401 }
402 if code >= '0' && code <= '9' {
403 return Key{Kind: KeyRune, Rune: rune(code), Shift: shift, Alt: alt, Ctrl: ctrl, Super: super}, true
404 }
405 return Key{}, false
406}
407
408// readPaste reads until ESC [ 2 0 1 ~ and returns the pasted text.
409func (r *Reader) readPaste() Key {
410 var sb strings.Builder
411 const end = "\x1b[201~"
412 tail := make([]byte, 0, len(end))
413 for {
414 b, err := r.src()
415 if err != nil {
416 break
417 }
418 tail = append(tail, b)
419 if len(tail) > len(end) {
420 sb.WriteByte(tail[0])
421 tail = tail[1:]
422 }
423 if string(tail) == end {
424 break
425 }
426 }
427 return Key{Kind: KeyPaste, Paste: sb.String()}
428}