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}
18
19type KeyKind int
20
21const (
22 KeyRune KeyKind = iota
23 KeyEnter
24 KeyBackspace
25 KeyTab
26 KeyShiftTab
27 KeyEsc
28 KeyUp
29 KeyDown
30 KeyLeft
31 KeyRight
32 KeyHome
33 KeyEnd
34 KeyPageUp
35 KeyPageDown
36 KeyDelete
37 KeyCtrlC
38 KeyCtrlD
39 KeyCtrlL
40 KeyCtrlU
41 KeyCtrlK
42 KeyCtrlA
43 KeyCtrlE
44 KeyCtrlW
45 KeyCtrlO
46 KeyPaste
47 KeyMouseWheelUp
48 KeyMouseWheelDown
49 KeyUnknown
50)
51
52// Reader parses a byte stream into Key events. It understands basic
53// xterm escape sequences and bracketed paste.
54type Reader struct {
55 src func() (byte, error)
56 peek func(time.Duration) (byte, bool, error) // optional; may be nil
57}
58
59// NewReader returns a Reader that pulls bytes from read.
60func NewReader(read func() (byte, error)) *Reader { return &Reader{src: read} }
61
62// NewReaderWithPeek returns a Reader that pulls bytes from read and uses
63// peek to disambiguate bare Esc from the start of an escape sequence.
64func NewReaderWithPeek(read func() (byte, error), peek func(time.Duration) (byte, bool, error)) *Reader {
65 return &Reader{src: read, peek: peek}
66}
67
68// Read returns the next parsed Key.
69func (r *Reader) Read() (Key, error) {
70 b, err := r.src()
71 if err != nil {
72 return Key{}, err
73 }
74 switch {
75 case b == 0x03:
76 return Key{Kind: KeyCtrlC}, nil
77 case b == 0x04:
78 return Key{Kind: KeyCtrlD}, nil
79 case b == 0x0c:
80 return Key{Kind: KeyCtrlL}, nil
81 case b == 0x15:
82 return Key{Kind: KeyCtrlU}, nil
83 case b == 0x0b:
84 return Key{Kind: KeyCtrlK}, nil
85 case b == 0x01:
86 return Key{Kind: KeyCtrlA}, nil
87 case b == 0x05:
88 return Key{Kind: KeyCtrlE}, nil
89 case b == 0x17:
90 return Key{Kind: KeyCtrlW}, nil
91 case b == 0x0f:
92 return Key{Kind: KeyCtrlO}, nil
93 case b == '\r', b == '\n':
94 return Key{Kind: KeyEnter}, nil
95 case b == '\t':
96 return Key{Kind: KeyTab}, nil
97 case b == 0x7f, b == 0x08:
98 return Key{Kind: KeyBackspace}, nil
99 case b == 0x1b:
100 return r.readEscape()
101 case b < 0x20:
102 return Key{Kind: KeyUnknown}, nil
103 }
104 // UTF-8 multibyte?
105 if b < 0x80 {
106 return Key{Kind: KeyRune, Rune: rune(b)}, nil
107 }
108 // Decode UTF-8 (up to 4 bytes).
109 n := utf8Len(b)
110 buf := []byte{b}
111 for i := 1; i < n; i++ {
112 bb, err := r.src()
113 if err != nil {
114 return Key{}, err
115 }
116 buf = append(buf, bb)
117 }
118 rn, _ := decodeRune(buf)
119 return Key{Kind: KeyRune, Rune: rn}, nil
120}
121
122func utf8Len(b byte) int {
123 switch {
124 case b&0xe0 == 0xc0:
125 return 2
126 case b&0xf0 == 0xe0:
127 return 3
128 case b&0xf8 == 0xf0:
129 return 4
130 }
131 return 1
132}
133
134func decodeRune(b []byte) (rune, int) {
135 // Minimal decoder; invalid runes become U+FFFD.
136 if len(b) == 1 {
137 return rune(b[0]), 1
138 }
139 var r rune
140 switch len(b) {
141 case 2:
142 r = rune(b[0]&0x1f)<<6 | rune(b[1]&0x3f)
143 case 3:
144 r = rune(b[0]&0x0f)<<12 | rune(b[1]&0x3f)<<6 | rune(b[2]&0x3f)
145 case 4:
146 r = rune(b[0]&0x07)<<18 | rune(b[1]&0x3f)<<12 | rune(b[2]&0x3f)<<6 | rune(b[3]&0x3f)
147 default:
148 r = 0xFFFD
149 }
150 return r, len(b)
151}
152
153// readEscape handles sequences starting with 0x1b.
154func (r *Reader) readEscape() (Key, error) {
155 // Bare ESC: maybe followed by another byte within a short window.
156 b, have, err := r.readEscapeNext(50 * time.Millisecond)
157 if err != nil || !have {
158 return Key{Kind: KeyEsc}, nil
159 }
160 switch b {
161 case '[':
162 return r.readCSI()
163 case 'O':
164 // SS3 sequences (function keys in some terminals).
165 c, err := r.src()
166 if err != nil {
167 return Key{}, err
168 }
169 switch c {
170 case 'H':
171 return Key{Kind: KeyHome}, nil
172 case 'F':
173 return Key{Kind: KeyEnd}, nil
174 }
175 return Key{Kind: KeyUnknown}, nil
176 case 0x7f, 0x08:
177 // Alt+Backspace (Option+Delete on macOS) — most terminals send
178 // ESC + DEL for this. Surface as a dedicated "alt backspace"
179 // so the editor can map it to delete-word.
180 return Key{Kind: KeyBackspace, Alt: true}, nil
181 case 'b':
182 // Emacs-style word-left, also emitted by some terminals for
183 // Option+LeftArrow.
184 return Key{Kind: KeyLeft, Alt: true}, nil
185 case 'f':
186 // Emacs-style word-right, also emitted for Option+RightArrow.
187 return Key{Kind: KeyRight, Alt: true}, nil
188 default:
189 // Alt+<char>
190 if b < 0x80 {
191 return Key{Kind: KeyRune, Rune: rune(b), Alt: true}, nil
192 }
193 }
194 return Key{Kind: KeyUnknown}, nil
195}
196
197// readEscapeNext tries to read one byte within d. If peek is available
198// we use it (true non-blocking). Otherwise we fall back to a blocking
199// read, which means bare Esc is only detected after the next keystroke.
200func (r *Reader) readEscapeNext(d time.Duration) (byte, bool, error) {
201 if r.peek != nil {
202 return r.peek(d)
203 }
204 b, err := r.src()
205 if err != nil {
206 return 0, false, err
207 }
208 return b, true, nil
209}
210
211// readCSI parses a CSI sequence after ESC [.
212func (r *Reader) readCSI() (Key, error) {
213 var params []byte
214 for {
215 c, err := r.src()
216 if err != nil {
217 return Key{}, err
218 }
219 if c >= 0x30 && c <= 0x3f {
220 params = append(params, c)
221 continue
222 }
223 // Final byte.
224 return r.dispatchCSI(string(params), c), nil
225 }
226}
227
228func (r *Reader) dispatchCSI(params string, final byte) Key {
229 // SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
230 // button codes 64 (up) and 65 (down). We ignore coordinates for
231 // now; the chat view only needs scroll direction.
232 if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
233 parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
234 if len(parts) >= 1 {
235 switch parts[0] {
236 case "64":
237 return Key{Kind: KeyMouseWheelUp}
238 case "65":
239 return Key{Kind: KeyMouseWheelDown}
240 }
241 }
242 return Key{Kind: KeyUnknown}
243 }
244
245 shift, alt := parseCSIModifiers(params)
246 if final == 'u' {
247 if key, ok := parseCSIU(params); ok {
248 return key
249 }
250 }
251 if final == '~' {
252 if key, ok := parseModifyOtherKeys(params); ok {
253 return key
254 }
255 }
256 switch final {
257 case 'A':
258 return Key{Kind: KeyUp, Alt: alt, Shift: shift}
259 case 'B':
260 return Key{Kind: KeyDown, Alt: alt, Shift: shift}
261 case 'C':
262 return Key{Kind: KeyRight, Alt: alt, Shift: shift}
263 case 'D':
264 return Key{Kind: KeyLeft, Alt: alt, Shift: shift}
265 case 'H':
266 return Key{Kind: KeyHome}
267 case 'F':
268 return Key{Kind: KeyEnd}
269 case 'Z':
270 return Key{Kind: KeyShiftTab}
271 case '~':
272 switch params {
273 case "3":
274 return Key{Kind: KeyDelete}
275 case "5":
276 return Key{Kind: KeyPageUp}
277 case "6":
278 return Key{Kind: KeyPageDown}
279 case "200":
280 // Start of bracketed paste.
281 return r.readPaste()
282 }
283 }
284 return Key{Kind: KeyUnknown}
285}
286
287func parseCSIModifiers(params string) (shift, alt bool) {
288 if params == "" {
289 return false, false
290 }
291 i := strings.LastIndexByte(params, ';')
292 if i < 0 || i+1 >= len(params) {
293 return false, false
294 }
295 mod, err := strconv.Atoi(params[i+1:])
296 if err != nil {
297 return false, false
298 }
299 // Xterm-style modifier values are 1 plus a bitmask:
300 // 2=Shift, 3=Alt, 4=Shift+Alt, 5=Ctrl, 6=Shift+Ctrl,
301 // 7=Alt+Ctrl, 8=Shift+Alt+Ctrl.
302 bits := mod - 1
303 return bits&1 != 0, bits&2 != 0
304}
305
306func parseCSIU(params string) (Key, bool) {
307 parts := strings.Split(params, ";")
308 if len(parts) == 0 {
309 return Key{}, false
310 }
311 code, err := strconv.Atoi(parts[0])
312 if err != nil {
313 return Key{}, false
314 }
315 mod := 1
316 if len(parts) >= 2 {
317 if mod, err = strconv.Atoi(parts[1]); err != nil {
318 return Key{}, false
319 }
320 }
321 return keyFromModifiedCode(code, mod)
322}
323
324func parseModifyOtherKeys(params string) (Key, bool) {
325 parts := strings.Split(params, ";")
326 if len(parts) != 3 || parts[0] != "27" {
327 return Key{}, false
328 }
329 mod, err := strconv.Atoi(parts[1])
330 if err != nil {
331 return Key{}, false
332 }
333 code, err := strconv.Atoi(parts[2])
334 if err != nil {
335 return Key{}, false
336 }
337 return keyFromModifiedCode(code, mod)
338}
339
340func keyFromModifiedCode(code, mod int) (Key, bool) {
341 bits := mod - 1
342 shift := bits&1 != 0
343 alt := bits&2 != 0
344 ctrl := bits&4 != 0
345 // Kitty keyboard protocol (CSI ... u) reports control keys as their
346 // codepoints: Esc=27, Enter=13, Tab=9, Backspace=127. Without the
347 // enhanced-mode handling these arrive as raw bytes; with it enabled
348 // they come through here, so map them back to their dedicated keys.
349 switch code {
350 case 13:
351 return Key{Kind: KeyEnter, Shift: shift, Alt: alt, Ctrl: ctrl}, true
352 case 27:
353 return Key{Kind: KeyEsc, Shift: shift, Alt: alt, Ctrl: ctrl}, true
354 case 9:
355 if shift {
356 return Key{Kind: KeyShiftTab, Alt: alt, Ctrl: ctrl}, true
357 }
358 return Key{Kind: KeyTab, Shift: shift, Alt: alt, Ctrl: ctrl}, true
359 case 127, 8:
360 return Key{Kind: KeyBackspace, Shift: shift, Alt: alt, Ctrl: ctrl}, true
361 }
362 if ctrl {
363 switch code {
364 case 'c', 'C':
365 return Key{Kind: KeyCtrlC, Shift: shift, Alt: alt, Ctrl: true}, true
366 case 'd', 'D':
367 return Key{Kind: KeyCtrlD, Shift: shift, Alt: alt, Ctrl: true}, true
368 case 'l', 'L':
369 return Key{Kind: KeyCtrlL, Shift: shift, Alt: alt, Ctrl: true}, true
370 case 'u', 'U':
371 return Key{Kind: KeyCtrlU, Shift: shift, Alt: alt, Ctrl: true}, true
372 case 'k', 'K':
373 return Key{Kind: KeyCtrlK, Shift: shift, Alt: alt, Ctrl: true}, true
374 case 'a', 'A':
375 return Key{Kind: KeyCtrlA, Shift: shift, Alt: alt, Ctrl: true}, true
376 case 'e', 'E':
377 return Key{Kind: KeyCtrlE, Shift: shift, Alt: alt, Ctrl: true}, true
378 case 'w', 'W':
379 return Key{Kind: KeyCtrlW, Shift: shift, Alt: alt, Ctrl: true}, true
380 case 'o', 'O':
381 return Key{Kind: KeyCtrlO, Shift: shift, Alt: alt, Ctrl: true}, true
382 }
383 }
384 return Key{}, false
385}
386
387// readPaste reads until ESC [ 2 0 1 ~ and returns the pasted text.
388func (r *Reader) readPaste() Key {
389 var sb strings.Builder
390 const end = "\x1b[201~"
391 tail := make([]byte, 0, len(end))
392 for {
393 b, err := r.src()
394 if err != nil {
395 break
396 }
397 tail = append(tail, b)
398 if len(tail) > len(end) {
399 sb.WriteByte(tail[0])
400 tail = tail[1:]
401 }
402 if string(tail) == end {
403 break
404 }
405 }
406 return Key{Kind: KeyPaste, Paste: sb.String()}
407}