Record shortcuts using the keyboard layout character (#6652)

* fix(app): record shortcuts using the keyboard layout character

Shortcut capture stored the QWERTY position while dispatch matched the logical character, so a Dvorak B became Cmd+N. Use the existing logical key mappings for capture and preserve code fallback for named and rewritten Option keys.

* fix(app): keep named shortcut keys on their supported codes

Character capture must not advertise unsupported named keys. Restrict the logical lookup to printable mappings so keypad Enter keeps its existing unsupported behavior.

---------

Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
paseo-bot[bot]
2026-10-12 01:58:11 +02:00
committed by GitHub
parent 166aabd57b
commit 40f0393064
3 changed files with 69 additions and 1 deletions
@@ -1,5 +1,6 @@
import { describe, expect, it, vi } from "vitest";
import { formatShortcut } from "@/utils/format-shortcut";
import { keyboardEventToComboString } from "./shortcut-string";
import {
buildKeyboardShortcutHelpSections,
buildEffectiveBindings,
@@ -1026,6 +1027,25 @@ describe("unassigned shortcuts", () => {
});
describe("matching", () => {
it("dispatches a captured Dvorak binding on the same logical key", () => {
const event = keyboardInput({ key: "b", code: "KeyN", metaKey: true });
const combo = keyboardEventToComboString(event as KeyboardEvent);
expect(combo).toBe("Cmd+B");
const bindings = buildEffectiveBindings({ "sidebar-toggle-left-mac-cmd-b": combo! });
expect(
resolveShortcut({ event, context: { isMac: true, isDesktop: true }, bindings }).match
?.action,
).toBe("sidebar.toggle.left");
expect(
resolveShortcut({
event: { key: "x", code: "KeyB", metaKey: true },
context: { isMac: true, isDesktop: true },
bindings,
}).match,
).toBeNull();
});
it("stops matching a shortcut the user unassigned", () => {
const bindings = buildEffectiveBindings({ [TAB_NEW_BINDING]: UNASSIGNED_COMBO });
@@ -22,6 +22,45 @@ function keyboardEvent(overrides: Partial<KeyboardEvent>): KeyboardEvent {
}
describe("keyboardEventToComboString", () => {
it("captures the Dvorak character rather than its QWERTY position", () => {
expect(
keyboardEventToComboString(keyboardEvent({ key: "b", code: "KeyN", metaKey: true })),
).toBe("Cmd+B");
});
it("captures shifted Dvorak letters and punctuation at other physical positions", () => {
expect(
keyboardEventToComboString(
keyboardEvent({ key: "T", code: "KeyK", metaKey: true, shiftKey: true }),
),
).toBe("Shift+Cmd+T");
expect(
keyboardEventToComboString(keyboardEvent({ key: ";", code: "KeyZ", ctrlKey: true })),
).toBe("Ctrl+;");
expect(
keyboardEventToComboString(
keyboardEvent({ key: "?", code: "BracketLeft", ctrlKey: true, shiftKey: true }),
),
).toBe("Ctrl+Shift+/");
});
it("keeps named keys and rewritten Option characters capturable", () => {
expect(keyboardEventToComboString(keyboardEvent({ key: "ArrowLeft", code: "ArrowLeft" }))).toBe(
"ArrowLeft",
);
expect(
keyboardEventToComboString(keyboardEvent({ key: "†", code: "KeyT", altKey: true })),
).toBe("Alt+T");
});
it("does not record a named key that dispatch does not support", () => {
expect(
keyboardEventToComboString(
keyboardEvent({ key: "Enter", code: "NumpadEnter", ctrlKey: true }),
),
).toBeNull();
});
it("captures the minus key (regression: previously returned null)", () => {
expect(keyboardEventToComboString(keyboardEvent({ key: "-", code: "Minus" }))).toBe("-");
});
+10 -1
View File
@@ -266,7 +266,16 @@ export function keyboardEventToComboString(event: KeyboardEvent): string | null
if (event.shiftKey) parts.push("Shift");
if (event.metaKey) parts.push("Cmd");
const humanKey = CODE_TO_KEY[event.code];
// Capture the same logical character that dispatch matches. Keep the code
// fallback for named keys and Option combinations whose key is rewritten.
const eventKey = event.key.toLowerCase();
const logicalKey = Object.entries(KEY_MAP).find(
([, mapping]) =>
mapping.key?.length === 1 &&
(mapping.key.toLowerCase() === eventKey ||
(event.shiftKey && mapping.shiftedKey === event.key)),
)?.[0];
const humanKey = logicalKey ?? CODE_TO_KEY[event.code];
if (humanKey) {
parts.push(humanKey);
} else {