From caf2ca1438c1a413ee978c97a41ce4e9f818fa9f Mon Sep 17 00:00:00 2001 From: Tommaso Sciortino Date: Fri, 31 Oct 2025 16:19:55 -0700 Subject: [PATCH] Add kitty support for function keys. (#12415) --- .../src/ui/contexts/KeypressContext.test.tsx | 4 + .../cli/src/ui/contexts/KeypressContext.tsx | 90 +++++++++---------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx index 24cc88b0b5..e4605e3f83 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx @@ -469,6 +469,10 @@ describe('KeypressContext - Kitty Protocol', () => { { sequence: `\x1b[1~`, expected: { name: 'home' } }, { sequence: `\x1b[4~`, expected: { name: 'end' } }, { sequence: `\x1b[2~`, expected: { name: 'insert' } }, + { sequence: `\x1b[11~`, expected: { name: 'f1' } }, + { sequence: `\x1b[17~`, expected: { name: 'f6' } }, + { sequence: `\x1b[23~`, expected: { name: 'f11' } }, + { sequence: `\x1b[24~`, expected: { name: 'f12' } }, // Reverse tabs { sequence: `\x1b[Z`, expected: { name: 'tab', shift: true } }, { sequence: `\x1b[1;2Z`, expected: { name: 'tab', shift: true } }, diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index ebe71a0238..e347d370b6 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -56,6 +56,48 @@ const MAC_ALT_KEY_CHARACTER_MAP: Record = { '\u00B5': 'm', // "ยต" toggle markup view }; +/** + * Maps symbols from parameterized functional keys `\x1b[1;1` + * to their corresponding key names (e.g., 'up', 'f1'). + */ +const LEGACY_FUNC_TO_NAME: { [k: string]: string } = { + A: 'up', + B: 'down', + C: 'right', + D: 'left', + H: 'home', + F: 'end', + P: 'f1', + Q: 'f2', + R: 'f3', + S: 'f4', +}; + +/** + * Maps key codes from tilde-coded functional keys `\x1b[~` + * to their corresponding key names. + */ +const TILDE_KEYCODE_TO_NAME: Record = { + 1: 'home', + 2: 'insert', + 3: 'delete', + 4: 'end', + 5: 'pageup', + 6: 'pagedown', + 11: 'f1', + 12: 'f2', + 13: 'f3', + 14: 'f4', + 15: 'f5', + 17: 'f6', // skipping 16 is intentional + 18: 'f7', + 19: 'f8', + 20: 'f9', + 21: 'f10', + 23: 'f11', // skipping 22 is intentional + 24: 'f12', +}; + /** * Check if a buffer could potentially be a valid kitty sequence or its prefix. */ @@ -169,19 +211,7 @@ function parseKittyPrefix(buffer: string): { key: Key; length: number } | null { const alt = (bits & MODIFIER_ALT_BIT) === MODIFIER_ALT_BIT; const ctrl = (bits & MODIFIER_CTRL_BIT) === MODIFIER_CTRL_BIT; const sym = m[2]; - const symbolToName: { [k: string]: string } = { - A: 'up', - B: 'down', - C: 'right', - D: 'left', - H: 'home', - F: 'end', - P: 'f1', - Q: 'f2', - R: 'f3', - S: 'f4', - }; - const name = symbolToName[sym] || ''; + const name = LEGACY_FUNC_TO_NAME[sym] || ''; if (!name) return null; return { key: { @@ -216,29 +246,7 @@ function parseKittyPrefix(buffer: string): { key: Key; length: number } | null { // Tilde-coded functional keys (Delete, Insert, PageUp/Down, Home/End) if (terminator === '~') { - let name: string | null = null; - switch (keyCode) { - case 1: - name = 'home'; - break; - case 2: - name = 'insert'; - break; - case 3: - name = 'delete'; - break; - case 4: - name = 'end'; - break; - case 5: - name = 'pageup'; - break; - case 6: - name = 'pagedown'; - break; - default: - break; - } + const name = TILDE_KEYCODE_TO_NAME[keyCode]; if (name) { return { key: { @@ -307,15 +315,7 @@ function parseKittyPrefix(buffer: string): { key: Key; length: number } | null { m = buffer.match(legacyFuncKey); if (m) { const sym = m[1]; - const nameMap: { [key: string]: string } = { - A: 'up', - B: 'down', - C: 'right', - D: 'left', - H: 'home', - F: 'end', - }; - const name = nameMap[sym]!; + const name = LEGACY_FUNC_TO_NAME[sym]!; return { key: { name,