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 }