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