From 1a20a6e53197438f7ca48f9461da3f204b5bb927 Mon Sep 17 00:00:00 2001 From: liziye627-design <232619260+liziye627-design@users.noreply.github.com> Date: Sun, 25 Oct 2026 01:39:42 +0800 Subject: [PATCH] fix(keys): treat keypad navigation keys as their main-block equivalents Fixes #1223 --- .changeset/keypad-navigation-aliases.md | 5 +++ packages/hunk/src/extension-api/keys.ts | 25 ++++++++++++- packages/hunk/src/lib/commandKeys.test.ts | 45 +++++++++++++++++++++++ 3 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 .changeset/keypad-navigation-aliases.md diff --git a/.changeset/keypad-navigation-aliases.md b/.changeset/keypad-navigation-aliases.md new file mode 100644 index 000000000..5c19a2551 --- /dev/null +++ b/.changeset/keypad-navigation-aliases.md @@ -0,0 +1,5 @@ +--- +"hunkdiff": patch +--- + +Numpad navigation keys such as PgUp and PgDn now trigger the same shortcuts as their main-keyboard counterparts. diff --git a/packages/hunk/src/extension-api/keys.ts b/packages/hunk/src/extension-api/keys.ts index 10afa208c..b5df2c2d7 100644 --- a/packages/hunk/src/extension-api/keys.ts +++ b/packages/hunk/src/extension-api/keys.ts @@ -175,9 +175,32 @@ function matchesControlCharacter(parsed: ParsedKeyChord, key: ExtensionKeyEvent) return key.sequence === String.fromCharCode(parsed.base.charCodeAt(0) - 0x60); } +/** + * Keypad keys OpenTUI decodes separately, mapped to the main-block key they act as. + * + * Mirrors the navigation entries of OpenTUI's `defaultKeyAliases`, which is not + * part of its public API and could not be imported here anyway: this module is + * the import-free published contract. Under the Kitty keyboard protocol a + * numpad PageUp arrives as `kppageup`, so without this a `pageup` binding never + * fires from the keypad. + */ +const KEYPAD_ALIASES: Readonly> = { + kpleft: "left", + kpright: "right", + kpup: "up", + kpdown: "down", + kppageup: "pageup", + kppagedown: "pagedown", + kphome: "home", + kpend: "end", + kpinsert: "insert", + kpdelete: "delete", +}; + /** Report whether one key event names a key, allowing for terminal naming disagreements. */ function matchesNamedKey(base: string, key: ExtensionKeyEvent) { - const name = key.name?.toLowerCase(); + const rawName = key.name?.toLowerCase(); + const name = (rawName && KEYPAD_ALIASES[rawName]) ?? rawName; if (name === base) { return true; } diff --git a/packages/hunk/src/lib/commandKeys.test.ts b/packages/hunk/src/lib/commandKeys.test.ts index 018658309..6a7818b05 100644 --- a/packages/hunk/src/lib/commandKeys.test.ts +++ b/packages/hunk/src/lib/commandKeys.test.ts @@ -82,3 +82,48 @@ describe("toKeyChordList", () => { expect(toKeyChordList(["y", "ctrl+g"])).toEqual(["y", "ctrl+g"]); }); }); + +describe("keypad navigation keys", () => { + test("numpad PageUp/PageDown decoded by OpenTUI match pageup/pagedown chords", () => { + // Kitty keyboard protocol reports numpad PageUp/PageDown as their own keys (57421/57422). + const pageUp = parseKeypress("\x1b[57421u", { useKittyKeyboard: true }); + const pageDown = parseKeypress("\x1b[57422u", { useKittyKeyboard: true }); + expect(pageUp?.name).toBe("kppageup"); + expect(pageDown?.name).toBe("kppagedown"); + + expect(matchesKeyChord(parsed("pageup"), new KeyEvent(pageUp!))).toBe(true); + expect(matchesKeyChord(parsed("pagedown"), new KeyEvent(pageDown!))).toBe(true); + expect(matchesKeyChord(parsed("pageup"), new KeyEvent(pageDown!))).toBe(false); + }); + + test("every keypad navigation key OpenTUI decodes matches its main-block chord", () => { + // Kitty functional-key codes for the keypad navigation cluster, and the + // main-block key each one is an alias of in OpenTUI's own key handling. + const keypadKeys: Array<[code: number, decoded: string, chord: string]> = [ + [57417, "kpleft", "left"], + [57418, "kpright", "right"], + [57419, "kpup", "up"], + [57420, "kpdown", "down"], + [57421, "kppageup", "pageup"], + [57422, "kppagedown", "pagedown"], + [57423, "kphome", "home"], + [57424, "kpend", "end"], + [57425, "kpinsert", "insert"], + [57426, "kpdelete", "delete"], + ]; + + for (const [code, decoded, chord] of keypadKeys) { + const event = parseKeypress(`\x1b[${code}u`, { useKittyKeyboard: true }); + expect(event?.name).toBe(decoded); + expect({ decoded, matches: matchesKeyChord(parsed(chord), new KeyEvent(event!)) }).toEqual({ + decoded, + matches: true, + }); + } + }); + + test("keypad aliases keep modifier matching exact", () => { + const shiftPageDown = parseKeypress("\x1b[57422;2u", { useKittyKeyboard: true }); + expect(matchesKeyChord(parsed("pagedown"), new KeyEvent(shiftPageDown!))).toBe(false); + }); +});