phi-code-tui 0.56.3 → 0.74.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +29 -11
  2. package/dist/autocomplete.d.ts +18 -14
  3. package/dist/autocomplete.d.ts.map +1 -1
  4. package/dist/autocomplete.js +151 -112
  5. package/dist/autocomplete.js.map +1 -1
  6. package/dist/components/box.d.ts.map +1 -1
  7. package/dist/components/box.js +6 -1
  8. package/dist/components/box.js.map +1 -1
  9. package/dist/components/cancellable-loader.d.ts.map +1 -1
  10. package/dist/components/cancellable-loader.js +6 -7
  11. package/dist/components/cancellable-loader.js.map +1 -1
  12. package/dist/components/editor.d.ts +45 -1
  13. package/dist/components/editor.d.ts.map +1 -1
  14. package/dist/components/editor.js +505 -221
  15. package/dist/components/editor.js.map +1 -1
  16. package/dist/components/image.d.ts.map +1 -1
  17. package/dist/components/image.js +22 -7
  18. package/dist/components/image.js.map +1 -1
  19. package/dist/components/input.d.ts.map +1 -1
  20. package/dist/components/input.js +57 -74
  21. package/dist/components/input.js.map +1 -1
  22. package/dist/components/loader.d.ts +12 -2
  23. package/dist/components/loader.d.ts.map +1 -1
  24. package/dist/components/loader.js +36 -13
  25. package/dist/components/loader.js.map +1 -1
  26. package/dist/components/markdown.d.ts +0 -5
  27. package/dist/components/markdown.d.ts.map +1 -1
  28. package/dist/components/markdown.js +101 -114
  29. package/dist/components/markdown.js.map +1 -1
  30. package/dist/components/select-list.d.ts +19 -1
  31. package/dist/components/select-list.d.ts.map +1 -1
  32. package/dist/components/select-list.js +82 -71
  33. package/dist/components/select-list.js.map +1 -1
  34. package/dist/components/settings-list.d.ts.map +1 -1
  35. package/dist/components/settings-list.js +18 -10
  36. package/dist/components/settings-list.js.map +1 -1
  37. package/dist/components/spacer.d.ts.map +1 -1
  38. package/dist/components/spacer.js +1 -0
  39. package/dist/components/spacer.js.map +1 -1
  40. package/dist/components/text.d.ts.map +1 -1
  41. package/dist/components/text.js +8 -0
  42. package/dist/components/text.js.map +1 -1
  43. package/dist/components/truncated-text.d.ts.map +1 -1
  44. package/dist/components/truncated-text.js +3 -0
  45. package/dist/components/truncated-text.js.map +1 -1
  46. package/dist/editor-component.d.ts.map +1 -1
  47. package/dist/fuzzy.d.ts.map +1 -1
  48. package/dist/fuzzy.js +3 -0
  49. package/dist/fuzzy.js.map +1 -1
  50. package/dist/index.d.ts +5 -5
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +3 -3
  53. package/dist/index.js.map +1 -1
  54. package/dist/keybindings.d.ts +187 -33
  55. package/dist/keybindings.d.ts.map +1 -1
  56. package/dist/keybindings.js +156 -95
  57. package/dist/keybindings.js.map +1 -1
  58. package/dist/keys.d.ts +21 -12
  59. package/dist/keys.d.ts.map +1 -1
  60. package/dist/keys.js +270 -112
  61. package/dist/keys.js.map +1 -1
  62. package/dist/kill-ring.d.ts.map +1 -1
  63. package/dist/kill-ring.js +1 -3
  64. package/dist/kill-ring.js.map +1 -1
  65. package/dist/stdin-buffer.d.ts +2 -0
  66. package/dist/stdin-buffer.d.ts.map +1 -1
  67. package/dist/stdin-buffer.js +31 -8
  68. package/dist/stdin-buffer.js.map +1 -1
  69. package/dist/terminal-image.d.ts +17 -0
  70. package/dist/terminal-image.d.ts.map +1 -1
  71. package/dist/terminal-image.js +41 -5
  72. package/dist/terminal-image.js.map +1 -1
  73. package/dist/terminal.d.ts +4 -0
  74. package/dist/terminal.d.ts.map +1 -1
  75. package/dist/terminal.js +56 -8
  76. package/dist/terminal.js.map +1 -1
  77. package/dist/tui.d.ts +21 -5
  78. package/dist/tui.d.ts.map +1 -1
  79. package/dist/tui.js +234 -118
  80. package/dist/tui.js.map +1 -1
  81. package/dist/undo-stack.d.ts.map +1 -1
  82. package/dist/undo-stack.js +1 -3
  83. package/dist/undo-stack.js.map +1 -1
  84. package/dist/utils.d.ts +1 -0
  85. package/dist/utils.d.ts.map +1 -1
  86. package/dist/utils.js +281 -81
  87. package/dist/utils.js.map +1 -1
  88. package/package.json +3 -3
