fix(tui): normalize keypad functional keys closes #2650

This commit is contained in:
Mario Zechner
2026-03-29 21:53:31 +02:00
parent 86d9220dfe
commit 8c64058809
4 changed files with 89 additions and 8 deletions

View File

@@ -325,6 +325,40 @@ const FUNCTIONAL_CODEPOINTS = {
end: -15,
} as const;
const KITTY_FUNCTIONAL_KEY_EQUIVALENTS = new Map<number, number>([
[57399, 48], // KP_0 -> 0
[57400, 49], // KP_1 -> 1
[57401, 50], // KP_2 -> 2
[57402, 51], // KP_3 -> 3
[57403, 52], // KP_4 -> 4
[57404, 53], // KP_5 -> 5
[57405, 54], // KP_6 -> 6
[57406, 55], // KP_7 -> 7
[57407, 56], // KP_8 -> 8
[57408, 57], // KP_9 -> 9
[57409, 46], // KP_DECIMAL -> .
[57410, 47], // KP_DIVIDE -> /
[57411, 42], // KP_MULTIPLY -> *
[57412, 45], // KP_SUBTRACT -> -
[57413, 43], // KP_ADD -> +
[57415, 61], // KP_EQUAL -> =
[57416, 44], // KP_SEPARATOR -> ,
[57417, ARROW_CODEPOINTS.left],
[57418, ARROW_CODEPOINTS.right],
[57419, ARROW_CODEPOINTS.up],
[57420, ARROW_CODEPOINTS.down],
[57421, FUNCTIONAL_CODEPOINTS.pageUp],
[57422, FUNCTIONAL_CODEPOINTS.pageDown],
[57423, FUNCTIONAL_CODEPOINTS.home],
[57424, FUNCTIONAL_CODEPOINTS.end],
[57425, FUNCTIONAL_CODEPOINTS.insert],
[57426, FUNCTIONAL_CODEPOINTS.delete],
]);
function normalizeKittyFunctionalCodepoint(codepoint: number): number {
return KITTY_FUNCTIONAL_KEY_EQUIVALENTS.get(codepoint) ?? codepoint;
}
const LEGACY_KEY_SEQUENCES = {
up: ["\x1b[A", "\x1bOA"],
down: ["\x1b[B", "\x1bOB"],
@@ -619,8 +653,11 @@ function matchesKittySequence(data: string, expectedCodepoint: number, expectedM
// Check if modifiers match
if (actualMod !== expectedMod) return false;
// Primary match: codepoint matches directly
if (parsed.codepoint === expectedCodepoint) return true;
const normalizedCodepoint = normalizeKittyFunctionalCodepoint(parsed.codepoint);
const normalizedExpectedCodepoint = normalizeKittyFunctionalCodepoint(expectedCodepoint);
// Primary match: codepoint matches directly after normalizing functional keys
if (normalizedCodepoint === normalizedExpectedCodepoint) return true;
// Alternate match: use base layout key for non-Latin keyboard layouts.
// This allows Ctrl+С (Cyrillic) to match Ctrl+c (Latin) when terminal reports
@@ -635,7 +672,7 @@ function matchesKittySequence(data: string, expectedCodepoint: number, expectedM
// (letter remapping) and Ctrl+/ could falsely match Ctrl+[ (symbol remapping)
// if the base layout key were always considered.
if (parsed.baseLayoutKey !== undefined && parsed.baseLayoutKey === expectedCodepoint) {
const cp = parsed.codepoint;
const cp = normalizedCodepoint;
const isLatinLetter = cp >= 97 && cp <= 122; // a-z
const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(cp));
if (!isLatinLetter && !isKnownSymbol) return true;
@@ -1148,15 +1185,18 @@ export function matchesKey(data: string, keyId: KeyId): boolean {
* @returns Key identifier string (e.g., "ctrl+c") or undefined
*/
function formatParsedKey(codepoint: number, modifier: number, baseLayoutKey?: number): string | undefined {
const normalizedCodepoint = normalizeKittyFunctionalCodepoint(codepoint);
// Use base layout key only when codepoint is not a recognized Latin
// letter (a-z), digit (0-9), or symbol (/, -, [, ;, etc.). For those,
// the codepoint is authoritative regardless of physical key position.
// This prevents remapped layouts (Dvorak, Colemak, xremap, etc.) from
// reporting the wrong key name based on the QWERTY physical position.
const isLatinLetter = codepoint >= 97 && codepoint <= 122; // a-z
const isDigit = codepoint >= 48 && codepoint <= 57; // 0-9
const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(codepoint));
const effectiveCodepoint = isLatinLetter || isDigit || isKnownSymbol ? codepoint : (baseLayoutKey ?? codepoint);
const isLatinLetter = normalizedCodepoint >= 97 && normalizedCodepoint <= 122; // a-z
const isDigit = normalizedCodepoint >= 48 && normalizedCodepoint <= 57; // 0-9
const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(normalizedCodepoint));
const effectiveCodepoint =
isLatinLetter || isDigit || isKnownSymbol ? normalizedCodepoint : (baseLayoutKey ?? normalizedCodepoint);
let keyName: string | undefined;
if (effectiveCodepoint === CODEPOINTS.escape) keyName = "escape";
@@ -1304,6 +1344,7 @@ export function decodeKittyPrintable(data: string): string | undefined {
if (modifier & MODIFIERS.shift && typeof shiftedKey === "number") {
effectiveCodepoint = shiftedKey;
}
effectiveCodepoint = normalizeKittyFunctionalCodepoint(effectiveCodepoint);
// Drop control characters or invalid codepoints.
if (!Number.isFinite(effectiveCodepoint) || effectiveCodepoint < 32) return undefined;