From 5bb3700717159ab386be245cf6bb6ab3935209d8 Mon Sep 17 00:00:00 2001 From: Ryota Date: Fri, 30 Jan 2026 13:20:35 +0000 Subject: [PATCH] fix(tui): prevent Kitty protocol base layout key from causing false shortcut matches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the Kitty keyboard protocol (flag 4), terminals report both the logical key codepoint and the physical base layout key (PC-101 QWERTY position). The key matching logic in matchesKittySequence matched against both unconditionally, which caused a single keypress to match multiple shortcuts on remapped keyboard layouts. For example, on a Dvorak layout with xremap, pressing Ctrl+K sends CSI 107::118;5u (codepoint=107 'k', base layout=118 'v'). The unconditional base layout match made this also match Ctrl+V, which is bound to pasteImage. Since CustomEditor checks app-level keybindings (pasteImage) before editor keybindings (deleteToLineEnd), Ctrl+K was silently intercepted by the paste image handler instead of deleting to end of line. The same issue affects symbol keys, as Dvorak also remaps symbols to different physical positions (e.g., '/' sits where '[' is on QWERTY). The fix restricts base layout key matching to cases where the codepoint is not a recognized Latin letter (a-z) or symbol (/, -, [, ;, etc.). When the codepoint is already a recognized key, it is authoritative and the base layout key is ignored. This preserves non-Latin layout support (Ctrl+К on a Russian layout still matches Ctrl+K via base layout key 107) while preventing false matches from differing physical key positions on remapped layouts. Both matchesKittySequence and parseKey are updated with the same logic. --- packages/tui/src/keys.ts | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/tui/src/keys.ts b/packages/tui/src/keys.ts index 6d4d14e1..aba7a5da 100644 --- a/packages/tui/src/keys.ts +++ b/packages/tui/src/keys.ts @@ -615,10 +615,24 @@ function matchesKittySequence(data: string, expectedCodepoint: number, expectedM // Primary match: codepoint matches directly if (parsed.codepoint === expectedCodepoint) return true; - // Alternate match: use base layout key for non-Latin keyboard layouts + // Alternate match: use base layout key for non-Latin keyboard layouts. // This allows Ctrl+С (Cyrillic) to match Ctrl+c (Latin) when terminal reports - // the base layout key (the key in standard PC-101 layout) - if (parsed.baseLayoutKey !== undefined && parsed.baseLayoutKey === expectedCodepoint) return true; + // the base layout key (the key in standard PC-101 layout). + // + // Only fall back to base layout key when the codepoint is NOT already a + // recognized Latin letter (a-z) or symbol (e.g., /, -, [, ;, etc.). + // When the codepoint is a recognized key, it is authoritative regardless + // of physical key position. This prevents remapped layouts (Dvorak, Colemak, + // xremap, etc.) from causing false matches: both letters and symbols move + // to different physical positions, so Ctrl+K could falsely match Ctrl+V + // (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 isLatinLetter = cp >= 97 && cp <= 122; // a-z + const isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(cp)); + if (!isLatinLetter && !isKnownSymbol) return true; + } return false; } @@ -1038,9 +1052,14 @@ export function parseKey(data: string): string | undefined { if (effectiveMod & MODIFIERS.ctrl) mods.push("ctrl"); if (effectiveMod & MODIFIERS.alt) mods.push("alt"); - // Prefer base layout key for consistent shortcut naming across keyboard layouts - // This ensures Ctrl+С (Cyrillic) is reported as "ctrl+c" (Latin) - const effectiveCodepoint = baseLayoutKey ?? codepoint; + // Use base layout key only when codepoint is not a recognized Latin + // letter (a-z) 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 isKnownSymbol = SYMBOL_KEYS.has(String.fromCharCode(codepoint)); + const effectiveCodepoint = isLatinLetter || isKnownSymbol ? codepoint : (baseLayoutKey ?? codepoint); let keyName: string | undefined; if (effectiveCodepoint === CODEPOINTS.escape) keyName = "escape";