diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts index d68e731e56..2a7960c9b9 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts @@ -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 }); diff --git a/packages/app/src/keyboard/shortcut-string.test.ts b/packages/app/src/keyboard/shortcut-string.test.ts index e7cffee597..8d48d5d986 100644 --- a/packages/app/src/keyboard/shortcut-string.test.ts +++ b/packages/app/src/keyboard/shortcut-string.test.ts @@ -22,6 +22,45 @@ function keyboardEvent(overrides: Partial): 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("-"); }); diff --git a/packages/app/src/keyboard/shortcut-string.ts b/packages/app/src/keyboard/shortcut-string.ts index 339d623954..8b632befbd 100644 --- a/packages/app/src/keyboard/shortcut-string.ts +++ b/packages/app/src/keyboard/shortcut-string.ts @@ -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 {