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