1// Package kittykey decodes kitty keyboard protocol key events and re-encodes
2// them for a receiver that requested a given set of enhancement flags.
3//
4// vev enables Disambiguate|AlternateKeys on the outer terminal. Keys that the
5// legacy encoding cannot express unambiguously then arrive as
6// CSI code[:shifted[:base]] [; mods] u, and F1-F4 as CSI [1;mods] P/Q/S.
7// Everything else stays legacy. Translate rewrites only those sequences, so
8// legacy input passes through byte-for-byte.
9//
10// The package is pure: no I/O, clocks, or ports.
11package kittykey
12
13import (
14 "bytes"
15 "strconv"
16 "unicode"
17 "unicode/utf8"
18)
19
20// Enhancement flags from the kitty keyboard protocol.
21const (
22 FlagDisambiguate = 1 << 0
23 FlagReportEvents = 1 << 1
24 FlagAlternateKeys = 1 << 2
25 FlagAllKeys = 1 << 3
26 FlagText = 1 << 4
27
28 // OuterFlags are the flags vev requests from the outer terminal.
29 OuterFlags = FlagDisambiguate | FlagAlternateKeys
30)
31
32// Modifier bits, as encoded in the mods parameter minus one.
33const (
34 ModShift = 1 << 0
35 ModAlt = 1 << 1
36 ModCtrl = 1 << 2
37 ModSuper = 1 << 3
38 ModHyper = 1 << 4
39 ModMeta = 1 << 5
40
41 modLocks = 1<<6 | 1<<7 // caps lock, num lock
42)
43
44// Key codes of the functional keys that have legacy single-byte forms.
45const (
46 KeyEscape = 27
47 KeyEnter = 13
48 KeyTab = 9
49 KeyBackspace = 127
50)
51
52// Event is one decoded CSI u key press.
53type Event struct {
54 // Code is the unicode key code: the unshifted key of the current layout.
55 Code rune
56 // Shifted is the shifted key, when reported (AlternateKeys with Shift).
57 Shifted rune
58 // Base is the key in the standard PC-101 layout, when reported.
59 Base rune
60 // Mods is the modifier bit set (ModShift...), lock bits removed.
61 Mods int
62 // Release reports a key-release event (only with FlagReportEvents).
63 Release bool
64}
65
66// OnlyMods reports whether exactly the given modifiers are active.
67func (e Event) OnlyMods(mods int) bool { return e.Mods == mods }
68
69// Digit reports the top-row digit 1-9 the key represents, preferring the
70// PC-101 base key so that layouts like AZERTY (whose unshifted digit keys
71// produce symbols) still map to digits.
72func (e Event) Digit() (int, bool) {
73 for _, r := range [...]rune{e.Base, e.Code} {
74 if r >= '1' && r <= '9' {
75 return int(r - '0'), true
76 }
77 }
78 return 0, false
79}
80
81const maxSeq = 64
82
83var (
84 pasteOpen = []byte("\x1b[200~")
85 pasteClose = []byte("\x1b[201~")
86)
87
88// Parse decodes one CSI u key sequence at the start of data. It returns the
89// event, the sequence length, and ok. partial reports that data is a strict
90// prefix of a sequence that could still become a CSI u key event.
91func Parse(data []byte) (ev Event, n int, ok, partial bool) {
92 if len(data) < 2 || data[0] != 0x1b || data[1] != '[' {
93 return Event{}, 0, false, len(data) == 1 && data[0] == 0x1b
94 }
95 i := 2
96 for ; i < len(data) && i < maxSeq; i++ {
97 b := data[i]
98 if (b >= '0' && b <= '9') || b == ';' || b == ':' {
99 continue
100 }
101 if b != 'u' || i == 2 {
102 return Event{}, 0, false, false
103 }
104 ev, ok := parseParams(data[2:i])
105 return ev, i + 1, ok, false
106 }
107 return Event{}, 0, false, i < maxSeq
108}
109
110func parseParams(params []byte) (Event, bool) {
111 fields := splitByte(params, ';')
112 if len(fields) > 3 {
113 return Event{}, false
114 }
115 keys := splitByte(fields[0], ':')
116 if len(keys) > 3 {
117 return Event{}, false
118 }
119 code, ok := atoi(keys[0])
120 if !ok || code == 0 {
121 return Event{}, false
122 }
123 ev := Event{Code: rune(code)}
124 if len(keys) > 1 && len(keys[1]) > 0 {
125 v, ok := atoi(keys[1])
126 if !ok {
127 return Event{}, false
128 }
129 ev.Shifted = rune(v)
130 }
131 if len(keys) > 2 && len(keys[2]) > 0 {
132 v, ok := atoi(keys[2])
133 if !ok {
134 return Event{}, false
135 }
136 ev.Base = rune(v)
137 }
138 if len(fields) > 1 && len(fields[1]) > 0 {
139 sub := splitByte(fields[1], ':')
140 if len(sub) > 2 {
141 return Event{}, false
142 }
143 mods, ok := atoi(sub[0])
144 if !ok || mods < 1 {
145 return Event{}, false
146 }
147 ev.Mods = (mods - 1) &^ modLocks
148 if len(sub) == 2 && len(sub[1]) > 0 {
149 event, ok := atoi(sub[1])
150 if !ok || event < 1 || event > 3 {
151 return Event{}, false
152 }
153 ev.Release = event == 3
154 }
155 }
156 // A third field carries associated text (FlagText); it is not needed to
157 // re-encode the key and is intentionally ignored.
158 return ev, true
159}
160
161// Translate rewrites every complete CSI u key sequence and kitty F1-F4 form in
162// data for a receiver that requested flags. With flags == 0 the result is the
163// legacy encoding; keys without a legacy form are dropped. With
164// FlagDisambiguate set, CSI u is kept and only subfields the receiver did not
165// ask for are removed. Other bytes, including bracketed paste bodies, are
166// copied unchanged. When nothing changes, data itself is returned.
167func Translate(data []byte, flags int) []byte {
168 var out []byte
169 last := 0
170 for i := 0; i+2 < len(data); i++ {
171 if data[i] != 0x1b || data[i+1] != '[' {
172 continue
173 }
174 if bytes.HasPrefix(data[i:], pasteOpen) {
175 end := bytes.Index(data[i:], pasteClose)
176 if end < 0 {
177 break
178 }
179 i += end + len(pasteClose) - 1
180 continue
181 }
182 var repl []byte
183 var n int
184 if ev, size, ok, _ := Parse(data[i:]); ok {
185 repl, n = encode(ev, flags), size
186 } else if fkey, size, ok := parseFunctionKey(data[i:]); ok {
187 repl, n = fkey.encode(flags), size
188 } else {
189 continue
190 }
191 if out == nil {
192 out = make([]byte, 0, len(data))
193 }
194 out = append(out, data[last:i]...)
195 out = append(out, repl...)
196 i += n - 1
197 last = i + 1
198 }
199 if out == nil {
200 return data
201 }
202 return append(out, data[last:]...)
203}
204
205func encode(ev Event, flags int) []byte {
206 if ev.Release && flags&FlagReportEvents == 0 {
207 return nil
208 }
209 if flags&FlagDisambiguate != 0 || flags&FlagAllKeys != 0 {
210 return encodeCSIu(ev, flags)
211 }
212 return encodeLegacy(ev)
213}
214
215func encodeCSIu(ev Event, flags int) []byte {
216 out := []byte("\x1b[")
217 out = strconv.AppendInt(out, int64(ev.Code), 10)
218 if flags&FlagAlternateKeys != 0 && (ev.Shifted != 0 || ev.Base != 0) {
219 out = append(out, ':')
220 if ev.Shifted != 0 {
221 out = strconv.AppendInt(out, int64(ev.Shifted), 10)
222 }
223 if ev.Base != 0 {
224 out = append(out, ':')
225 out = strconv.AppendInt(out, int64(ev.Base), 10)
226 }
227 }
228 release := ev.Release && flags&FlagReportEvents != 0
229 if ev.Mods != 0 || release {
230 out = append(out, ';')
231 out = strconv.AppendInt(out, int64(ev.Mods+1), 10)
232 if release {
233 out = append(out, ":3"...)
234 }
235 }
236 return append(out, 'u')
237}
238
239// encodeLegacy produces the xterm-style bytes for ev, or nil when the key has
240// no legacy representation.
241func encodeLegacy(ev Event) []byte {
242 if ev.Mods&^(ModShift|ModAlt|ModCtrl) != 0 {
243 // Super, Hyper, and Meta have no legacy encoding.
244 return nil
245 }
246 var body []byte
247 switch ev.Code {
248 case KeyEscape:
249 body = []byte{0x1b}
250 case KeyEnter:
251 body = []byte{'\r'}
252 case KeyTab:
253 if ev.Mods&ModShift != 0 {
254 body = []byte("\x1b[Z")
255 } else {
256 body = []byte{'\t'}
257 }
258 case KeyBackspace:
259 if ev.Mods&ModCtrl != 0 {
260 body = []byte{0x08}
261 } else {
262 body = []byte{0x7f}
263 }
264 default:
265 if seq, ok := keypadLegacy[ev.Code]; ok {
266 if mods := ev.Mods &^ ModShift; mods != 0 && len(seq) > 2 && seq[0] == 0x1b {
267 // Modified keypad navigation uses xterm's CSI 1;mods form.
268 return modifiedCSI(seq, ev.Mods)
269 }
270 body = []byte(seq)
271 break
272 }
273 if ev.Code < 0x20 || (ev.Code >= 0xe000 && ev.Code <= 0xf8ff) || !utf8.ValidRune(ev.Code) {
274 // Functional keys in the private use area have no legacy form.
275 return nil
276 }
277 body = legacyText(ev)
278 }
279 if ev.Mods&ModAlt != 0 {
280 return append([]byte{0x1b}, body...)
281 }
282 return body
283}
284
285func legacyText(ev Event) []byte {
286 r := ev.Code
287 if ev.Mods&ModShift != 0 {
288 switch {
289 case ev.Shifted != 0:
290 r = ev.Shifted
291 default:
292 r = unicode.ToUpper(r)
293 }
294 }
295 if ev.Mods&ModCtrl != 0 {
296 if c, ok := ctrlByte(r); ok {
297 return []byte{c}
298 }
299 // Non-Latin layouts: fall back to the PC-101 base key so Ctrl+letter
300 // still produces its control byte.
301 if c, ok := ctrlByte(ev.Base); ok {
302 return []byte{c}
303 }
304 }
305 return utf8.AppendRune(nil, r)
306}
307
308// ctrlByte maps a key to the C0 byte xterm sends for Ctrl+key.
309func ctrlByte(r rune) (byte, bool) {
310 switch {
311 case r >= 'a' && r <= 'z':
312 return byte(r - 'a' + 1), true
313 case r >= 'A' && r <= 'Z':
314 return byte(r - 'A' + 1), true
315 }
316 switch r {
317 case ' ', '@', '2':
318 return 0x00, true
319 case '[', '3':
320 return 0x1b, true
321 case '\\', '4':
322 return 0x1c, true
323 case ']', '5':
324 return 0x1d, true
325 case '^', '6', '~':
326 return 0x1e, true
327 case '_', '/', '7', '-':
328 return 0x1f, true
329 case '8', '?':
330 return 0x7f, true
331 }
332 return 0, false
333}
334
335// keypadLegacy maps kitty keypad private-use codes (reported as separate keys
336// under FlagDisambiguate) to the bytes a legacy terminal sends.
337var keypadLegacy = map[rune]string{
338 57399: "0", 57400: "1", 57401: "2", 57402: "3", 57403: "4",
339 57404: "5", 57405: "6", 57406: "7", 57407: "8", 57408: "9",
340 57409: ".", 57410: "/", 57411: "*", 57412: "-", 57413: "+",
341 57414: "\r", 57415: "=", 57416: ",",
342 57417: "\x1b[D", 57418: "\x1b[C", 57419: "\x1b[A", 57420: "\x1b[B",
343 57421: "\x1b[5~", 57422: "\x1b[6~", 57423: "\x1b[H", 57424: "\x1b[F",
344 57425: "\x1b[2~", 57426: "\x1b[3~", 57427: "\x1b[E",
345}
346
347// modifiedCSI turns a legacy cursor (CSI X) or tilde (CSI n ~) key into its
348// xterm modified form: CSI 1;m X or CSI n;m ~.
349func modifiedCSI(seq string, mods int) []byte {
350 m := strconv.Itoa(mods + 1)
351 final := seq[len(seq)-1]
352 if final == '~' {
353 return []byte(seq[:len(seq)-1] + ";" + m + "~")
354 }
355 return []byte("\x1b[1;" + m + string(final))
356}
357
358// functionKey is a kitty CSI [1;mods] P|Q|S form of F1, F2, or F4. Legacy
359// terminals send SS3 P|Q|S when unmodified.
360type functionKey struct {
361 final byte
362 mods []byte // the raw "1;mods" parameter bytes, empty when unmodified
363}
364
365func parseFunctionKey(data []byte) (functionKey, int, bool) {
366 for i := 2; i < len(data) && i < maxSeq; i++ {
367 b := data[i]
368 if (b >= '0' && b <= '9') || b == ';' || b == ':' {
369 continue
370 }
371 if b != 'P' && b != 'Q' && b != 'S' {
372 return functionKey{}, 0, false
373 }
374 params := data[2:i]
375 if len(params) != 0 && (len(params) < 3 || params[0] != '1' || params[1] != ';') {
376 return functionKey{}, 0, false
377 }
378 return functionKey{final: b, mods: params}, i + 1, true
379 }
380 return functionKey{}, 0, false
381}
382
383func (f functionKey) encode(flags int) []byte {
384 if len(f.mods) == 0 && flags&FlagDisambiguate == 0 {
385 return []byte{0x1b, 'O', f.final}
386 }
387 out := append([]byte("\x1b["), f.mods...)
388 return append(out, f.final)
389}
390
391func splitByte(b []byte, sep byte) [][]byte {
392 var out [][]byte
393 start := 0
394 for i := 0; i <= len(b); i++ {
395 if i == len(b) || b[i] == sep {
396 out = append(out, b[start:i])
397 start = i + 1
398 }
399 }
400 return out
401}
402
403func atoi(b []byte) (int, bool) {
404 if len(b) == 0 || len(b) > 7 {
405 return 0, false
406 }
407 n := 0
408 for _, c := range b {
409 if c < '0' || c > '9' {
410 return 0, false
411 }
412 n = n*10 + int(c-'0')
413 }
414 return n, true
415}