1 module tui.kittykeyboardprotocol;
2 import std.algorithm : map;
3 import std.array : array;
4 import std.ascii : ControlChar;
5 import std.format : format;
6 import std.range : empty;
7 import std.string : split;
8 import std.typecons : tuple;
9 
10 /// https://sw.kovidgoyal.net/kitty/keyboard-protocol
11 
12 /// enable all kitty keyboard protocol enhancements
13 immutable KITTY_KEYBOARD_ENABLE = format!("\x1b[>%du")(0b11111);
14 /// disable kitty keyboard protocol enhancements
15 immutable KITTY_KEYBOARD_DISABLE = "\x1b[<u";
16 
17 class Tokenizer
18 {
19     struct Result
20     {
21         State state;
22         immutable(KeyInput) keyInput;
23     }
24 
25     abstract class State
26     {
27         public byte[] all;
28         this(byte[] all)
29         {
30             this.all = all;
31         }
32 
33         abstract Result feed(byte b);
34     }
35 
36     class Idle : State
37     {
38         this()
39         {
40             super([]);
41         }
42 
43         override Result feed(byte b)
44         {
45             if (b == ControlChar.esc)
46             {
47                 return Result(new Escape(this.all ~ b), null);
48             }
49             return Result(this, null);
50         }
51     }
52 
53     class Escape : State
54     {
55         this(byte[] all)
56         {
57             super(all);
58         }
59 
60         override Result feed(byte b)
61         {
62             if (b == '[')
63             {
64                 return Result(new CsiReceived(this.all ~ b), null);
65             }
66             return Result(new Idle, null);
67         }
68     }
69 
70     /// Control Sequence Indicator Received
71     class CsiReceived : State
72     {
73         this(byte[] all)
74         {
75             super(all);
76         }
77 
78         override Result feed(byte b)
79         {
80             if ((b == 'u') || (b >= 'A' && b <= 'Z') || (b == '~'))
81             {
82                 all ~= b;
83                 return Result(new Idle(), parseKeyInput(all));
84             }
85             else if ((b >= '0' && b <= '9') || (b == ';') || (b == ':'))
86             {
87                 all ~= b;
88                 return Result(this, null);
89             }
90             else
91             {
92                 return Result(new Idle, null);
93             }
94         }
95     }
96 
97     State state;
98     this()
99     {
100         state = new Idle();
101     }
102 
103     auto feed(byte b)
104     {
105         auto result = state.feed(b);
106         state = result.state;
107         return result.keyInput;
108     }
109 }
110 
111 /// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys
112 enum Modifier : uint
113 {
114     // dfmt off
115     none     = 0b0000_0000,
116     shift    = 0b0000_0001,
117     alt      = 0b0000_0010,
118     ctrl     = 0b0000_0100,
119     super_   = 0b0000_1000,
120     hyper    = 0b0001_0000,
121     meta     = 0b0010_0000,
122     capsLock = 0b0100_0000,
123     numLock  = 0b1000_0000,
124     // dfmt on
125 }
126 
127 string toString(Modifier m)
128 {
129     string res = "";
130     auto add(string s, uint i)
131     {
132         if ((m & i) != 0)
133         {
134             if (res.empty)
135             {
136                 res = s;
137             }
138             else
139             {
140                 res ~= "|";
141                 res ~= s;
142             }
143         }
144     }
145 
146     add("shift", Modifier.shift);
147     add("alt", Modifier.alt);
148     add("ctrl", Modifier.ctrl);
149     add("super", Modifier.super_);
150     add("hyper", Modifier.hyper);
151     add("meta", Modifier.meta);
152     add("caps-lock", Modifier.capsLock);
153     add("num-lock", Modifier.numLock);
154     return res;
155 }
156 
157 Modifier parseModifier(int i)
158 {
159     if ((i == 0) || (i == 1))
160     {
161         return Modifier.none;
162     }
163 
164     auto bits = i - 1;
165     return cast(Modifier) bits;
166 }
167 
168 enum EventType
169 {
170     press = 1,
171     repeat = 2,
172     release = 3,
173 }
174 
175 EventType parseEventType(int i)
176 {
177     switch (i)
178     {
179     case 1:
180         return EventType.press;
181     case 2:
182         return EventType.repeat;
183     case 3:
184         return EventType.release;
185     default:
186         throw new Exception(format("Unknown event type: %s", i));
187     }
188 }
189 
190 class KeyInput
191 {
192     /// For Key.normal look at the c field
193     Key key;
194     /// Unicode character
195     dchar c;
196     Modifier modifiers; /// modifier keys held
197     EventType eventType; /// press / repeat / release
198     bool ctrlC;
199     bool empty;
200 
201     static auto fromKey(Key key, Modifier modifiers = Modifier.none,
202             EventType eventType = EventType.press)
203     {
204         KeyInput ki = new KeyInput;
205         ki.key = key;
206         ki.modifiers = modifiers;
207         ki.eventType = eventType;
208         return cast(immutable) ki;
209     }
210 
211     static auto fromPrintable(dchar ch, Modifier modifiers = Modifier.none,
212             EventType eventType = EventType.press)
213     {
214         KeyInput ki = new KeyInput;
215         ki.key = Key.normal;
216         ki.c = ch;
217         ki.modifiers = modifiers;
218         ki.eventType = eventType;
219         return cast(immutable) ki;
220     }
221 
222     static auto fromCtrlC()
223     {
224         KeyInput ki = new KeyInput;
225         ki.ctrlC = true;
226         return cast(immutable) ki;
227     }
228 
229     static auto fromInterrupt()
230     {
231         KeyInput ki = new KeyInput;
232         ki.empty = true;
233         return cast(immutable) ki;
234     }
235 
236     static auto fromEmpty()
237     {
238         KeyInput ki;
239         ki.empty = true;
240         return cast(immutable) ki;
241     }
242 
243     override string toString() const
244     {
245         if (ctrlC)
246             return "ctrlC";
247         if (empty)
248             return "empty";
249         if (key == Key.normal)
250             return format("c='%s' modifiers=%s eventType=%s", c, modifiers.toString(), eventType);
251         return format("key=%s modifiers=%s eventType=%s", key, modifiers.toString(), eventType);
252     }
253 }
254 
255 private int parseNumber(string s)
256 {
257     uint n = 0;
258     int pos = 0;
259     while (pos < s.length && s[pos] >= '0' && s[pos] <= '9')
260     {
261         n = n * 10 + (s[pos++] - '0');
262     }
263     return n;
264 }
265 
266 auto parseModifierAndEvent(string s)
267 {
268     if (s.empty)
269     {
270         // no modifier -> event = press
271         return tuple!("modifiers", "event")(Modifier.none, EventType.press);
272     }
273     auto parts = s.split(":");
274     if (parts.length == 1)
275     {
276         // only modifier -> event = press
277         return tuple!("modifiers", "event")(parseModifier(parseNumber(parts[0])), EventType.press);
278     }
279     else if (parts.length == 2)
280     {
281         // modifier and event
282         return tuple!("modifiers", "event")(parseModifier(parseNumber(parts[0])),
283                 parseEventType(parseNumber(parts[1])));
284     }
285     throw new Exception(format!("cannot parse modifier and event '%s'")(s));
286 }
287 
288 immutable(KeyInput) parseKeyInput(const(byte)[] input) @trusted
289 {
290     try
291     {
292         if (input[0 .. 2] != [27, 91])
293         {
294             throw new Exception("input does not start with 'esc ['");
295         }
296         auto parts = input[2 .. $].split(";");
297         if (parts.length == 0)
298         {
299             throw new Exception("Cannot split on ;");
300         }
301         if (parts.length == 1)
302         {
303             // no modifiers and events -> press no modifiers
304             auto key = kittyStringToKey(cast(string) parts[0]);
305             return KeyInput.fromKey(key, Modifier.none, EventType.press);
306         }
307         else if (parts.length == 2)
308         {
309             // modifiers and events present
310             auto kittyString = parts[0] ~ parts[1][$ - 1];
311             auto key = kittyStringToKey(cast(string) kittyString);
312             auto modifierAndEvent = parseModifierAndEvent(cast(string) parts[1][0 .. $ - 1]);
313             if (key == Key.normal)
314             {
315                 return KeyInput.fromPrintable(cast(dchar) parseNumber(cast(string) parts[0]),
316                         modifierAndEvent.modifiers, modifierAndEvent.event);
317             }
318             return KeyInput.fromKey(key, modifierAndEvent.modifiers, modifierAndEvent.event);
319         }
320         else if (parts.length == 3)
321         {
322             // the whole story including keycode, modifier/event and alternate character
323             auto kittyString = parts[0] ~ parts[2][$ - 1];
324             auto key = kittyStringToKey(cast(string) kittyString);
325             auto modifierAndEvent = parseModifierAndEvent(cast(string) parts[1]);
326             auto character = parseNumber(cast(string) parts[2][0 .. $ - 1]);
327             return KeyInput.fromPrintable(cast(dchar) character,
328                     modifierAndEvent.modifiers, modifierAndEvent.event);
329         }
330         else
331         {
332             throw new Exception("not 1, 2 or 3 parts");
333         }
334     }
335     catch (Exception e)
336     {
337         throw new Exception(format!("Cannot parse '%s'")(input), e);
338     }
339 }
340 
341 /// generated by kbd macro from the table at https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys
342 enum Key
343 {
344     normal,
345     escape,
346     enter,
347     tab,
348     backspace,
349     insert,
350     delete_,
351     left,
352     right,
353     up,
354     down,
355     page_up,
356     page_down,
357     home,
358     end,
359     caps_lock,
360     scroll_lock,
361     num_lock,
362     print_screen,
363     pause,
364     menu,
365     f1,
366     f2,
367     f3,
368     f4,
369     f5,
370     f6,
371     f7,
372     f8,
373     f9,
374     f10,
375     f11,
376     f12,
377     f13,
378     f14,
379     f15,
380     f16,
381     f17,
382     f18,
383     f19,
384     f20,
385     f21,
386     f22,
387     f23,
388     f24,
389     f25,
390     f26,
391     f27,
392     f28,
393     f29,
394     f30,
395     f31,
396     f32,
397     f33,
398     f34,
399     f35,
400     kp_0,
401     kp_1,
402     kp_2,
403     kp_3,
404     kp_4,
405     kp_5,
406     kp_6,
407     kp_7,
408     kp_8,
409     kp_9,
410     kp_decimal,
411     kp_divide,
412     kp_multiply,
413     kp_subtract,
414     kp_add,
415     kp_enter,
416     kp_equal,
417     kp_separator,
418     kp_left,
419     kp_right,
420     kp_up,
421     kp_down,
422     kp_page_up,
423     kp_page_down,
424     kp_home,
425     kp_end,
426     kp_insert,
427     kp_delete,
428     kp_begin,
429     media_play,
430     media_pause,
431     media_play_pause,
432     media_reverse,
433     media_stop,
434     media_fast_forward,
435     media_rewind,
436     media_track_next,
437     media_track_previous,
438     media_record,
439     lower_volume,
440     raise_volume,
441     mute_volume,
442     left_shift,
443     left_control,
444     left_alt,
445     left_super,
446     left_hyper,
447     left_meta,
448     right_shift,
449     right_control,
450     right_alt,
451     right_super,
452     right_hyper,
453     right_meta,
454     iso_level3_shift,
455     iso_level5_shift,
456 }
457 
458 /// generated by kbd macro from the table at https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys
459 private Key kittyStringToKey(string s)
460 {
461     switch (s)
462     {
463     case "27u":
464         return Key.escape;
465     case "13u":
466         return Key.enter;
467     case "9u":
468         return Key.tab;
469     case "127u":
470         return Key.backspace;
471     case "2~":
472         return Key.insert;
473     case "3~":
474         return Key.delete_;
475     case "1D":
476     case "D":
477         return Key.left;
478     case "1C":
479     case "C":
480         return Key.right;
481     case "1A":
482     case "A":
483         return Key.up;
484     case "1B":
485     case "B":
486         return Key.down;
487     case "5~":
488         return Key.page_up;
489     case "6~":
490         return Key.page_down;
491     case "1H":
492     case "H":
493     case "7~":
494         return Key.home;
495     case "1F":
496     case "F":
497     case "8~":
498         return Key.end;
499     case "57358u":
500         return Key.caps_lock;
501     case "57359u":
502         return Key.scroll_lock;
503     case "57360u":
504         return Key.num_lock;
505     case "57361u":
506         return Key.print_screen;
507     case "57362u":
508         return Key.pause;
509     case "57363u":
510         return Key.menu;
511     case "1P":
512     case "P":
513     case "11~":
514         return Key.f1;
515     case "1Q":
516     case "Q":
517     case "12~":
518         return Key.f2;
519     case "13~":
520         return Key.f3;
521     case "1S":
522     case "14~":
523         return Key.f4;
524     case "15~":
525         return Key.f5;
526     case "17~":
527         return Key.f6;
528     case "18~":
529         return Key.f7;
530     case "19~":
531         return Key.f8;
532     case "20~":
533         return Key.f9;
534     case "21~":
535         return Key.f10;
536     case "23~":
537         return Key.f11;
538     case "24~":
539         return Key.f12;
540     case "57376u":
541         return Key.f13;
542     case "57377u":
543         return Key.f14;
544     case "57378u":
545         return Key.f15;
546     case "57379u":
547         return Key.f16;
548     case "57380u":
549         return Key.f17;
550     case "57381u":
551         return Key.f18;
552     case "57382u":
553         return Key.f19;
554     case "57383u":
555         return Key.f20;
556     case "57384u":
557         return Key.f21;
558     case "57385u":
559         return Key.f22;
560     case "57386u":
561         return Key.f23;
562     case "57387u":
563         return Key.f24;
564     case "57388u":
565         return Key.f25;
566     case "57389u":
567         return Key.f26;
568     case "57390u":
569         return Key.f27;
570     case "57391u":
571         return Key.f28;
572     case "57392u":
573         return Key.f29;
574     case "57393u":
575         return Key.f30;
576     case "57394u":
577         return Key.f31;
578     case "57395u":
579         return Key.f32;
580     case "57396u":
581         return Key.f33;
582     case "57397u":
583         return Key.f34;
584     case "57398u":
585         return Key.f35;
586     case "57399u":
587         return Key.kp_0;
588     case "57400u":
589         return Key.kp_1;
590     case "57401u":
591         return Key.kp_2;
592     case "57402u":
593         return Key.kp_3;
594     case "57403u":
595         return Key.kp_4;
596     case "57404u":
597         return Key.kp_5;
598     case "57405u":
599         return Key.kp_6;
600     case "57406u":
601         return Key.kp_7;
602     case "57407u":
603         return Key.kp_8;
604     case "57408u":
605         return Key.kp_9;
606     case "57409u":
607         return Key.kp_decimal;
608     case "57410u":
609         return Key.kp_divide;
610     case "57411u":
611         return Key.kp_multiply;
612     case "57412u":
613         return Key.kp_subtract;
614     case "57413u":
615         return Key.kp_add;
616     case "57414u":
617         return Key.kp_enter;
618     case "57415u":
619         return Key.kp_equal;
620     case "57416u":
621         return Key.kp_separator;
622     case "57417u":
623         return Key.kp_left;
624     case "57418u":
625         return Key.kp_right;
626     case "57419u":
627         return Key.kp_up;
628     case "57420u":
629         return Key.kp_down;
630     case "57421u":
631         return Key.kp_page_up;
632     case "57422u":
633         return Key.kp_page_down;
634     case "57423u":
635         return Key.kp_home;
636     case "57424u":
637         return Key.kp_end;
638     case "57425u":
639         return Key.kp_insert;
640     case "57426u":
641         return Key.kp_delete;
642     case "1E":
643     case "57427~":
644         return Key.kp_begin;
645     case "57428u":
646         return Key.media_play;
647     case "57429u":
648         return Key.media_pause;
649     case "57430u":
650         return Key.media_play_pause;
651     case "57431u":
652         return Key.media_reverse;
653     case "57432u":
654         return Key.media_stop;
655     case "57433u":
656         return Key.media_fast_forward;
657     case "57434u":
658         return Key.media_rewind;
659     case "57435u":
660         return Key.media_track_next;
661     case "57436u":
662         return Key.media_track_previous;
663     case "57437u":
664         return Key.media_record;
665     case "57438u":
666         return Key.lower_volume;
667     case "57439u":
668         return Key.raise_volume;
669     case "57440u":
670         return Key.mute_volume;
671     case "57441u":
672         return Key.left_shift;
673     case "57442u":
674         return Key.left_control;
675     case "57443u":
676         return Key.left_alt;
677     case "57444u":
678         return Key.left_super;
679     case "57445u":
680         return Key.left_hyper;
681     case "57446u":
682         return Key.left_meta;
683     case "57447u":
684         return Key.right_shift;
685     case "57448u":
686         return Key.right_control;
687     case "57449u":
688         return Key.right_alt;
689     case "57450u":
690         return Key.right_super;
691     case "57451u":
692         return Key.right_hyper;
693     case "57452u":
694         return Key.right_meta;
695     case "57453u":
696         return Key.iso_level3_shift;
697     case "57454u":
698         return Key.iso_level5_shift;
699     default:
700         return Key.normal;
701     }
702 }