package/dist/keys.js CHANGED
@@ -40,8 +40,8 @@ export function isKittyProtocolActive() {
40
40
  * Usage:
41
41
  * - Key.escape, Key.enter, Key.tab, etc. for special keys
42
42
  * - Key.backtick, Key.comma, Key.period, etc. for symbol keys
43
- * - Key.ctrl("c"), Key.alt("x") for single modifier
44
- * - Key.ctrlShift("p"), Key.ctrlAlt("x") for combined modifiers
43
+ * - Key.ctrl("c"), Key.alt("x"), Key.super("k") for single modifiers
44
+ * - Key.ctrlShift("p"), Key.ctrlAlt("x"), Key.ctrlSuper("k") for combined modifiers
45
45
  */
46
46
  export const Key = {
47
47
  // Special keys
@@ -111,6 +111,7 @@ export const Key = {
111
111
  ctrl: (key) => `ctrl+${key}`,
112
112
  shift: (key) => `shift+${key}`,
113
113
  alt: (key) => `alt+${key}`,
114
+ super: (key) => `super+${key}`,
114
115
  // Combined modifiers
115
116
  ctrlShift: (key) => `ctrl+shift+${key}`,
116
117
  shiftCtrl: (key) => `shift+ctrl+${key}`,
@@ -118,8 +119,15 @@ export const Key = {
118
119
  altCtrl: (key) => `alt+ctrl+${key}`,
119
120
  shiftAlt: (key) => `shift+alt+${key}`,
120
121
  altShift: (key) => `alt+shift+${key}`,
122
+ ctrlSuper: (key) => `ctrl+super+${key}`,
123
+ superCtrl: (key) => `super+ctrl+${key}`,
124
+ shiftSuper: (key) => `shift+super+${key}`,
125
+ superShift: (key) => `super+shift+${key}`,
126
+ altSuper: (key) => `alt+super+${key}`,
127
+ superAlt: (key) => `super+alt+${key}`,
121
128
  // Triple modifiers
122
129
  ctrlShiftAlt: (key) => `ctrl+shift+alt+${key}`,
130
+ ctrlShiftSuper: (key) => `ctrl+shift+super+${key}`,
123
131
  };
124
132
  // =============================================================================
125
133
  // Constants
@@ -161,6 +169,7 @@ const MODIFIERS = {
161
169
  shift: 1,
162
170
  alt: 2,
163
171
  ctrl: 4,
172
+ super: 8,
164
173
  };
165
174
  const LOCK_MASK = 64 + 128; // Caps Lock + Num Lock
166
175
  const CODEPOINTS = {
@@ -185,6 +194,45 @@ const FUNCTIONAL_CODEPOINTS = {
185
194
  home: -14,
186
195
  end: -15,
187
196
  };
197
+ const KITTY_FUNCTIONAL_KEY_EQUIVALENTS = new Map([
198
+ [57399, 48], // KP_0 -> 0
199
+ [57400, 49], // KP_1 -> 1
200
+ [57401, 50], // KP_2 -> 2
201
+ [57402, 51], // KP_3 -> 3
202
+ [57403, 52], // KP_4 -> 4
203
+ [57404, 53], // KP_5 -> 5
204
+ [57405, 54], // KP_6 -> 6
205
+ [57406, 55], // KP_7 -> 7
206
+ [57407, 56], // KP_8 -> 8
207
+ [57408, 57], // KP_9 -> 9
208
+ [57409, 46], // KP_DECIMAL -> .
209
+ [57410, 47], // KP_DIVIDE -> /
210
+ [57411, 42], // KP_MULTIPLY -> *
211
+ [57412, 45], // KP_SUBTRACT -> -
212
+ [57413, 43], // KP_ADD -> +
213
+ [57415, 61], // KP_EQUAL -> =
214
+ [57416, 44], // KP_SEPARATOR -> ,
215
+ [57417, ARROW_CODEPOINTS.left],
216
+ [57418, ARROW_CODEPOINTS.right],
217
+ [57419, ARROW_CODEPOINTS.up],
218
+ [57420, ARROW_CODEPOINTS.down],
219
+ [57421, FUNCTIONAL_CODEPOINTS.pageUp],
220
+ [57422, FUNCTIONAL_CODEPOINTS.pageDown],
221
+ [57423, FUNCTIONAL_CODEPOINTS.home],
222
+ [57424, FUNCTIONAL_CODEPOINTS.end],
223
+ [57425, FUNCTIONAL_CODEPOINTS.insert],
224
+ [57426, FUNCTIONAL_CODEPOINTS.delete],
225
+ ]);
226
+ function normalizeKittyFunctionalCodepoint(codepoint) {
227
+ return KITTY_FUNCTIONAL_KEY_EQUIVALENTS.get(codepoint) ?? codepoint;
228
+ }
229
+ function normalizeShiftedLetterIdentityCodepoint(codepoint, modifier) {
230
+ const effectiveModifier = modifier & ~LOCK_MASK;
231
+ if ((effectiveModifier & MODIFIERS.shift) !== 0 && codepoint >= 65 && codepoint <= 90) {
232
+ return codepoint + 32;
233
+ }
234
+ return codepoint;
235
+ }
188
236
  const LEGACY_KEY_SEQUENCES = {
189
237
  up: ["\x1b[A", "\x1bOA"],
190
238
  down: ["\x1b[B", "\x1bOB"],
@@ -436,8 +484,10 @@ function matchesKittySequence(data, expectedCodepoint, expectedModifier) {
436
484
  // Check if modifiers match
437
485
  if (actualMod !== expectedMod)
438
486
  return false;
439
- // Primary match: codepoint matches directly
440
- if (parsed.codepoint === expectedCodepoint)
487
+ const normalizedCodepoint = normalizeShiftedLetterIdentityCodepoint(normalizeKittyFunctionalCodepoint(parsed.codepoint), parsed.modifier);
488
+ const normalizedExpectedCodepoint = normalizeShiftedLetterIdentityCodepoint(normalizeKittyFunctionalCodepoint(expectedCodepoint), expectedModifier);
489
+ // Primary match: codepoint matches directly after normalizing functional keys
490
+ if (normalizedCodepoint === normalizedExpectedCodepoint)
441
491
  return true;
442
492
  // Alternate match: use base layout key for non-Latin keyboard layouts.
443
493
  // This allows Ctrl+С (Cyrillic) to match Ctrl+c (Latin) when terminal reports
@@ -452,7 +502,7 @@ function matchesKittySequence(data, expectedCodepoint, expectedModifier) {
452
502
  // (letter remapping) and Ctrl+/ could falsely match Ctrl+[ (symbol remapping)
453
503
  // if the base layout key were always considered.
454
504
  if (parsed.baseLayoutKey !== undefined && parsed.baseLayoutKey === expectedCodepoint) {
455
- const cp = parsed.codepoint;
505
+ const cp = normalizedCodepoint;
456
506
  const isLatinLetter = cp >= 97 && cp <= 122; // a-z
457
507
  const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(cp));
458
508
  if (!isLatinLetter && !isKnownSymbol)
@@ -460,20 +510,43 @@ function matchesKittySequence(data, expectedCodepoint, expectedModifier) {
460
510
  }
461
511
  return false;
462
512
  }
513
+ function parseModifyOtherKeysSequence(data) {
514
+ const match = data.match(/^\x1b\[27;(\d+);(\d+)~$/);
515
+ if (!match)
516
+ return null;
517
+ const modValue = parseInt(match[1], 10);
518
+ const codepoint = parseInt(match[2], 10);
519
+ return { codepoint, modifier: modValue - 1 };
520
+ }
463
521
  /**
464
522
  * Match xterm modifyOtherKeys format: CSI 27 ; modifiers ; keycode ~
465
523
  * This is used by terminals when Kitty protocol is not enabled.
466
524
  * Modifier values are 1-indexed: 2=shift, 3=alt, 5=ctrl, etc.
467
525
  */
468
526
  function matchesModifyOtherKeys(data, expectedKeycode, expectedModifier) {
469
- const match = data.match(/^\x1b\[27;(\d+);(\d+)~$/);
470
- if (!match)
527
+ const parsed = parseModifyOtherKeysSequence(data);
528
+ if (!parsed)
471
529
  return false;
472
- const modValue = parseInt(match[1], 10);
473
- const keycode = parseInt(match[2], 10);
474
- // Convert from 1-indexed xterm format to our 0-indexed format
475
- const actualMod = modValue - 1;
476
- return keycode === expectedKeycode && actualMod === expectedModifier;
530
+ return parsed.codepoint === expectedKeycode && parsed.modifier === expectedModifier;
531
+ }
532
+ function isWindowsTerminalSession() {
533
+ return (Boolean(process.env.WT_SESSION) && !process.env.SSH_CONNECTION && !process.env.SSH_CLIENT && !process.env.SSH_TTY);
534
+ }
535
+ /**
536
+ * Raw 0x08 (BS) is ambiguous in legacy terminals.
537
+ *
538
+ * - Windows Terminal uses it for Ctrl+Backspace.
539
+ * - Some legacy terminals and tmux setups send it for plain Backspace.
540
+ *
541
+ * Prefer explicit Kitty / CSI-u / modifyOtherKeys sequences whenever they are
542
+ * available. Fall back to a Windows Terminal heuristic only for raw BS bytes.
543
+ */
544
+ function matchesRawBackspace(data, expectedModifier) {
545
+ if (data === "\x7f")
546
+ return expectedModifier === 0;
547
+ if (data !== "\x08")
548
+ return false;
549
+ return isWindowsTerminalSession() ? expectedModifier === MODIFIERS.ctrl : expectedModifier === 0;
477
550
  }
478
551
  // =============================================================================
479
552
  // Generic Key Matching
@@ -499,6 +572,34 @@ function rawCtrlChar(key) {
499
572
  }
500
573
  return null;
501
574
  }
575
+ function isDigitKey(key) {
576
+ return key >= "0" && key <= "9";
577
+ }
578
+ function matchesPrintableModifyOtherKeys(data, expectedKeycode, expectedModifier) {
579
+ if (expectedModifier === 0)
580
+ return false;
581
+ const parsed = parseModifyOtherKeysSequence(data);
582
+ if (!parsed || parsed.modifier !== expectedModifier)
583
+ return false;
584
+ return (normalizeShiftedLetterIdentityCodepoint(parsed.codepoint, parsed.modifier) ===
585
+ normalizeShiftedLetterIdentityCodepoint(expectedKeycode, expectedModifier));
586
+ }
587
+ function formatKeyNameWithModifiers(keyName, modifier) {
588
+ const mods = [];
589
+ const effectiveMod = modifier & ~LOCK_MASK;
590
+ const supportedModifierMask = MODIFIERS.shift | MODIFIERS.ctrl | MODIFIERS.alt | MODIFIERS.super;
591
+ if ((effectiveMod & ~supportedModifierMask) !== 0)
592
+ return undefined;
593
+ if (effectiveMod & MODIFIERS.shift)
594
+ mods.push("shift");
595
+ if (effectiveMod & MODIFIERS.ctrl)
596
+ mods.push("ctrl");
597
+ if (effectiveMod & MODIFIERS.alt)
598
+ mods.push("alt");
599
+ if (effectiveMod & MODIFIERS.super)
600
+ mods.push("super");
601
+ return mods.length > 0 ? `${mods.join("+")}+${keyName}` : keyName;
602
+ }
502
603
  function parseKeyId(keyId) {
503
604
  const parts = keyId.toLowerCase().split("+");
504
605
  const key = parts[parts.length - 1];
@@ -509,6 +610,7 @@ function parseKeyId(keyId) {
509
610
  ctrl: parts.includes("ctrl"),
510
611
  shift: parts.includes("shift"),
511
612
  alt: parts.includes("alt"),
613
+ super: parts.includes("super"),
512
614
  };
513
615
  }
514
616
  /**
@@ -520,9 +622,10 @@ function parseKeyId(keyId) {
520
622
  * - Ctrl combinations: "ctrl+c", "ctrl+z", etc.
521
623
  * - Shift combinations: "shift+tab", "shift+enter"
522
624
  * - Alt combinations: "alt+enter", "alt+backspace"
523
- * - Combined modifiers: "shift+ctrl+p", "ctrl+alt+x"
625
+ * - Super combinations: "super+k", "super+enter"
626
+ * - Combined modifiers: "shift+ctrl+p", "ctrl+alt+x", "ctrl+super+k"
524
627
  *
525
- * Use the Key helper for autocomplete: Key.ctrl("c"), Key.escape, Key.ctrlShift("p")
628
+ * Use the Key helper for autocomplete: Key.ctrl("c"), Key.escape, Key.ctrlShift("p"), Key.super("k")
526
629
  *
527
630
  * @param data - Raw input data from terminal
528
631
  * @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c"))
@@ -531,7 +634,7 @@ export function matchesKey(data, keyId) {
531
634
  const parsed = parseKeyId(keyId);
532
635
  if (!parsed)
533
636
  return false;
534
- const { key, ctrl, shift, alt } = parsed;
637
+ const { key, ctrl, shift, alt, super: superModifier } = parsed;
535
638
  let modifier = 0;
536
639
  if (shift)
537
640
  modifier |= MODIFIERS.shift;
@@ -539,36 +642,46 @@ export function matchesKey(data, keyId) {
539
642
  modifier |= MODIFIERS.alt;
540
643
  if (ctrl)
541
644
  modifier |= MODIFIERS.ctrl;
645
+ if (superModifier)
646
+ modifier |= MODIFIERS.super;
542
647
  switch (key) {
543
648
  case "escape":
544
649
  case "esc":
545
650
  if (modifier !== 0)
546
651
  return false;
547
- return data === "\x1b" || matchesKittySequence(data, CODEPOINTS.escape, 0);
652
+ return (data === "\x1b" ||
653
+ matchesKittySequence(data, CODEPOINTS.escape, 0) ||
654
+ matchesModifyOtherKeys(data, CODEPOINTS.escape, 0));
548
655
  case "space":
549
656
  if (!_kittyProtocolActive) {
550
- if (ctrl && !alt && !shift && data === "\x00") {
657
+ if (modifier === MODIFIERS.ctrl && data === "\x00") {
551
658
  return true;
552
659
  }
553
- if (alt && !ctrl && !shift && data === "\x1b ") {
660
+ if (modifier === MODIFIERS.alt && data === "\x1b ") {
554
661
  return true;
555
662
  }
556
663
  }
557
664
  if (modifier === 0) {
558
- return data === " " || matchesKittySequence(data, CODEPOINTS.space, 0);
665
+ return (data === " " ||
666
+ matchesKittySequence(data, CODEPOINTS.space, 0) ||
667
+ matchesModifyOtherKeys(data, CODEPOINTS.space, 0));
559
668
  }
560
- return matchesKittySequence(data, CODEPOINTS.space, modifier);
669
+ return (matchesKittySequence(data, CODEPOINTS.space, modifier) ||
670
+ matchesModifyOtherKeys(data, CODEPOINTS.space, modifier));
561
671
  case "tab":
562
- if (shift && !ctrl && !alt) {
563
- return data === "\x1b[Z" || matchesKittySequence(data, CODEPOINTS.tab, MODIFIERS.shift);
672
+ if (modifier === MODIFIERS.shift) {
673
+ return (data === "\x1b[Z" ||
674
+ matchesKittySequence(data, CODEPOINTS.tab, MODIFIERS.shift) ||
675
+ matchesModifyOtherKeys(data, CODEPOINTS.tab, MODIFIERS.shift));
564
676
  }
565
677
  if (modifier === 0) {
566
678
  return data === "\t" || matchesKittySequence(data, CODEPOINTS.tab, 0);
567
679
  }
568
- return matchesKittySequence(data, CODEPOINTS.tab, modifier);
680
+ return (matchesKittySequence(data, CODEPOINTS.tab, modifier) ||
681
+ matchesModifyOtherKeys(data, CODEPOINTS.tab, modifier));
569
682
  case "enter":
570
683
  case "return":
571
- if (shift && !ctrl && !alt) {
684
+ if (modifier === MODIFIERS.shift) {
572
685
  // CSI u sequences (standard Kitty protocol)
573
686
  if (matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.shift) ||
574
687
  matchesKittySequence(data, CODEPOINTS.kpEnter, MODIFIERS.shift)) {
@@ -586,7 +699,7 @@ export function matchesKey(data, keyId) {
586
699
  }
587
700
  return false;
588
701
  }
589
- if (alt && !ctrl && !shift) {
702
+ if (modifier === MODIFIERS.alt) {
590
703
  // CSI u sequences (standard Kitty protocol)
591
704
  if (matchesKittySequence(data, CODEPOINTS.enter, MODIFIERS.alt) ||
592
705
  matchesKittySequence(data, CODEPOINTS.kpEnter, MODIFIERS.alt)) {
@@ -611,18 +724,32 @@ export function matchesKey(data, keyId) {
611
724
  matchesKittySequence(data, CODEPOINTS.kpEnter, 0));
612
725
  }
613
726
  return (matchesKittySequence(data, CODEPOINTS.enter, modifier) ||
614
- matchesKittySequence(data, CODEPOINTS.kpEnter, modifier));
727
+ matchesKittySequence(data, CODEPOINTS.kpEnter, modifier) ||
728
+ matchesModifyOtherKeys(data, CODEPOINTS.enter, modifier));
615
729
  case "backspace":
616
- if (alt && !ctrl && !shift) {
730
+ if (modifier === MODIFIERS.alt) {
617
731
  if (data === "\x1b\x7f" || data === "\x1b\b") {
618
732
  return true;
619
733
  }
620
- return matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt);
734
+ return (matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt) ||
735
+ matchesModifyOtherKeys(data, CODEPOINTS.backspace, MODIFIERS.alt));
736
+ }
737
+ if (modifier === MODIFIERS.ctrl) {
738
+ // Legacy raw 0x08 is ambiguous: it can be Ctrl+Backspace on Windows
739
+ // Terminal or plain Backspace on other terminals, while also
740
+ // overlapping with Ctrl+H.
741
+ if (matchesRawBackspace(data, MODIFIERS.ctrl))
742
+ return true;
743
+ return (matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.ctrl) ||
744
+ matchesModifyOtherKeys(data, CODEPOINTS.backspace, MODIFIERS.ctrl));
621
745
  }
622
746
  if (modifier === 0) {
623
- return data === "\x7f" || data === "\x08" || matchesKittySequence(data, CODEPOINTS.backspace, 0);
747
+ return (matchesRawBackspace(data, 0) ||
748
+ matchesKittySequence(data, CODEPOINTS.backspace, 0) ||
749
+ matchesModifyOtherKeys(data, CODEPOINTS.backspace, 0));
624
750
  }
625
- return matchesKittySequence(data, CODEPOINTS.backspace, modifier);
751
+ return (matchesKittySequence(data, CODEPOINTS.backspace, modifier) ||
752
+ matchesModifyOtherKeys(data, CODEPOINTS.backspace, modifier));
626
753
  case "insert":
627
754
  if (modifier === 0) {
628
755
  return (matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.insert) ||
@@ -683,7 +810,7 @@ export function matchesKey(data, keyId) {
683
810
  }
684
811
  return matchesKittySequence(data, FUNCTIONAL_CODEPOINTS.pageDown, modifier);
685
812
  case "up":
686
- if (alt && !ctrl && !shift) {
813
+ if (modifier === MODIFIERS.alt) {
687
814
  return data === "\x1bp" || matchesKittySequence(data, ARROW_CODEPOINTS.up, MODIFIERS.alt);
688
815
  }
689
816
  if (modifier === 0) {
@@ -695,7 +822,7 @@ export function matchesKey(data, keyId) {
695
822
  }
696
823
  return matchesKittySequence(data, ARROW_CODEPOINTS.up, modifier);
697
824
  case "down":
698
- if (alt && !ctrl && !shift) {
825
+ if (modifier === MODIFIERS.alt) {
699
826
  return data === "\x1bn" || matchesKittySequence(data, ARROW_CODEPOINTS.down, MODIFIERS.alt);
700
827
  }
701
828
  if (modifier === 0) {
@@ -707,13 +834,13 @@ export function matchesKey(data, keyId) {
707
834
  }
708
835
  return matchesKittySequence(data, ARROW_CODEPOINTS.down, modifier);
709
836
  case "left":
710
- if (alt && !ctrl && !shift) {
837
+ if (modifier === MODIFIERS.alt) {
711
838
  return (data === "\x1b[1;3D" ||
712
839
  (!_kittyProtocolActive && data === "\x1bB") ||
713
840
  data === "\x1bb" ||
714
841
  matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.alt));
715
842
  }
716
- if (ctrl && !alt && !shift) {
843
+ if (modifier === MODIFIERS.ctrl) {
717
844
  return (data === "\x1b[1;5D" ||
718
845
  matchesLegacyModifierSequence(data, "left", MODIFIERS.ctrl) ||
719
846
  matchesKittySequence(data, ARROW_CODEPOINTS.left, MODIFIERS.ctrl));
@@ -727,13 +854,13 @@ export function matchesKey(data, keyId) {
727
854
  }
728
855
  return matchesKittySequence(data, ARROW_CODEPOINTS.left, modifier);
729
856
  case "right":
730
- if (alt && !ctrl && !shift) {
857
+ if (modifier === MODIFIERS.alt) {
731
858
  return (data === "\x1b[1;3C" ||
732
859
  (!_kittyProtocolActive && data === "\x1bF") ||
733
860
  data === "\x1bf" ||
734
861
  matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.alt));
735
862
  }
736
- if (ctrl && !alt && !shift) {
863
+ if (modifier === MODIFIERS.ctrl) {
737
864
  return (data === "\x1b[1;5C" ||
738
865
  matchesLegacyModifierSequence(data, "right", MODIFIERS.ctrl) ||
739
866
  matchesKittySequence(data, ARROW_CODEPOINTS.right, MODIFIERS.ctrl));
@@ -765,36 +892,45 @@ export function matchesKey(data, keyId) {
765
892
  return matchesLegacySequence(data, LEGACY_KEY_SEQUENCES[functionKey]);
766
893
  }
767
894
  }
768
- // Handle single letter keys (a-z) and some symbols
769
- if (key.length === 1 && ((key >= "a" && key <= "z") || SYMBOL_KEYS.has(key))) {
895
+ // Handle single letter/digit keys and symbols
896
+ if (key.length === 1 && ((key >= "a" && key <= "z") || isDigitKey(key) || SYMBOL_KEYS.has(key))) {
770
897
  const codepoint = key.charCodeAt(0);
771
898
  const rawCtrl = rawCtrlChar(key);
772
- if (ctrl && alt && !shift && !_kittyProtocolActive && rawCtrl) {
773
- // Legacy: ctrl+alt+key is ESC followed by the control character
774
- return data === `\x1b${rawCtrl}`;
899
+ const isLetter = key >= "a" && key <= "z";
900
+ const isDigit = isDigitKey(key);
901
+ if (modifier === MODIFIERS.ctrl + MODIFIERS.alt && !_kittyProtocolActive && rawCtrl) {
902
+ // Legacy: ctrl+alt+key is ESC followed by the control character.
903
+ // If that legacy form does not match, continue so CSI-u and
904
+ // modifyOtherKeys sequences from tmux can still be recognized.
905
+ if (data === `\x1b${rawCtrl}`)
906
+ return true;
775
907
  }
776
- if (alt && !ctrl && !shift && !_kittyProtocolActive && key >= "a" && key <= "z") {
777
- // Legacy: alt+letter is ESC followed by the letter
908
+ if (modifier === MODIFIERS.alt && !_kittyProtocolActive && (isLetter || isDigit)) {
909
+ // Legacy: alt+letter/digit is ESC followed by the key
778
910
  if (data === `\x1b${key}`)
779
911
  return true;
780
912
  }
781
- if (ctrl && !shift && !alt) {
913
+ if (modifier === MODIFIERS.ctrl) {
782
914
  // Legacy: ctrl+key sends the control character
783
915
  if (rawCtrl && data === rawCtrl)
784
916
  return true;
785
- return matchesKittySequence(data, codepoint, MODIFIERS.ctrl);
917
+ return (matchesKittySequence(data, codepoint, MODIFIERS.ctrl) ||
918
+ matchesPrintableModifyOtherKeys(data, codepoint, MODIFIERS.ctrl));
786
919
  }
787
- if (ctrl && shift && !alt) {
788
- return matchesKittySequence(data, codepoint, MODIFIERS.shift + MODIFIERS.ctrl);
920
+ if (modifier === MODIFIERS.shift + MODIFIERS.ctrl) {
921
+ return (matchesKittySequence(data, codepoint, MODIFIERS.shift + MODIFIERS.ctrl) ||
922
+ matchesPrintableModifyOtherKeys(data, codepoint, MODIFIERS.shift + MODIFIERS.ctrl));
789
923
  }
790
- if (shift && !ctrl && !alt) {
924
+ if (modifier === MODIFIERS.shift) {
791
925
  // Legacy: shift+letter produces uppercase
792
- if (data === key.toUpperCase())
926
+ if (isLetter && data === key.toUpperCase())
793
927
  return true;
794
- return matchesKittySequence(data, codepoint, MODIFIERS.shift);
928
+ return (matchesKittySequence(data, codepoint, MODIFIERS.shift) ||
929
+ matchesPrintableModifyOtherKeys(data, codepoint, MODIFIERS.shift));
795
930
  }
796
931
  if (modifier !== 0) {
797
- return matchesKittySequence(data, codepoint, modifier);
932
+ return (matchesKittySequence(data, codepoint, modifier) ||
933
+ matchesPrintableModifyOtherKeys(data, codepoint, modifier));
798
934
  }
799
935
  // Check both raw char and Kitty sequence (needed for release events)
800
936
  return data === key || matchesKittySequence(data, codepoint, 0);
@@ -807,67 +943,67 @@ export function matchesKey(data, keyId) {
807
943
  * @param data - Raw input data from terminal
808
944
  * @returns Key identifier string (e.g., "ctrl+c") or undefined
809
945
  */
946
+ function formatParsedKey(codepoint, modifier, baseLayoutKey) {
947
+ const normalizedCodepoint = normalizeKittyFunctionalCodepoint(codepoint);
948
+ const identityCodepoint = normalizeShiftedLetterIdentityCodepoint(normalizedCodepoint, modifier);
949
+ // Use base layout key only when codepoint is not a recognized Latin
950
+ // letter (a-z), digit (0-9), or symbol (/, -, [, ;, etc.). For those,
951
+ // the codepoint is authoritative regardless of physical key position.
952
+ // This prevents remapped layouts (Dvorak, Colemak, xremap, etc.) from
953
+ // reporting the wrong key name based on the QWERTY physical position.
954
+ const isLatinLetter = identityCodepoint >= 97 && identityCodepoint <= 122; // a-z
955
+ const isDigit = identityCodepoint >= 48 && identityCodepoint <= 57; // 0-9
956
+ const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(identityCodepoint));
957
+ const effectiveCodepoint = isLatinLetter || isDigit || isKnownSymbol ? identityCodepoint : (baseLayoutKey ?? identityCodepoint);
958
+ let keyName;
959
+ if (effectiveCodepoint === CODEPOINTS.escape)
960
+ keyName = "escape";
961
+ else if (effectiveCodepoint === CODEPOINTS.tab)
962
+ keyName = "tab";
963
+ else if (effectiveCodepoint === CODEPOINTS.enter || effectiveCodepoint === CODEPOINTS.kpEnter)
964
+ keyName = "enter";
965
+ else if (effectiveCodepoint === CODEPOINTS.space)
966
+ keyName = "space";
967
+ else if (effectiveCodepoint === CODEPOINTS.backspace)
968
+ keyName = "backspace";
969
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.delete)
970
+ keyName = "delete";
971
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.insert)
972
+ keyName = "insert";
973
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.home)
974
+ keyName = "home";
975
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.end)
976
+ keyName = "end";
977
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.pageUp)
978
+ keyName = "pageUp";
979
+ else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.pageDown)
980
+ keyName = "pageDown";
981
+ else if (effectiveCodepoint === ARROW_CODEPOINTS.up)
982
+ keyName = "up";
983
+ else if (effectiveCodepoint === ARROW_CODEPOINTS.down)
984
+ keyName = "down";
985
+ else if (effectiveCodepoint === ARROW_CODEPOINTS.left)
986
+ keyName = "left";
987
+ else if (effectiveCodepoint === ARROW_CODEPOINTS.right)
988
+ keyName = "right";
989
+ else if (effectiveCodepoint >= 48 && effectiveCodepoint <= 57)
990
+ keyName = String.fromCharCode(effectiveCodepoint);
991
+ else if (effectiveCodepoint >= 97 && effectiveCodepoint <= 122)
992
+ keyName = String.fromCharCode(effectiveCodepoint);
993
+ else if (SYMBOL_KEYS.has(String.fromCharCode(effectiveCodepoint)))
994
+ keyName = String.fromCharCode(effectiveCodepoint);
995
+ if (!keyName)
996
+ return undefined;
997
+ return formatKeyNameWithModifiers(keyName, modifier);
998
+ }
810
999
  export function parseKey(data) {
811
1000
  const kitty = parseKittySequence(data);
812
1001
  if (kitty) {
813
- const { codepoint, baseLayoutKey, modifier } = kitty;
814
- const mods = [];
815
- const effectiveMod = modifier & ~LOCK_MASK;
816
- const supportedModifierMask = MODIFIERS.shift | MODIFIERS.ctrl | MODIFIERS.alt;
817
- if ((effectiveMod & ~supportedModifierMask) !== 0)
818
- return undefined;
819
- if (effectiveMod & MODIFIERS.shift)
820
- mods.push("shift");
821
- if (effectiveMod & MODIFIERS.ctrl)
822
- mods.push("ctrl");
823
- if (effectiveMod & MODIFIERS.alt)
824
- mods.push("alt");
825
- // Use base layout key only when codepoint is not a recognized Latin
826
- // letter (a-z) or symbol (/, -, [, ;, etc.). For those, the codepoint
827
- // is authoritative regardless of physical key position. This prevents
828
- // remapped layouts (Dvorak, Colemak, xremap, etc.) from reporting the
829
- // wrong key name based on the QWERTY physical position.
830
- const isLatinLetter = codepoint >= 97 && codepoint <= 122; // a-z
831
- const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(codepoint));
832
- const effectiveCodepoint = isLatinLetter || isKnownSymbol ? codepoint : (baseLayoutKey ?? codepoint);
833
- let keyName;
834
- if (effectiveCodepoint === CODEPOINTS.escape)
835
- keyName = "escape";
836
- else if (effectiveCodepoint === CODEPOINTS.tab)
837
- keyName = "tab";
838
- else if (effectiveCodepoint === CODEPOINTS.enter || effectiveCodepoint === CODEPOINTS.kpEnter)
839
- keyName = "enter";
840
- else if (effectiveCodepoint === CODEPOINTS.space)
841
- keyName = "space";
842
- else if (effectiveCodepoint === CODEPOINTS.backspace)
843
- keyName = "backspace";
844
- else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.delete)
845
- keyName = "delete";
846
- else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.insert)
847
- keyName = "insert";
848
- else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.home)
849
- keyName = "home";
850
- else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.end)
851
- keyName = "end";
852
- else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.pageUp)
853
- keyName = "pageUp";
854
- else if (effectiveCodepoint === FUNCTIONAL_CODEPOINTS.pageDown)
855
- keyName = "pageDown";
856
- else if (effectiveCodepoint === ARROW_CODEPOINTS.up)
857
- keyName = "up";
858
- else if (effectiveCodepoint === ARROW_CODEPOINTS.down)
859
- keyName = "down";
860
- else if (effectiveCodepoint === ARROW_CODEPOINTS.left)
861
- keyName = "left";
862
- else if (effectiveCodepoint === ARROW_CODEPOINTS.right)
863
- keyName = "right";
864
- else if (effectiveCodepoint >= 97 && effectiveCodepoint <= 122)
865
- keyName = String.fromCharCode(effectiveCodepoint);
866
- else if (SYMBOL_KEYS.has(String.fromCharCode(effectiveCodepoint)))
867
- keyName = String.fromCharCode(effectiveCodepoint);
868
- if (keyName) {
869
- return mods.length > 0 ? `${mods.join("+")}+${keyName}` : keyName;
870
- }
1002
+ return formatParsedKey(kitty.codepoint, kitty.modifier, kitty.baseLayoutKey);
1003
+ }
1004
+ const modifyOtherKeys = parseModifyOtherKeysSequence(data);
1005
+ if (modifyOtherKeys) {
1006
+ return formatParsedKey(modifyOtherKeys.codepoint, modifyOtherKeys.modifier);
871
1007
  }
