@gajae-code/tui 0.13.0 → 0.13.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 (3) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/package.json +3 -3
  3. package/src/keys.ts +107 -13
package/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.13.1] - 2026-08-11
6
+
5
7
  ### Added
6
8
 
7
9
  - Added atomic same-line deletion APIs and an `Editor` undo callback for keeping application state synchronized with editor history.
@@ -10,6 +12,8 @@
10
12
  - macOS Terminal.app Option+Arrow input is now buffered and decoded as a single Meta-wrapped escape sequence, so Option+Up/Down can open and navigate queued-message selectors.
11
13
  - Terminal.app Meta-prefix decoding now covers legacy Option shortcuts for printable symbols, digits, spaces, and Ctrl+Option symbol chords while preserving enhanced Kitty and modifyOtherKeys Super/Command matching.
12
14
  - Kitty and modifyOtherKeys function-key sequences now match consistently for F1–F12, including unmodified CSI forms.
15
+ - Kitty protocol release/repeat and modifyOtherKeys lock-mask handling now fail closed and stay symmetric across native and TypeScript key matching, including duplicate macOS modifier aliases.
16
+ - TypeScript Kitty and modifyOtherKeys printable decoding now rejects surrogate and out-of-range Unicode code points before text extraction.
13
17
 
14
18
  ## [0.12.21] - 2026-08-09
15
19
 
@@ -26,6 +30,7 @@
26
30
  ### Changed
27
31
 
28
32
  - Native fuzzy matching and image encoding bindings now load only when their TUI feature is used instead of at module startup.
33
+ - Kitty-protocol shortcuts can match modified Korean Dubeolsik compatibility-jamo input when terminals omit base-layout metadata, without treating unmodified or Shift-only text input as shortcuts.
29
34
 
30
35
  ### Fixed
31
36
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/tui",
4
- "version": "0.13.0",
4
+ "version": "0.13.1",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://gajae-code.com",
7
7
  "author": "Yeachan-Heo and Gajae Code Contributors",
@@ -36,8 +36,8 @@
36
36
  "fmt": "biome format --write ."
37
37
  },
38
38
  "dependencies": {
39
- "@gajae-code/natives": "0.13.0",
40
- "@gajae-code/utils": "0.13.0",
39
+ "@gajae-code/natives": "0.13.1",
40
+ "@gajae-code/utils": "0.13.1",
41
41
  "lru-cache": "11.3.6",
42
42
  "marked": "18.0.6"
43
43
  },
