@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.
- package/CHANGELOG.md +5 -0
- package/package.json +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.
|
|
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.
|
|
40
|
-
"@gajae-code/utils": "0.13.
|
|
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
|
|
431
|
-
const KITTY_RELEASE_PATTERN = /^\x1b\[[\d:;]*:3[u~
|
|
432
|
-
const KITTY_REPEAT_PATTERN = /^\x1b\[[\d:;]*:2[u~
|
|
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 (!
|
|
611
|
+
if (!isValidProtocolNumber(codepoint)) return undefined;
|
|
526
612
|
|
|
527
|
-
|
|
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
|
-
|
|
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 =>
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
747
|
+
return (
|
|
748
|
+
nativeKeys().matchesKey(data, keyId, kittyProtocolActive) ||
|
|
749
|
+
(kittyProtocolActive && matchesKoreanDubeolsikKittySequence(data, keyId))
|
|
750
|
+
);
|
|
657
751
|
}
|
|
658
752
|
|
|
659
753
|
/**
|