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 }