package/src/keys.ts CHANGED
@@ -301,7 +301,7 @@ export function parseKeyId(value: string): ParsedKeyId | undefined {
301
301
  .map(part => part.trim());
302
302
  const modifiers: KeyModifier[] = [];
303
303
  for (const part of modifierParts) {
304
- const modifier = KEY_MODIFIER_ALIASES[part];
304
+ const modifier = Object.hasOwn(KEY_MODIFIER_ALIASES, part) ? KEY_MODIFIER_ALIASES[part] : undefined;
305
305
  if (!modifier || modifiers.includes(modifier)) return undefined;
306
306
  modifiers.push(modifier);
307
307
  }
@@ -427,9 +427,9 @@ interface ParsedKittySequence {
427
427
 
428
428
  // Regex for Kitty protocol event type detection
429
429
  // Matches CSI sequences with :2 (repeat) or :3 (release) event type
430
- // Format: \x1b[...;modifier:event_type<terminator> where terminator is u, ~, or A-F/H
431
- const KITTY_RELEASE_PATTERN = /^\x1b\[[\d:;]*:3[u~ABCDHF]$/;
432
- const KITTY_REPEAT_PATTERN = /^\x1b\[[\d:;]*:2[u~ABCDHF]$/;
430
+ // Format: \x1b[...;modifier:event_type<terminator> where terminator is u, ~, navigation, or function-key finals
431
+ const KITTY_RELEASE_PATTERN = /^\x1b\[[\d:;]*:3[u~ABCDEFHPQRS]$/;
432
+ const KITTY_REPEAT_PATTERN = /^\x1b\[[\d:;]*:2[u~ABCDEFHPQRS]$/;
433
433
  const KITTY_CSI_U_PATTERN = /^\x1b\[(\d+)(?::(\d*))?(?::(\d+))?(?:;(\d+))?(?::(\d+))?(?:;([\d:]*))?u$/;
434
434
  const KITTY_MOD_SHIFT = 1;
435
435
  const KITTY_MOD_ALT = 2;
@@ -437,6 +437,77 @@ const KITTY_MOD_CTRL = 4;
437
437
  const KITTY_MOD_SUPER = 8;
438
438
  const KITTY_MOD_NUM_LOCK = 128;
439
439
  const KITTY_LOCK_MASK = 64 + 128; // Caps Lock + Num Lock
440
+ const KOREAN_DUBEOLSIK_BASE_KEYS = new Map<number, BaseKey>(
441
+ [
442
+ ["ㄱ", "r"],
443
+ ["ㄲ", "r"],
444
+ ["ㄴ", "s"],
445
+ ["ㄷ", "e"],
446
+ ["ㄸ", "e"],
447
+ ["ㄹ", "f"],
448
+ ["ㅁ", "a"],
449
+ ["ㅂ", "q"],
450
+ ["ㅃ", "q"],
451
+ ["ㅅ", "t"],
452
+ ["ㅆ", "t"],
453
+ ["ㅇ", "d"],
454
+ ["ㅈ", "w"],
455
+ ["ㅉ", "w"],
456
+ ["ㅊ", "c"],
457
+ ["ㅋ", "z"],
458
+ ["ㅌ", "x"],
459
+ ["ㅍ", "v"],
460
+ ["ㅎ", "g"],
461
+ ["ㅏ", "k"],
462
+ ["ㅐ", "o"],
463
+ ["ㅑ", "i"],
464
+ ["ㅒ", "o"],
465
+ ["ㅓ", "j"],
466
+ ["ㅔ", "p"],
467
+ ["ㅕ", "u"],
468
+ ["ㅖ", "p"],
469
+ ["ㅗ", "h"],
470
+ ["ㅛ", "y"],
471
+ ["ㅜ", "n"],
472
+ ["ㅠ", "b"],
473
+ ["ㅡ", "m"],
474
+ ["ㅣ", "l"],
475
+ ].map(([character, key]) => [character.codePointAt(0)!, key as BaseKey]),
476
+ );
477
+
478
+ function matchesKoreanDubeolsikKittySequence(data: string, keyId: KeyId): boolean {
479
+ const event = parseKittySequence(data);
480
+ if (!event || event.eventType === 3 || event.baseLayoutKey !== undefined) return false;
481
+
482
+ const baseKey = KOREAN_DUBEOLSIK_BASE_KEYS.get(event.codepoint);
483
+ if (!baseKey) return false;
484
+
485
+ const expected = parseKeyId(keyId);
486
+ if (
487
+ !expected?.modifiers.some(modifier => modifier === "alt" || modifier === "ctrl" || modifier === "super") ||
488
+ expected.baseKey !== baseKey
489
+ )
490
+ return false;
491
+
492
+ let expectedModifier = 0;
493
+ for (const modifier of expected.modifiers) {
494
+ switch (modifier) {
495
+ case "shift":
496
+ expectedModifier |= KITTY_MOD_SHIFT;
497
+ break;
498
+ case "alt":
499
+ expectedModifier |= KITTY_MOD_ALT;
500
+ break;
501
+ case "ctrl":
502
+ expectedModifier |= KITTY_MOD_CTRL;
503
+ break;
504
+ case "super":
505
+ expectedModifier |= KITTY_MOD_SUPER;
506
+ break;
507
+ }
508
+ }
509
+ return (event.modifier & ~KITTY_LOCK_MASK) === expectedModifier;
510
+ }
440
511
  const MODIFY_OTHER_KEYS_PATTERN = /^\x1b\[27;(\d+);(\d+)~$/;
441
512
  const KITTY_KEYPAD_OPERATOR_TEXT: Record<number, string> = {
442
513
  57410: "/",
@@ -517,18 +588,37 @@ function hasControlChars(data: string): boolean {
517
588
  });
518
589
  }
519
590
 
591
+ function isValidProtocolNumber(value: number): boolean {
592
+ return Number.isSafeInteger(value) && value >= 0 && value <= 0xffffffff;
593
+ }
594
+
595
+ function isPrintableCodePoint(value: number): boolean {
596
+ return (
597
+ Number.isSafeInteger(value) &&
598
+ value >= 32 &&
599
+ value <= 0x10ffff &&
600
+ value !== 0x7f &&
601
+ !(value >= 0x80 && value <= 0x9f) &&
602
+ !(value >= 0xd800 && value <= 0xdfff)
603
+ );
604
+ }
605
+
520
606
  function decodeKittyPrintable(data: string): string | undefined {
521
607
  const match = data.match(KITTY_CSI_U_PATTERN);
522
608
  if (!match) return undefined;
523
609
 
524
610
  const codepoint = Number.parseInt(match[1] ?? "", 10);
525
- if (!Number.isFinite(codepoint)) return undefined;
611
+ if (!isValidProtocolNumber(codepoint)) return undefined;
526
612
 
527
- if (match[5] === "3") return undefined;
613
+ const eventTypeText = match[5];
614
+ const eventType =
615
+ eventTypeText === undefined || eventTypeText.length === 0 ? undefined : Number.parseInt(eventTypeText, 10);
616
+ if (eventType !== undefined && eventType !== 1 && eventType !== 2) return undefined;
528
617
 
529
618
  const shiftedKey = match[2] && match[2].length > 0 ? Number.parseInt(match[2], 10) : undefined;
530
619
  const modValue = match[4] ? Number.parseInt(match[4], 10) : 1;
531
- const modifier = Number.isFinite(modValue) ? modValue - 1 : 0;
620
+ if (!isValidProtocolNumber(modValue) || modValue < 1) return undefined;
621
+ const modifier = modValue - 1;
532
622
  const effectiveMod = modifier & ~KITTY_LOCK_MASK;
533
623
  const supportedModifierMask = KITTY_MOD_SHIFT | KITTY_MOD_ALT | KITTY_MOD_CTRL | KITTY_MOD_SUPER;
534
624
 
@@ -541,7 +631,7 @@ function decodeKittyPrintable(data: string): string | undefined {
541
631
  .split(":")
542
632
  .filter(Boolean)
543
633
  .map(value => Number.parseInt(value, 10))
544
- .filter(value => Number.isFinite(value) && value >= 32);
634
+ .filter(value => isPrintableCodePoint(value));
545
635
  if (codepoints.length > 0) {
546
636
  try {
547
637
  return String.fromCodePoint(...codepoints);
@@ -549,6 +639,7 @@ function decodeKittyPrintable(data: string): string | undefined {
549
639
  return undefined;
550
640
  }
551
641
  }
642
+ return undefined;
552
643
  }
553
644
  const keypadOperatorText = KITTY_KEYPAD_OPERATOR_TEXT[codepoint];
554
645
  if (keypadOperatorText) return keypadOperatorText;
@@ -567,7 +658,7 @@ function decodeKittyPrintable(data: string): string | undefined {
567
658
  return undefined;
568
659
  }
569
660
 
570
- if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) return undefined;
661
+ if (!isPrintableCodePoint(effectiveCodepoint)) return undefined;
571
662
 
572
663
  try {
573
664
  return String.fromCodePoint(effectiveCodepoint);
@@ -584,7 +675,7 @@ function decodeKittyPrintable(data: string): string | undefined {
584
675
  */
585
676
  export function extractPrintableText(data: string): string | undefined {
586
677
  const printable = decodePrintableKey(data);
587
- if (printable !== undefined) return printable;
678
+ if (printable !== undefined && !hasControlChars(printable)) return printable;
588
679
  if (data.length === 0 || hasControlChars(data)) return undefined;
589
680
  return data;
590
681
  }
@@ -603,7 +694,7 @@ function parseModifyOtherKeysSequence(data: string): ParsedModifyOtherKeysSequen
603
694
  if (!match) return null;
604
695
  const modValue = Number.parseInt(match[1] ?? "", 10);
605
696
  const codepoint = Number.parseInt(match[2] ?? "", 10);
606
- if (!Number.isFinite(modValue) || !Number.isFinite(codepoint)) return null;
697
+ if (!isValidProtocolNumber(modValue) || modValue < 1 || !isValidProtocolNumber(codepoint)) return null;
607
698
  return { codepoint, modifier: modValue - 1 };
608
699
  }
609
700
 
@@ -618,7 +709,7 @@ function decodeModifyOtherKeysPrintable(data: string): string | undefined {
618
709
  if (!parsed) return undefined;
619
710
  const modifier = parsed.modifier & ~KITTY_LOCK_MASK;
620
711
  if ((modifier & ~KITTY_MOD_SHIFT) !== 0) return undefined;
621
- if (!Number.isFinite(parsed.codepoint) || parsed.codepoint < 32) return undefined;
712
+ if (!isPrintableCodePoint(parsed.codepoint)) return undefined;
622
713
  try {
623
714
  return String.fromCodePoint(parsed.codepoint);
624
715
  } catch {
@@ -653,7 +744,10 @@ export function decodePrintableKey(data: string): string | undefined {
653
744
  * @param keyId - Key identifier (e.g., "ctrl+c", "escape", Key.ctrl("c"))
654
745
  */
655
746
  export function matchesKey(data: string, keyId: KeyId): boolean {
656
- return nativeKeys().matchesKey(data, keyId, kittyProtocolActive);
747
+ return (
748
+ nativeKeys().matchesKey(data, keyId, kittyProtocolActive) ||
749
+ (kittyProtocolActive && matchesKoreanDubeolsikKittySequence(data, keyId))
750
+ );
657
751
  }
658
752
 
659
753
  /**