Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/keypad-navigation-aliases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Numpad navigation keys such as PgUp and PgDn now trigger the same shortcuts as their main-keyboard counterparts.
25 changes: 24 additions & 1 deletion packages/hunk/src/extension-api/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, string>> = {
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;
}
Expand Down
45 changes: 45 additions & 0 deletions packages/hunk/src/lib/commandKeys.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});