872
1008
  // Mode-aware legacy sequences
873
1009
  // When Kitty protocol is active, ambiguous sequences are interpreted as custom terminal mappings:
@@ -905,8 +1041,10 @@ export function parseKey(data) {
905
1041
  return "ctrl+space";
906
1042
  if (data === " ")
907
1043
  return "space";
908
- if (data === "\x7f" || data === "\x08")
1044
+ if (data === "\x7f")
909
1045
  return "backspace";
1046
+ if (data === "\x08")
1047
+ return isWindowsTerminalSession() ? "ctrl+backspace" : "backspace";
910
1048
  if (data === "\x1b[Z")
911
1049
  return "shift+tab";
912
1050
  if (!_kittyProtocolActive && data === "\x1b\r")
@@ -924,8 +1062,8 @@ export function parseKey(data) {
924
1062
  if (code >= 1 && code <= 26) {
925
1063
  return `ctrl+alt+${String.fromCharCode(code + 96)}`;
926
1064
  }
927
- // Legacy alt+letter (ESC followed by letter a-z)
928
- if (code >= 97 && code <= 122) {
1065
+ // Legacy alt+letter/digit (ESC followed by the key)
1066
+ if ((code >= 97 && code <= 122) || (code >= 48 && code <= 57)) {
929
1067
  return `alt+${String.fromCharCode(code)}`;
930
1068
  }
931
1069
  }
@@ -1002,6 +1140,7 @@ export function decodeKittyPrintable(data) {
1002
1140
  if (modifier & MODIFIERS.shift && typeof shiftedKey === "number") {
1003
1141
  effectiveCodepoint = shiftedKey;
1004
1142
  }
1143
+ effectiveCodepoint = normalizeKittyFunctionalCodepoint(effectiveCodepoint);
1005
1144
  // Drop control characters or invalid codepoints.
1006
1145
  if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32)
1007
1146
  return undefined;
@@ -1012,4 +1151,23 @@ export function decodeKittyPrintable(data) {
1012
1151
  return undefined;
1013
1152
  }
1014
1153
  }
1154
+ function decodeModifyOtherKeysPrintable(data) {
1155
+ const parsed = parseModifyOtherKeysSequence(data);
1156
+ if (!parsed)
1157
+ return undefined;
1158
+ const modifier = parsed.modifier & ~LOCK_MASK;
1159
+ if ((modifier & ~MODIFIERS.shift) !== 0)
1160
+ return undefined;
1161
+ if (!Number.isFinite(parsed.codepoint) || parsed.codepoint < 32)
1162
+ return undefined;
1163
+ try {
1164
+ return String.fromCodePoint(parsed.codepoint);
1165
+ }
1166
+ catch {
1167
+ return undefined;
1168
+ }
1169
+ }
1170
+ export function decodePrintableKey(data) {
1171
+ return decodeKittyPrintable(data) ?? decodeModifyOtherKeysPrintable(data);
1172
+ }
1015
1173
  //# sourceMappingURL=keys.js.map