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