1 /**
2  * Parser module for dcell contains the code for parsing terminfo escapes
3  * as they arrive on /dev/tty.
4  *
5  * Copyright: Copyright 2025 Garrett D'Amore
6  * Authors: Garrett D'Amore
7  * License:
8  *   Distributed under the Boost Software License, Version 1.0.
9  *   (See accompanying file LICENSE or https://www.boost.org/LICENSE_1_0.txt)
10  *   SPDX-License-Identifier: BSL-1.0
11  */
12 module dcell.parser;
13 
14 import core.time;
15 import std.algorithm : max;
16 import std.ascii;
17 import std.base64;
18 import std.conv : to;
19 import std.process : environment;
20 import std.string;
21 import std.utf : decode, UTFException;
22 
23 import dcell.event;
24 import dcell.key;
25 import dcell.mouse;
26 
27 package:
28 
29 struct KeyCode
30 {
31     Key key;
32     Modifiers mod;
33 }
34 
35 struct CsiKey
36 {
37     char M; // Mode
38     int P; // Parameter (first)
39 }
40 
41 // Fixed set of keys that are returned as CSI sequences (apart from Csi-U and Csi-_)
42 // All terminals we support use some of these, and they do not overlap/collide.
43 immutable KeyCode[CsiKey] csiAllKeys = [
44     CsiKey('A'): KeyCode(Key.up),
45     CsiKey('B'): KeyCode(Key.down),
46     CsiKey('C'): KeyCode(Key.right),
47     CsiKey('D'): KeyCode(Key.left),
48     CsiKey('F'): KeyCode(Key.end),
49     CsiKey('H'): KeyCode(Key.home),
50     CsiKey('L'): KeyCode(Key.insert),
51     CsiKey('P'): KeyCode(Key.f1),
52     CsiKey('Q'): KeyCode(Key.f2),
53     CsiKey('S'): KeyCode(Key.f4),
54     CsiKey('Z'): KeyCode(Key.backtab),
55     CsiKey('a'): KeyCode(Key.up, Modifiers.shift),
56     CsiKey('b'): KeyCode(Key.down, Modifiers.shift),
57     CsiKey('c'): KeyCode(Key.right, Modifiers.shift),
58     CsiKey('d'): KeyCode(Key.left, Modifiers.shift),
59     CsiKey('q', 1): KeyCode(Key.f1), // all these 'q' are for aixterm
60     CsiKey('q', 2): KeyCode(Key.f2),
61     CsiKey('q', 3): KeyCode(Key.f3),
62     CsiKey('q', 4): KeyCode(Key.f4),
63     CsiKey('q', 5): KeyCode(Key.f5),
64     CsiKey('q', 6): KeyCode(Key.f6),
65     CsiKey('q', 7): KeyCode(Key.f7),
66     CsiKey('q', 8): KeyCode(Key.f8),
67     CsiKey('q', 9): KeyCode(Key.f9),
68     CsiKey('q', 10): KeyCode(Key.f10),
69     CsiKey('q', 11): KeyCode(Key.f11),
70     CsiKey('q', 12): KeyCode(Key.f12),
71     CsiKey('q', 13): KeyCode(Key.f13),
72     CsiKey('q', 14): KeyCode(Key.f14),
73     CsiKey('q', 15): KeyCode(Key.f15),
74     CsiKey('q', 16): KeyCode(Key.f16),
75     CsiKey('q', 17): KeyCode(Key.f17),
76     CsiKey('q', 18): KeyCode(Key.f18),
77     CsiKey('q', 19): KeyCode(Key.f19),
78     CsiKey('q', 20): KeyCode(Key.f20),
79     CsiKey('q', 21): KeyCode(Key.f21),
80     CsiKey('q', 22): KeyCode(Key.f22),
81     CsiKey('q', 23): KeyCode(Key.f23),
82     CsiKey('q', 24): KeyCode(Key.f24),
83     CsiKey('q', 25): KeyCode(Key.f25),
84     CsiKey('q', 26): KeyCode(Key.f26),
85     CsiKey('q', 27): KeyCode(Key.f27),
86     CsiKey('q', 28): KeyCode(Key.f28),
87     CsiKey('q', 29): KeyCode(Key.f29),
88     CsiKey('q', 30): KeyCode(Key.f30),
89     CsiKey('q', 31): KeyCode(Key.f31),
90     CsiKey('q', 32): KeyCode(Key.f32),
91     CsiKey('q', 33): KeyCode(Key.f33),
92     CsiKey('q', 34): KeyCode(Key.f34),
93     CsiKey('q', 35): KeyCode(Key.f35),
94     CsiKey('q', 36): KeyCode(Key.f36),
95     CsiKey('q', 144): KeyCode(Key.clear),
96     CsiKey('q', 146): KeyCode(Key.end),
97     CsiKey('q', 150): KeyCode(Key.pgUp),
98     CsiKey('q', 154): KeyCode(Key.pgDn),
99     CsiKey('z', 214): KeyCode(Key.home),
100     CsiKey('z', 216): KeyCode(Key.pgUp),
101     CsiKey('z', 220): KeyCode(Key.end),
102     CsiKey('z', 222): KeyCode(Key.pgDn),
103     CsiKey('z', 224): KeyCode(Key.f1),
104     CsiKey('z', 225): KeyCode(Key.f2),
105     CsiKey('z', 226): KeyCode(Key.f3),
106     CsiKey('z', 227): KeyCode(Key.f4),
107     CsiKey('z', 228): KeyCode(Key.f5),
108     CsiKey('z', 229): KeyCode(Key.f6),
109     CsiKey('z', 230): KeyCode(Key.f7),
110     CsiKey('z', 231): KeyCode(Key.f8),
111     CsiKey('z', 232): KeyCode(Key.f9),
112     CsiKey('z', 233): KeyCode(Key.f10),
113     CsiKey('z', 234): KeyCode(Key.f11),
114     CsiKey('z', 235): KeyCode(Key.f12),
115     CsiKey('z', 247): KeyCode(Key.insert),
116     CsiKey('^', 1): KeyCode(Key.home, Modifiers.ctrl),
117     CsiKey('^', 2): KeyCode(Key.insert, Modifiers.ctrl),
118     CsiKey('^', 3): KeyCode(Key.del, Modifiers.ctrl),
119     CsiKey('^', 4): KeyCode(Key.end, Modifiers.ctrl),
120     CsiKey('^', 5): KeyCode(Key.pgUp, Modifiers.ctrl),
121     CsiKey('^', 6): KeyCode(Key.pgDn, Modifiers.ctrl),
122     CsiKey('^', 7): KeyCode(Key.home, Modifiers.ctrl),
123     CsiKey('^', 8): KeyCode(Key.end, Modifiers.ctrl),
124     CsiKey('^', 11): KeyCode(Key.f23),
125     CsiKey('^', 12): KeyCode(Key.f24),
126     CsiKey('^', 13): KeyCode(Key.f25),
127     CsiKey('^', 14): KeyCode(Key.f26),
128     CsiKey('^', 15): KeyCode(Key.f27),
129     CsiKey('^', 17): KeyCode(Key.f28), // 16 is a gap
130     CsiKey('^', 18): KeyCode(Key.f29),
131     CsiKey('^', 19): KeyCode(Key.f30),
132     CsiKey('^', 20): KeyCode(Key.f31),
133     CsiKey('^', 21): KeyCode(Key.f32),
134     CsiKey('^', 23): KeyCode(Key.f33), // 22 is a gap
135     CsiKey('^', 24): KeyCode(Key.f34),
136     CsiKey('^', 25): KeyCode(Key.f35),
137     CsiKey('^', 26): KeyCode(Key.f36),
138     CsiKey('^', 28): KeyCode(Key.f37), // 27 is a gap
139     CsiKey('^', 29): KeyCode(Key.f38),
140     CsiKey('^', 31): KeyCode(Key.f39), // 30 is a gap
141     CsiKey('^', 32): KeyCode(Key.f40),
142     CsiKey('^', 33): KeyCode(Key.f41),
143     CsiKey('^', 34): KeyCode(Key.f42),
144     CsiKey('@', 23): KeyCode(Key.f43),
145     CsiKey('@', 24): KeyCode(Key.f44),
146     CsiKey('@', 1): KeyCode(Key.home, Modifiers.shift | Modifiers.ctrl),
147     CsiKey('@', 2): KeyCode(Key.insert, Modifiers.shift | Modifiers.ctrl),
148     CsiKey('@', 3): KeyCode(Key.del, Modifiers.shift | Modifiers.ctrl),
149     CsiKey('@', 4): KeyCode(Key.end, Modifiers.shift | Modifiers.ctrl),
150     CsiKey('@', 5): KeyCode(Key.pgUp, Modifiers.shift | Modifiers.ctrl),
151     CsiKey('@', 6): KeyCode(Key.pgDn, Modifiers.shift | Modifiers.ctrl),
152     CsiKey('@', 7): KeyCode(Key.home, Modifiers.shift | Modifiers.ctrl),
153     CsiKey('@', 8): KeyCode(Key.end, Modifiers.shift | Modifiers.ctrl),
154     CsiKey('$', 1): KeyCode(Key.home, Modifiers.shift),
155     CsiKey('$', 2): KeyCode(Key.insert, Modifiers.shift),
156     CsiKey('$', 3): KeyCode(Key.del, Modifiers.shift),
157     CsiKey('$', 4): KeyCode(Key.end, Modifiers.shift),
158     CsiKey('$', 5): KeyCode(Key.pgUp, Modifiers.shift),
159     CsiKey('$', 6): KeyCode(Key.pgDn, Modifiers.shift),
160     CsiKey('$', 7): KeyCode(Key.home, Modifiers.shift),
161     CsiKey('$', 8): KeyCode(Key.end, Modifiers.shift),
162     CsiKey('$', 23): KeyCode(Key.f21),
163     CsiKey('$', 24): KeyCode(Key.f22),
164     CsiKey('~', 1): KeyCode(Key.home),
165     CsiKey('~', 2): KeyCode(Key.insert),
166     CsiKey('~', 3): KeyCode(Key.del),
167     CsiKey('~', 4): KeyCode(Key.end),
168     CsiKey('~', 5): KeyCode(Key.pgUp),
169     CsiKey('~', 6): KeyCode(Key.pgDn),
170     CsiKey('~', 7): KeyCode(Key.home),
171     CsiKey('~', 8): KeyCode(Key.end),
172     CsiKey('~', 11): KeyCode(Key.f1),
173     CsiKey('~', 12): KeyCode(Key.f2),
174     CsiKey('~', 13): KeyCode(Key.f3),
175     CsiKey('~', 14): KeyCode(Key.f4),
176     CsiKey('~', 15): KeyCode(Key.f5),
177     CsiKey('~', 16): KeyCode(Key.f6),
178     CsiKey('~', 18): KeyCode(Key.f7),
179     CsiKey('~', 19): KeyCode(Key.f8),
180     CsiKey('~', 20): KeyCode(Key.f9),
181     CsiKey('~', 21): KeyCode(Key.f10),
182     CsiKey('~', 23): KeyCode(Key.f11),
183     CsiKey('~', 24): KeyCode(Key.f12),
184     CsiKey('~', 25): KeyCode(Key.f13),
185     CsiKey('~', 26): KeyCode(Key.f14),
186     CsiKey('~', 28): KeyCode(Key.f15),
187     CsiKey('~', 29): KeyCode(Key.f16),
188     CsiKey('~', 31): KeyCode(Key.f17),
189     CsiKey('~', 32): KeyCode(Key.f18),
190     CsiKey('~', 33): KeyCode(Key.f19),
191     CsiKey('~', 34): KeyCode(Key.f20),
192     // CsiKey('~', 200): KeyCode(keyPasteStart),
193     // CsiKey('~', 201): KeyCode(keyPasteEnd),
194 ];
195 
196 // keys by their SS3 - used in application mode usually (legacy VT-style)
197 immutable KeyCode[char] ss3Keys = [
198     'A': KeyCode(Key.up),
199     'B': KeyCode(Key.down),
200     'C': KeyCode(Key.right),
201     'D': KeyCode(Key.left),
202     'F': KeyCode(Key.end),
203     'H': KeyCode(Key.home),
204     'P': KeyCode(Key.f1),
205     'Q': KeyCode(Key.f2),
206     'R': KeyCode(Key.f3),
207     'S': KeyCode(Key.f4),
208     't': KeyCode(Key.f5),
209     'u': KeyCode(Key.f6),
210     'v': KeyCode(Key.f7),
211     'l': KeyCode(Key.f8),
212     'w': KeyCode(Key.f9),
213     'x': KeyCode(Key.f10),
214 ];
215 
216 // linux terminal uses these non ECMA keys prefixed by CSI-[
217 immutable KeyCode[char] linuxFKeys = [
218     'A': KeyCode(Key.f1),
219     'B': KeyCode(Key.f2),
220     'C': KeyCode(Key.f3),
221     'D': KeyCode(Key.f4),
222     'E': KeyCode(Key.f5),
223 ];
224 
225 immutable KeyCode[int] csiUKeys = [
226     27: KeyCode(Key.esc),
227     9: KeyCode(Key.tab),
228     13: KeyCode(Key.enter),
229     127: KeyCode(Key.backspace),
230     // 57_358: KeyCode(KeyCapsLock),
231     // 57_359: KeyCode(KeyScrollLock),
232     // 57_360: KeyCode(KeyNumLock),
233     57_361: KeyCode(Key.print),
234     57_362: KeyCode(Key.pause),
235     // 57_363: KeyCode(Key.menu),
236     57_376: KeyCode(Key.f13),
237     57_377: KeyCode(Key.f14),
238     57_378: KeyCode(Key.f15),
239     57_379: KeyCode(Key.f16),
240     57_380: KeyCode(Key.f17),
241     57_381: KeyCode(Key.f18),
242     57_382: KeyCode(Key.f19),
243     57_383: KeyCode(Key.f20),
244     57_384: KeyCode(Key.f21),
245     57_385: KeyCode(Key.f22),
246     57_386: KeyCode(Key.f23),
247     57_387: KeyCode(Key.f24),
248     57_388: KeyCode(Key.f25),
249     57_389: KeyCode(Key.f26),
250     57_390: KeyCode(Key.f27),
251     57_391: KeyCode(Key.f28),
252     57_392: KeyCode(Key.f29),
253     57_393: KeyCode(Key.f30),
254     57_394: KeyCode(Key.f31),
255     57_395: KeyCode(Key.f32),
256     57_396: KeyCode(Key.f33),
257     57_397: KeyCode(Key.f34),
258     57_398: KeyCode(Key.f35),
259     // TODO: KP keys
260     // TODO: Media keys
261 ];
262 
263 // windows virtual key codes per microsoft
264 immutable KeyCode[int] winKeys = [
265     0x03: KeyCode(Key.cancel), // vkCancel
266     0x08: KeyCode(Key.backspace), // vkBackspace
267     0x09: KeyCode(Key.tab), // vkTab
268     0x0d: KeyCode(Key.enter), // vkReturn
269     0x12: KeyCode(Key.clear), // vClear
270     0x13: KeyCode(Key.pause), // vkPause
271     0x1b: KeyCode(Key.esc), // vkEscape
272     0x21: KeyCode(Key.pgUp), // vkPrior
273     0x22: KeyCode(Key.pgDn), // vkNext
274     0x23: KeyCode(Key.end), // vkEnd
275     0x24: KeyCode(Key.home), // vkHome
276     0x25: KeyCode(Key.left), // vkLeft
277     0x26: KeyCode(Key.up), // vkUp
278     0x27: KeyCode(Key.right), // vkRight
279     0x28: KeyCode(Key.down), // vkDown
280     0x2a: KeyCode(Key.print), // vkPrint
281     0x2c: KeyCode(Key.print), // vkPrtScr
282     0x2d: KeyCode(Key.insert), // vkInsert
283     0x2e: KeyCode(Key.del), // vkDelete
284     0x2f: KeyCode(Key.help), // vkHelp
285     0x70: KeyCode(Key.f1), // vkF1
286     0x71: KeyCode(Key.f2), // vkF2
287     0x72: KeyCode(Key.f3), // vkF3
288     0x73: KeyCode(Key.f4), // vkF4
289     0x74: KeyCode(Key.f5), // vkF5
290     0x75: KeyCode(Key.f6), // vkF6
291     0x76: KeyCode(Key.f7), // vkF7
292     0x77: KeyCode(Key.f8), // vkF8
293     0x78: KeyCode(Key.f9), // vkF9
294     0x79: KeyCode(Key.f10), // vkF10
295     0x7a: KeyCode(Key.f11), // vkF11
296     0x7b: KeyCode(Key.f12), // vkF12
297     0x7c: KeyCode(Key.f13), // vkF13
298     0x7d: KeyCode(Key.f14), // vkF14
299     0x7e: KeyCode(Key.f15), // vkF15
300     0x7f: KeyCode(Key.f16), // vkF16
301     0x80: KeyCode(Key.f17), // vkF17
302     0x81: KeyCode(Key.f18), // vkF18
303     0x82: KeyCode(Key.f19), // vkF19
304     0x83: KeyCode(Key.f20), // vkF20
305     0x84: KeyCode(Key.f21), // vkF21
306     0x85: KeyCode(Key.f22), // vkF22
307     0x86: KeyCode(Key.f23), // vkF23
308     0x87: KeyCode(Key.f24), // vkF24
309 ];
310 
311 class Parser
312 {
313 
314     Event[] events() pure @safe @nogc
315     {
316         auto res = evs;
317         evs = null;
318         return cast(Event[]) res;
319     }
320 
321     // Parse the supplied content, returns true if data is fully parsed.
322     bool parse(string b) @safe
323     {
324         buf ~= b;
325         scan();
326         return parseState == ParseState.ini;
327     }
328 
329     bool empty() const pure @safe
330     {
331         return buf.length == 0;
332     }
333 
334 private:
335     enum ParseState
336     {
337         ini, // initial state
338         esc, // escaped
339         utf, // inside a UTF-8
340         csi, // control sequence introducer
341         osc, // operating system command
342         dcs, // device control string
343         sos, // start of string (unused)
344         pm, // privacy message (unused)
345         apc, // application program command
346         str, // string terminator
347         ss2, // single shift 2
348         ss3, // single shift 3
349         lnx, // linux F-key (not ECMA-48 compliant - bogus CSI)
350     }
351 
352     ParseState parseState;
353     ParseState strState;
354     Parser nested; // nested parser, required for Windows key processing with 3rd party terminals
355     string csiParams;
356     string csiInterm;
357     string scratch;
358 
359     bool escaped;
360     ubyte[] buf;
361     ubyte[] accum;
362     Event[] evs;
363     int utfLen; // how many UTF bytes are expected
364     ubyte escChar; // character immediately following escape (zero if none)
365     bool partial; // record partially parsed sequences
366     MonoTime keyStart; // when the timer started
367     Duration seqTime = msecs(50); // time to fully decode a partial sequence
368     bool buttonDown; // true if buttons were down
369     bool pasting;
370     dstring pasteBuf;
371 
372     void postKey(Key k, dchar dch, Modifiers mod) nothrow @safe
373     {
374         if (pasting)
375         {
376             if (dch != 0)
377             {
378                 pasteBuf ~= dch;
379             }
380         }
381         else
382         {
383             evs ~= newKeyEvent(k, dch, mod);
384         }
385     }
386 
387     void scan() @trusted
388     {
389         while (!buf.empty)
390         {
391             ubyte ch = buf[0];
392             buf = buf[1 .. $];
393             escChar = 0;
394 
395             final switch (parseState)
396             {
397             case ParseState.utf:
398                 accum ~= ch;
399                 if (accum.length >= utfLen)
400                 {
401                     parseState = ParseState.ini;
402                     size_t index = 0;
403                     dchar dch = decode(cast(string) accum, index);
404                     accum = null;
405                     postKey(Key.graph, dch, Modifiers.none);
406                 }
407                 break;
408             case ParseState.ini:
409                 if (ch >= 0x80)
410                 {
411                     accum = null;
412                     parseState = ParseState.utf;
413                     accum ~= ch;
414                     if ((ch & 0xE0) == 0xC0)
415                     {
416                         utfLen = 2;
417                     }
418                     else if ((ch & 0xF0) == 0xE0)
419                     {
420                         utfLen = 3;
421                     }
422                     else if ((ch & 0xF0) == 0xF0)
423                     {
424                         utfLen = 4;
425                     }
426                     else
427                     {
428                         // garbled - got a non-leading byte (e.g. 0x80 through 0xBF)
429                         parseState = ParseState.ini;
430                         accum = null;
431                     }
432                     continue;
433                 }
434                 switch (ch)
435                 {
436                 case '\x1b':
437                     parseState = ParseState.esc;
438                     keyStart = MonoTime.currTime();
439                     continue;
440                 case '\t':
441                     postKey(Key.tab, ch, Modifiers.none);
442                     break;
443                 case '\b', '\x7F':
444                     postKey(Key.backspace, ch, Modifiers.none);
445                     break;
446                 case '\n', '\r':
447                     // will be converted by postKey
448                     postKey(Key.enter, ch, Modifiers.none);
449                     break;
450                 default:
451                     // simple runes
452                     if (ch >= ' ')
453                     {
454                         postKey(Key.graph, ch, Modifiers.none);
455                     }
456                     // Control keys below here - legacy handling
457                     else if (ch == 0)
458                     {
459                         postKey(Key.graph, ' ', Modifiers.ctrl);
460                     }
461                     else if (ch < '\x1b')
462                     {
463                         postKey(Key.graph, ch + 0x60, Modifiers.ctrl);
464                     }
465                     else
466                     {
467                         // control keys
468                         postKey(Key.graph, ch + 0x40, Modifiers.ctrl);
469                     }
470                     break;
471                 }
472                 break;
473             case ParseState.esc:
474                 switch (ch)
475                 {
476                 case '[':
477                     parseState = ParseState.csi;
478                     csiInterm = null;
479                     csiParams = null;
480                     escChar = ch; // save the escChar, it might be just esc as alt
481                     break;
482                 case ']':
483                     parseState = ParseState.osc;
484                     scratch = null;
485                     escChar = ch; // save the escChar, it might be just esc as alt
486                     break;
487                 case 'N':
488                     parseState = ParseState.ss2; // no known uses
489                     scratch = null;
490                     escChar = ch; // save the escChar, it might be just esc as alt
491                     break;
492                 case 'O':
493                     parseState = ParseState.ss3;
494                     scratch = null;
495                     escChar = ch; // save the escChar, it might be just esc as alt
496                     break;
497                 case 'X':
498                     parseState = ParseState.sos;
499                     scratch = null;
500                     escChar = ch; // save the escChar, it might be just esc as alt
501                     break;
502                 case '^':
503                     parseState = ParseState.pm;
504                     scratch = null;
505                     escChar = ch; // save the escChar, it might be just esc as alt
506                     break;
507                 case '_':
508                     parseState = ParseState.apc;
509                     scratch = null;
510                     escChar = ch; // save the escChar, it might be just esc as alt
511                     break;
512                 case '\\': // string terminator reached, (orphaned?)
513                     parseState = ParseState.ini;
514                     break;
515                 case '\t': // Linux console only, does not conform to ECMA
516                     parseState = ParseState.ini;
517                     postKey(Key.backtab, 0, Modifiers.none);
518                     break;
519                 case '\x1b':
520                     // leading ESC to capture alt
521                     escaped = true;
522                     break;
523                 default:
524                     // treat as alt-key ... legacy emulators only (no CSI-u or other)
525                     parseState = parseState.ini;
526                     escaped = false;
527                     if (ch >= ' ')
528                     {
529                         postKey(Key.graph, ch, Modifiers.meta);
530                     }
531                     else if (ch < '\x1b')
532                     {
533                         postKey(Key.graph, ch + 0x60, Modifiers.meta | Modifiers.ctrl);
534                     }
535                     else
536                     {
537                         postKey(Key.graph, ch + 0x40, Modifiers.meta | Modifiers.ctrl);
538                     }
539                 }
540                 break;
541             case ParseState.ss2:
542                 // no known uses
543                 parseState = ParseState.ini;
544                 break;
545             case ParseState.ss3:
546                 parseState = ParseState.ini;
547                 if (ch in ss3Keys)
548                 {
549                     auto k = ss3Keys[ch];
550                     postKey(k.key, 0, k.mod);
551                 }
552                 break;
553 
554             case ParseState.apc, ParseState.pm, ParseState.sos, ParseState.dcs: // these we just eat
555                 switch (ch)
556                 {
557                 case '\x1b':
558                     strState = parseState;
559                     parseState = ParseState.str;
560                     break;
561                 case '\x07': // bell - some send this instead of ST
562                     parseState = ParseState.ini;
563                     break;
564                 default:
565                     break;
566                 }
567                 break;
568             case ParseState.osc: // not sure if used
569                 switch (ch)
570                 {
571                 case '\x1b':
572                     strState = parseState;
573                     parseState = ParseState.str;
574                     break;
575                 case '\x07':
576                     handleOsc();
577                     break;
578                 default:
579                     scratch ~= (ch & 0x7F);
580                     break;
581                 }
582                 break;
583             case ParseState.str:
584                 if (ch == '\\' || ch == '\x07')
585                 {
586                     parseState = ParseState.ini;
587                     if (strState == ParseState.osc)
588                     {
589                         handleOsc();
590                     }
591                     else
592                     {
593                         parseState = ParseState.ini;
594                     }
595                 }
596                 else
597                 {
598                     scratch ~= '\x1b';
599                     scratch ~= ch;
600                     parseState = strState;
601                 }
602                 break;
603             case ParseState.lnx:
604                 if (ch in linuxFKeys)
605                 {
606                     auto k = linuxFKeys[ch];
607                     postKey(k.key, 0, Modifiers.none);
608                 }
609                 parseState = ParseState.ini;
610                 break;
611 
612             case ParseState.csi:
613                 // usual case for incoming keys
614                 // NB: rxvt uses terminating '$' which is not a legal CSI terminator,
615                 // for certain shifted key sequences.  We special case this, and it's ok
616                 // because no other terminal seems to use this for CSI intermediates from
617                 // the terminal to the host (queries in the other direction can use it.)
618                 if (ch >= 0x30 && ch <= 0x3F)
619                 { // parameter bytes
620                     csiParams ~= ch;
621                 }
622                 else if (ch == '$' && !csiParams.empty)
623                 {
624                     // rxvt $ terminator (not technically legal)
625                     handleCsi(ch, csiParams, csiInterm);
626                 }
627                 else if ((ch >= 0x20) && (ch <= 0x2F))
628                 {
629                     // intermediate bytes, rarely used
630                     csiInterm ~= ch;
631                 }
632                 else if (ch >= 0x40 && ch <= 0x7F)
633                 {
634                     // final byte
635                     handleCsi(ch, csiParams, csiInterm);
636                 }
637                 else
638                 {
639                     // bad parse, just swallow it all
640                     parseState = ParseState.ini;
641                 }
642                 break;
643             }
644         }
645 
646         auto now = MonoTime.currTime();
647         if ((now - keyStart) > seqTime)
648         {
649             if (parseState == ParseState.esc)
650             {
651                 postKey(Key.esc, '\x1b', Modifiers.none);
652                 parseState = ParseState.ini;
653             }
654             else if (escChar != 0)
655             {
656                 postKey(Key.graph, escChar, Modifiers.alt);
657                 escChar = 0;
658                 parseState = ParseState.ini;
659             }
660         }
661     }
662 
663     void handleOsc()
664     {
665         if (scratch.startsWith("52;c;"))
666         {
667             scratch = scratch["52;c;".length .. $];
668             try
669             {
670                 auto bin = Base64.decode(scratch);
671                 evs ~= newPasteEvent(bin);
672             }
673             catch (Base64Exception) // just discard the data if it was malformed
674             {
675             }
676         }
677 
678         // string is located in scratch.
679         parseState = ParseState.ini;
680     }
681 
682     void handleCsi(ubyte mode, string params, string interm) @safe
683     {
684         parseState = ParseState.ini;
685 
686         if (!interm.empty)
687         {
688             // we don't know what to do with these for now
689             return;
690         }
691 
692         auto hasLT = false;
693         int plen, p0, p1, p2, p3, p4, p5;
694 
695         // extract numeric parameters
696         if (!params.empty && params[0] == '<')
697         {
698             hasLT = true;
699             params = params[1 .. $];
700         }
701         if ((!params.empty) && params[0] >= '0' && params[0] <= '9')
702         {
703             int[6] pints;
704             string[] parts = split(params, ";");
705             plen = cast(int) parts.length;
706             foreach (i, ps; parts)
707             {
708                 if (i < 6 && !ps.empty)
709                 {
710                     try
711                     {
712                         pints[i] = ps.to!int;
713                     }
714                     catch (Exception)
715                     {
716                     }
717                 }
718             }
719 
720             // None of the use cases use care about have more than three parameters.
721             p0 = pints[0];
722             p1 = pints[1];
723             p2 = pints[2];
724             p3 = pints[3];
725             p4 = pints[4];
726             p5 = pints[5];
727         }
728 
729         // leading less than is only used for mouse reports.
730         if (hasLT)
731         {
732             if (mode == 'm' || mode == 'M')
733             {
734                 handleMouse(mode, p0, p1, p2);
735             }
736             return;
737         }
738 
739         switch (mode)
740         {
741         case 'I': // focus in
742             evs ~= newFocusEvent(true);
743             return;
744         case 'O': // focus out
745             evs ~= newFocusEvent(false);
746             return;
747         case '[': // linux console F-key - CSI-[ modifies next key
748             parseState = ParseState.lnx;
749             return;
750         case 'u': // CSI-u kitty keyboard protocol
751             if (plen > 0)
752             {
753                 Modifiers mod = Modifiers.none;
754                 Key key = Key.graph;
755                 dchar chr = 0;
756                 if (p0 in csiUKeys)
757                 {
758                     auto k = csiUKeys[p0];
759                     key = k.key;
760                 }
761                 else
762                 {
763                     chr = cast(dchar) p0;
764                 }
765 
766                 evs ~= newKeyEvent(key, chr, plen > 1 ? calcModifier(p1) : Modifiers.none);
767             }
768             return;
769 
770         case '_':
771             if (plen > 0)
772             {
773                 handleWinKey(p0, p1, p2, p3, p4, p5);
774             }
775             return;
776 
777         case 't':
778             // if (P.length == 3 && P[0] == 8)
779             // {
780             //     // window size report
781             //     auto h = p1;
782             //     auto w = p2;
783             //     if (h != rows || w != cols)
784             //     {
785             //         setSize(w, h);
786             //     }
787             // }
788             return;
789 
790         case '~':
791 
792             // look for modified keys (note that unmodified keys are handled below)
793             auto ck = CsiKey(mode, p0);
794             auto mod = plen > 1 ? calcModifier(p1) : Modifiers.none;
795 
796             if (ck in csiAllKeys)
797             {
798                 auto kc = csiAllKeys[ck];
799                 evs ~= newKeyEvent(kc.key, 0, mod);
800                 return;
801             }
802 
803             // this might be XTerm modifyOtherKeys protocol
804             // CSI 27; modifiers; chr; ~
805             if (p0 == 27 && p2 > 0 && p2 <= 0xff)
806             {
807                 if (p2 < ' ' || p2 == 0x7F)
808                 {
809                     evs ~= newKeyEvent(cast(Key) p2, 0, mod);
810                 }
811                 else
812                 {
813                     evs ~= newKeyEvent(Key.graph, p2, mod);
814                 }
815                 return;
816             }
817 
818             if (p0 == 200)
819             {
820                 pasting = true;
821                 pasteBuf = null;
822             }
823             else if (p0 == 201)
824             {
825                 if (pasting)
826                 {
827                     evs ~= newPasteEvent(pasteBuf.to!string);
828                     pasting = false;
829                     pasteBuf = null;
830                 }
831             }
832 
833             break;
834 
835         case 'P':
836             // aixterm uses this for KeyDelete, but it is F1 for others
837             if (environment.get("TERM") == "aixterm")
838             {
839                 evs ~= newKeyEvent(Key.del, 0, Modifiers.none);
840                 return;
841             }
842             // other cases we use the lookup (P is an SS3 key)
843             goto default;
844 
845         case 'c':
846             if (!params.empty && params[0] == '?')
847             {
848                 // device attributes response - we use this for wake ups, but don't care about the content.
849                 return;
850             }
851             goto default;
852 
853         default:
854 
855             if ((mode in ss3Keys) && p0 == 1 && plen > 1)
856             {
857                 auto kc = ss3Keys[mode];
858                 evs ~= newKeyEvent(kc.key, 0, calcModifier(p1));
859             }
860             else
861             {
862                 auto ck = CsiKey(mode, p0);
863                 if (ck in csiAllKeys)
864                 {
865                     auto kc = csiAllKeys[ck];
866                     evs ~= newKeyEvent(kc.key, 0, kc.mod);
867                 }
868             }
869             return;
870         }
871     }
872 
873     void handleMouse(ubyte mode, int p0, int p1, int p2) nothrow @safe
874     {
875         // XTerm mouse events only report at most one button at a time,
876         // which may include a wheel button.  Wheel motion events are
877         // reported as single impulses, while other button events are reported
878         // as separate press & release events.
879         //
880         auto btn = p0;
881         auto x = p1 - 1;
882         auto y = p2 - 1;
883         bool motion = (btn & 0x20) != 0;
884         bool scroll = (btn & 0x42) == 0x40;
885         btn &= ~0x20;
886         if (mode == 'm')
887         {
888             // mouse release, clear all buttons
889             btn |= 0x03;
890             btn &= ~0x40;
891             buttonDown = false;
892         }
893         else if (motion)
894         {
895             // Some broken terminals appear to send
896             // mouse button one motion events, instead of
897             // encoding 35 (no buttons) into these events.
898             // We resolve these by looking for a non-motion
899             // event first.
900             if (!buttonDown)
901             {
902                 btn |= 0x03;
903                 btn &= ~0x40;
904             }
905         }
906         else if (!scroll)
907         {
908             buttonDown = true;
909         }
910 
911         auto button = Buttons.none;
912         auto mod = Modifiers.none;
913 
914         // Mouse wheel has bit 6 set, no release events.  It should be noted
915         // that wheel events are sometimes misdelivered as mouse button events
916         // during a click-drag, so we debounce these, considering them to be
917         // button press events unless we see an intervening release event.
918         final switch (btn & 0x43)
919         {
920         case 0:
921             button = Buttons.button1;
922             break;
923         case 1:
924             button = Buttons.button3; // Note we prefer to treat right as button 2
925             break;
926         case 2:
927             button = Buttons.button2; // And the middle button as button 3
928             break;
929         case 3:
930             button = Buttons.none;
931             break;
932         case 0x40:
933             button = Buttons.wheelUp;
934             break;
935         case 0x41:
936             button = Buttons.wheelDown;
937             break;
938         case 0x42:
939             button = Buttons.wheelLeft;
940             break;
941         case 0x43:
942             button = Buttons.wheelRight;
943             break;
944         }
945 
946         if ((btn & 0x4) != 0)
947         {
948             mod |= Modifiers.shift;
949         }
950         if ((btn & 0x8) != 0)
951         {
952             mod |= Modifiers.alt;
953         }
954         if ((btn & 0x10) != 0)
955         {
956             mod |= Modifiers.ctrl;
957         }
958 
959         evs ~= newMouseEvent(x, y, button, mod);
960     }
961 
962     void handleWinKey(int p0, int p1, int p2, int p3, int p4, int p5) @safe
963     {
964         // win32-input-mode
965         //  ^[ [ Vk ; Sc ; Uc ; Kd ; Cs ; Rc _
966         // Vk: the value of wVirtualKeyCode - any number. If omitted, defaults to '0'.
967         // Sc: the value of wVirtualScanCode - any number. If omitted, defaults to '0'.
968         // Uc: the decimal value of UnicodeChar - for example, NUL is "0", LF is
969         //     "10", the character 'A' is "65". If omitted, defaults to '0'.
970         // Kd: the value of bKeyDown - either a '0' or '1'. If omitted, defaults to '0'.
971         // Cs: the value of dwControlKeyState - any number. If omitted, defaults to '0'.
972         // Rc: the value of wRepeatCount - any number. If omitted, defaults to '1'.
973         //
974         // Note that some 3rd party terminal emulators (not Terminal) suffer from a bug
975         // where other events, such as mouse events, are doubly encoded, using Vk 0
976         // for each character.  (So a CSI-M sequence is encoded as a series of CSI-_
977         // sequences.)  We consider this a bug in those terminal emulators -- Windows 11
978         // Terminal does not suffer this brain damage. (We've observed this with both Alacritty
979         // and WezTerm.)
980         //
981         if (p3 == 0)
982         {
983             // key up event ignore ignore
984             return;
985         }
986 
987         if (p0 == 0 && p1 == 0 && p2 > 0 && p2 < 0x80)
988         {
989             if (nested is null)
990             {
991                 nested = new Parser();
992             }
993             // only ASCII in win32-input-mode
994             nested.buf ~= cast(ubyte) p2;
995             nested.scan();
996             foreach (ev; nested.evs)
997             {
998                 evs ~= ev;
999             }
1000             nested.evs = null;
1001             return;
1002         }
1003 
1004         auto key = Key.graph;
1005         auto chr = p2;
1006         auto mod = Modifiers.none;
1007         auto rpt = max(1, p5);
1008 
1009         if (p0 in winKeys)
1010         {
1011             auto kc = winKeys[p0];
1012             key = kc.key;
1013             chr = 0;
1014         }
1015         else if (chr == 0 && p0 >= 0x30 && p0 <= 0x39)
1016         {
1017             chr = p0;
1018         }
1019         else if (chr < ' ' && p0 >= 0x41 && p0 <= 0x5a)
1020         {
1021             chr = p0;
1022         }
1023         else if (key == 0x11 || key == 0x13 || key == 0x14)
1024         {
1025             // lone modifiers
1026             return;
1027         }
1028 
1029         // Modifiers
1030         if ((p4 & 0x010) != 0)
1031         {
1032             mod |= Modifiers.shift;
1033         }
1034         if ((p4 & 0x000c) != 0)
1035         {
1036             mod |= Modifiers.ctrl;
1037         }
1038         if ((p4 & 0x0003) != 0)
1039         {
1040             mod |= Modifiers.alt;
1041         }
1042         if (key == Key.graph && chr > ' ' && mod == Modifiers.shift)
1043         {
1044             // filter out lone shift for printable chars
1045             mod = Modifiers.none;
1046         }
1047         if (((mod & (Modifiers.ctrl | Modifiers.alt)) == (Modifiers.ctrl | Modifiers.alt)) && (
1048                 chr != 0))
1049         {
1050             // Filter out ctrl+alt (it means AltGr)
1051             mod = Modifiers.none;
1052         }
1053 
1054         for (; rpt > 0; rpt--)
1055         {
1056             if (key != key.graph || chr != 0)
1057             {
1058                 evs ~= newKeyEvent(key, chr, mod);
1059             }
1060         }
1061     }
1062 
1063     // calculate the modifiers from the CSI modifier parameter.
1064     Modifiers calcModifier(int n) pure nothrow @safe @nogc
1065     {
1066         n--;
1067         Modifiers m;
1068         if ((n & 1) != 0)
1069         {
1070             m |= Modifiers.shift;
1071         }
1072         if ((n & 2) != 0)
1073         {
1074             m |= Modifiers.alt;
1075         }
1076         if ((n & 4) != 0)
1077         {
1078             m |= Modifiers.ctrl;
1079         }
1080         if ((n & 8) != 0)
1081         {
1082             m |= Modifiers.meta; // kitty calls this Super
1083         }
1084         if ((n & 16) != 0)
1085         {
1086             m |= Modifiers.hyper;
1087         }
1088         if ((n & 32) != 0)
1089         {
1090             m |= Modifiers.meta; // for now not separating from Super
1091         }
1092         // Not doing (kitty only):
1093         // caps_lock 0b1000000   (64)
1094         // num_lock  0b10000000  (128)
1095 
1096         return m;
1097     }
1098 
1099     Event newFocusEvent(bool focused) nothrow @safe
1100     {
1101         Event ev =
1102         {
1103             type: EventType.focus, when: MonoTime.currTime(), focus: {
1104                 focused: focused
1105             }
1106         };
1107         return ev;
1108     }
1109 
1110     Event newKeyEvent(Key k, dchar dch = 0, Modifiers mod = Modifiers.none) nothrow @safe
1111     {
1112         if (escaped)
1113         {
1114             mod |= Modifiers.alt;
1115             escaped = false;
1116         }
1117         if (dch < ' ' && k < Key.graph)
1118         {
1119             switch (cast(int) k)
1120             {
1121             case 0xd, 0xa:
1122                 k = Key.enter;
1123                 break;
1124             case 0x9:
1125                 k = Key.tab;
1126                 break;
1127             case 0x8:
1128                 k = Key.backspace;
1129                 break;
1130             case 0x1b:
1131                 k = Key.esc;
1132                 break;
1133             case 0: // control-space
1134                 k = Key.graph;
1135                 mod |= Modifiers.ctrl;
1136                 dch = ' ';
1137                 break;
1138             default:
1139                 // most likely entered with a CTRL keypress
1140                 k = Key.graph;
1141                 mod |= Modifiers.ctrl;
1142                 dch = dch + '\x60';
1143                 break;
1144             }
1145         }
1146 
1147         Event ev = {
1148             type: EventType.key, when: MonoTime.currTime(), key: {
1149                 key: k, ch: dch, mod: mod
1150             }
1151         };
1152         return ev;
1153     }
1154 
1155     Event newMouseEvent(int x, int y, Buttons btn, Modifiers mod) nothrow @safe
1156     {
1157         Event ev = {
1158             type: EventType.mouse, when: MonoTime.currTime, mouse: {
1159                 pos: Coord(x, y),
1160                 btn: btn,
1161                 mod: mod,
1162             }
1163         };
1164         return ev;
1165     }
1166 
1167     // NB: it is possible for x and y to be outside the current coordinates
1168     // (happens for click drag for example).  Consumer of the event should clip
1169     // the coordinates as needed.
1170     Event newMouseEvent(int x, int y, int btn) nothrow @safe
1171     {
1172         Event ev = {
1173             type: EventType.mouse, when: MonoTime.currTime, mouse: {
1174                 pos: Coord(x, y)
1175             }
1176         };
1177 
1178         // Mouse wheel has bit 6 set, no release events.  It should be noted
1179         // that wheel events are sometimes misdelivered as mouse button events
1180         // during a click-drag, so we debounce these, considering them to be
1181         // button press events unless we see an intervening release event.
1182 
1183         switch (btn & 0x43)
1184         {
1185         case 0:
1186             ev.mouse.btn = Buttons.button1;
1187             break;
1188         case 1:
1189             ev.mouse.btn = Buttons.button3;
1190             break;
1191         case 2:
1192             ev.mouse.btn = Buttons.button2;
1193             break;
1194         case 3:
1195             ev.mouse.btn = Buttons.none;
1196             break;
1197         case 0x40:
1198             ev.mouse.btn = Buttons.wheelUp;
1199             break;
1200         case 0x41:
1201             ev.mouse.btn = Buttons.wheelDown;
1202             break;
1203         default:
1204             break;
1205         }
1206         if (btn & 0x4)
1207             ev.mouse.mod |= Modifiers.shift;
1208         if (btn & 0x8)
1209             ev.mouse.mod |= Modifiers.alt;
1210         if (btn & 0x10)
1211             ev.mouse.mod |= Modifiers.ctrl;
1212         return ev;
1213     }
1214 
1215     Event newPasteEvent(string buffer) nothrow @safe
1216     {
1217         Event ev = {
1218             type: EventType.paste, when: MonoTime.currTime(), paste: {
1219                 content: buffer
1220             }
1221         };
1222         return ev;
1223     }
1224 
1225     Event newPasteEvent(ubyte[] buffer) nothrow @safe
1226     {
1227         Event ev = {
1228             type: EventType.paste, when: MonoTime.currTime(), paste: {
1229                 binary: buffer
1230             }
1231         };
1232         return ev;
1233     }
1234 
1235     unittest
1236     {
1237         import core.thread;
1238 
1239         // taken from xterm, but pared down
1240         Parser p = new Parser();
1241         assert(p.empty());
1242         assert(p.parse("")); // no data, is fine
1243         assert(p.parse("\x1bOC"));
1244         auto ev = p.events();
1245 
1246         assert(ev.length == 1);
1247         assert(ev[0].type == EventType.key);
1248         assert(ev[0].key.key == Key.right);
1249 
1250         // this tests that the timed pase parsing works -
1251         // escape sequences are kept partially until we
1252         // have a match or we have waited long enough.
1253         assert(p.parse(['\x1b', 'O']) == false);
1254         ev = p.events();
1255         assert(ev.length == 0);
1256         Thread.sleep(p.seqTime * 2);
1257         assert(p.parse([]) == true);
1258         ev = p.events();
1259         assert(ev.length == 1);
1260         assert(ev[0].type == EventType.key);
1261         assert(ev[0].key.key == Key.graph);
1262         assert(ev[0].key.mod == Modifiers.alt);
1263 
1264         // lone escape
1265         assert(p.parse(['\x1b']) == false);
1266         ev = p.events();
1267         assert(ev.length == 0);
1268         Thread.sleep(p.seqTime * 2);
1269         assert(p.parse([]) == true);
1270         ev = p.events();
1271         assert(ev.length == 1);
1272         assert(ev[0].type == EventType.key);
1273         assert(ev[0].key.key == Key.esc);
1274         assert(ev[0].key.mod == Modifiers.none);
1275 
1276         // try injecting paste events
1277         assert(p.parse(['\x1b', '[', '2', '0', '0', '~']));
1278         assert(p.parse(['A']));
1279         assert(p.parse(['\x1b', '[', '2', '0', '1', '~']));
1280 
1281         ev = p.events();
1282         assert(ev.length == 1);
1283         assert(ev[0].type == EventType.paste);
1284         assert(ev[0].paste.content == "A");
1285 
1286         // mouse events
1287         assert(p.parse(['\x1b', '[', '<', '3', ';', '2', ';', '3', 'M']));
1288         ev = p.events();
1289         assert(ev.length == 1);
1290         assert(ev[0].type == EventType.mouse);
1291         assert(ev[0].mouse.pos.x == 1);
1292         assert(ev[0].mouse.pos.y == 2);
1293 
1294         // unicode
1295         string b = [0xe2, 0x82, 0xac];
1296         assert(p.parse(b));
1297         ev = p.events();
1298         assert(ev.length == 1);
1299         assert(ev[0].type == EventType.key);
1300         assert(ev[0].key.key == Key.graph);
1301         assert(ev[0].key.ch == '€');
1302     }
1303 }