1package tui
2
3import (
4 "strings"
5 "time"
6)
7
8// Key is a parsed keypress.
9type Key struct {
10 Kind KeyKind
11 Rune rune // for KeyRune
12 Paste string // for KeyPaste
13 Ctrl bool
14 Alt bool
15}
16
17type KeyKind int
18
19const (
20 KeyRune KeyKind = iota
21 KeyEnter
22 KeyBackspace
23 KeyTab
24 KeyShiftTab
25 KeyEsc
26 KeyUp
27 KeyDown
28 KeyLeft
29 KeyRight
30 KeyHome
31 KeyEnd
32 KeyPageUp
33 KeyPageDown
34 KeyDelete
35 KeyCtrlC
36 KeyCtrlD
37 KeyCtrlL
38 KeyCtrlU
39 KeyCtrlK
40 KeyCtrlA
41 KeyCtrlE
42 KeyCtrlW
43 KeyCtrlO
44 KeyPaste
45 KeyMouseWheelUp
46 KeyMouseWheelDown
47 KeyUnknown
48)
49
50// Reader parses a byte stream into Key events. It understands basic
51// xterm escape sequences and bracketed paste.
52type Reader struct {
53 src func() (byte, error)
54 peek func(time.Duration) (byte, bool, error) // optional; may be nil
55}
56
57// NewReader returns a Reader that pulls bytes from read.
58func NewReader(read func() (byte, error)) *Reader { return &Reader{src: read} }
59
60// NewReaderWithPeek returns a Reader that pulls bytes from read and uses
61// peek to disambiguate bare Esc from the start of an escape sequence.
62func NewReaderWithPeek(read func() (byte, error), peek func(time.Duration) (byte, bool, error)) *Reader {
63 return &Reader{src: read, peek: peek}
64}
65
66// Read returns the next parsed Key.
67func (r *Reader) Read() (Key, error) {
68 b, err := r.src()
69 if err != nil {
70 return Key{}, err
71 }
72 switch {
73 case b == 0x03:
74 return Key{Kind: KeyCtrlC}, nil
75 case b == 0x04:
76 return Key{Kind: KeyCtrlD}, nil
77 case b == 0x0c:
78 return Key{Kind: KeyCtrlL}, nil
79 case b == 0x15:
80 return Key{Kind: KeyCtrlU}, nil
81 case b == 0x0b:
82 return Key{Kind: KeyCtrlK}, nil
83 case b == 0x01:
84 return Key{Kind: KeyCtrlA}, nil
85 case b == 0x05:
86 return Key{Kind: KeyCtrlE}, nil
87 case b == 0x17:
88 return Key{Kind: KeyCtrlW}, nil
89 case b == 0x0f:
90 return Key{Kind: KeyCtrlO}, nil
91 case b == '\r', b == '\n':
92 return Key{Kind: KeyEnter}, nil
93 case b == '\t':
94 return Key{Kind: KeyTab}, nil
95 case b == 0x7f, b == 0x08:
96 return Key{Kind: KeyBackspace}, nil
97 case b == 0x1b:
98 return r.readEscape()
99 case b < 0x20:
100 return Key{Kind: KeyUnknown}, nil
101 }
102 // UTF-8 multibyte?
103 if b < 0x80 {
104 return Key{Kind: KeyRune, Rune: rune(b)}, nil
105 }
106 // Decode UTF-8 (up to 4 bytes).
107 n := utf8Len(b)
108 buf := []byte{b}
109 for i := 1; i < n; i++ {
110 bb, err := r.src()
111 if err != nil {
112 return Key{}, err
113 }
114 buf = append(buf, bb)
115 }
116 rn, _ := decodeRune(buf)
117 return Key{Kind: KeyRune, Rune: rn}, nil
118}
119
120func utf8Len(b byte) int {
121 switch {
122 case b&0xe0 == 0xc0:
123 return 2
124 case b&0xf0 == 0xe0:
125 return 3
126 case b&0xf8 == 0xf0:
127 return 4
128 }
129 return 1
130}
131
132func decodeRune(b []byte) (rune, int) {
133 // Minimal decoder; invalid runes become U+FFFD.
134 if len(b) == 1 {
135 return rune(b[0]), 1
136 }
137 var r rune
138 switch len(b) {
139 case 2:
140 r = rune(b[0]&0x1f)<<6 | rune(b[1]&0x3f)
141 case 3:
142 r = rune(b[0]&0x0f)<<12 | rune(b[1]&0x3f)<<6 | rune(b[2]&0x3f)
143 case 4:
144 r = rune(b[0]&0x07)<<18 | rune(b[1]&0x3f)<<12 | rune(b[2]&0x3f)<<6 | rune(b[3]&0x3f)
145 default:
146 r = 0xFFFD
147 }
148 return r, len(b)
149}
150
151// readEscape handles sequences starting with 0x1b.
152func (r *Reader) readEscape() (Key, error) {
153 // Bare ESC: maybe followed by another byte within a short window.
154 b, have, err := r.readEscapeNext(50 * time.Millisecond)
155 if err != nil || !have {
156 return Key{Kind: KeyEsc}, nil
157 }
158 switch b {
159 case '[':
160 return r.readCSI()
161 case 'O':
162 // SS3 sequences (function keys in some terminals).
163 c, err := r.src()
164 if err != nil {
165 return Key{}, err
166 }
167 switch c {
168 case 'H':
169 return Key{Kind: KeyHome}, nil
170 case 'F':
171 return Key{Kind: KeyEnd}, nil
172 }
173 return Key{Kind: KeyUnknown}, nil
174 case 0x7f, 0x08:
175 // Alt+Backspace (Option+Delete on macOS) — most terminals send
176 // ESC + DEL for this. Surface as a dedicated "alt backspace"
177 // so the editor can map it to delete-word.
178 return Key{Kind: KeyBackspace, Alt: true}, nil
179 case 'b':
180 // Emacs-style word-left, also emitted by some terminals for
181 // Option+LeftArrow.
182 return Key{Kind: KeyLeft, Alt: true}, nil
183 case 'f':
184 // Emacs-style word-right, also emitted for Option+RightArrow.
185 return Key{Kind: KeyRight, Alt: true}, nil
186 default:
187 // Alt+<char>
188 if b < 0x80 {
189 return Key{Kind: KeyRune, Rune: rune(b), Alt: true}, nil
190 }
191 }
192 return Key{Kind: KeyUnknown}, nil
193}
194
195// readEscapeNext tries to read one byte within d. If peek is available
196// we use it (true non-blocking). Otherwise we fall back to a blocking
197// read, which means bare Esc is only detected after the next keystroke.
198func (r *Reader) readEscapeNext(d time.Duration) (byte, bool, error) {
199 if r.peek != nil {
200 return r.peek(d)
201 }
202 b, err := r.src()
203 if err != nil {
204 return 0, false, err
205 }
206 return b, true, nil
207}
208
209// readCSI parses a CSI sequence after ESC [.
210func (r *Reader) readCSI() (Key, error) {
211 var params []byte
212 for {
213 c, err := r.src()
214 if err != nil {
215 return Key{}, err
216 }
217 if c >= 0x30 && c <= 0x3f {
218 params = append(params, c)
219 continue
220 }
221 // Final byte.
222 return r.dispatchCSI(string(params), c), nil
223 }
224}
225
226func (r *Reader) dispatchCSI(params string, final byte) Key {
227 // SGR mouse mode: CSI < button ; x ; y M/m. Wheel events use
228 // button codes 64 (up) and 65 (down). We ignore coordinates for
229 // now; the chat view only needs scroll direction.
230 if strings.HasPrefix(params, "<") && (final == 'M' || final == 'm') {
231 parts := strings.Split(strings.TrimPrefix(params, "<"), ";")
232 if len(parts) >= 1 {
233 switch parts[0] {
234 case "64":
235 return Key{Kind: KeyMouseWheelUp}
236 case "65":
237 return Key{Kind: KeyMouseWheelDown}
238 }
239 }
240 return Key{Kind: KeyUnknown}
241 }
242
243 // Modified arrow keys come in as CSI 1;<mod><final>. Modifier values
244 // we care about: 2=Shift, 3=Alt/Option, 5=Ctrl. We only extract Alt.
245 var alt bool
246 if params != "" {
247 if i := strings.IndexByte(params, ';'); i >= 0 {
248 mod := params[i+1:]
249 if mod == "3" || mod == "4" || mod == "7" || mod == "8" {
250 // 3=Alt, 4=Shift+Alt, 7=Ctrl+Alt, 8=Ctrl+Shift+Alt
251 alt = true
252 }
253 }
254 }
255 switch final {
256 case 'A':
257 return Key{Kind: KeyUp, Alt: alt}
258 case 'B':
259 return Key{Kind: KeyDown, Alt: alt}
260 case 'C':
261 return Key{Kind: KeyRight, Alt: alt}
262 case 'D':
263 return Key{Kind: KeyLeft, Alt: alt}
264 case 'H':
265 return Key{Kind: KeyHome}
266 case 'F':
267 return Key{Kind: KeyEnd}
268 case 'Z':
269 return Key{Kind: KeyShiftTab}
270 case '~':
271 switch params {
272 case "3":
273 return Key{Kind: KeyDelete}
274 case "5":
275 return Key{Kind: KeyPageUp}
276 case "6":
277 return Key{Kind: KeyPageDown}
278 case "200":
279 // Start of bracketed paste.
280 return r.readPaste()
281 }
282 }
283 return Key{Kind: KeyUnknown}
284}
285
286// readPaste reads until ESC [ 2 0 1 ~ and returns the pasted text.
287func (r *Reader) readPaste() Key {
288 var sb strings.Builder
289 const end = "\x1b[201~"
290 tail := make([]byte, 0, len(end))
291 for {
292 b, err := r.src()
293 if err != nil {
294 break
295 }
296 tail = append(tail, b)
297 if len(tail) > len(end) {
298 sb.WriteByte(tail[0])
299 tail = tail[1:]
300 }
301 if string(tail) == end {
302 break
303 }
304 }
305 return Key{Kind: KeyPaste, Paste: sb.String()}
306}