diff --git a/packages/app/src/keyboard/keyboard-shortcuts.test.ts b/packages/app/src/keyboard/keyboard-shortcuts.test.ts index 9fa7a3444..01cc7e2d5 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.test.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.test.ts @@ -272,6 +272,12 @@ describe("keyboard-shortcuts", () => { context: { isMac: false }, action: "sidebar.toggle.both", }, + { + name: "matches Dvorak logical Cmd+. to toggle both sidebars on macOS", + event: { key: ".", code: "KeyE", metaKey: true }, + context: { isMac: true }, + action: "sidebar.toggle.both", + }, { name: "routes Mod+D to message-input action outside terminal", event: { key: "d", code: "KeyD", metaKey: true }, @@ -390,6 +396,11 @@ describe("keyboard-shortcuts", () => { event: { key: " ", code: "Space" }, context: { focusScope: "message-input" }, }, + { + name: "keeps Dvorak Cmd+V available for paste in message input", + event: { key: "v", code: "Period", metaKey: true }, + context: { isMac: true, isDesktop: true, focusScope: "message-input" }, + }, ]; it.each(nonMatchingCases)("$name", ({ event, context }) => { diff --git a/packages/app/src/keyboard/keyboard-shortcuts.ts b/packages/app/src/keyboard/keyboard-shortcuts.ts index 96e33b9bc..26a86a272 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.ts @@ -1041,9 +1041,17 @@ function matchesCombo(combo: KeyCombo, event: KeyboardEvent, isMac: boolean): bo return parseDigit(event) !== null; } + if (combo.key !== undefined) { + const eventKey = event.key.toLowerCase(); + if (eventKey === combo.key) return true; + if (combo.shift === true && combo.shiftedKey !== undefined && eventKey === combo.shiftedKey) { + return true; + } + return combo.codeFallback === true && event.code === combo.code; + } + const codeMatch = event.code === combo.code; - const keyMatch = combo.key !== undefined && event.key.toLowerCase() === combo.key.toLowerCase(); - return codeMatch || keyMatch; + return codeMatch; } function matchesWhen(when: ShortcutWhen | undefined, context: KeyboardShortcutContext): boolean { diff --git a/packages/app/src/keyboard/shortcut-string.ts b/packages/app/src/keyboard/shortcut-string.ts index bef1bbf39..c7448a74e 100644 --- a/packages/app/src/keyboard/shortcut-string.ts +++ b/packages/app/src/keyboard/shortcut-string.ts @@ -3,6 +3,8 @@ import type { ShortcutKey } from "@/utils/format-shortcut"; export interface KeyCombo { code: string; key?: string; + shiftedKey?: string; + codeFallback?: boolean; meta?: true; ctrl?: true; alt?: true; @@ -14,6 +16,8 @@ export interface KeyCombo { interface KeyMapping { code: string; key?: string; + shiftedKey?: string; + codeFallback?: boolean; } const KEY_MAP: Record = {}; @@ -26,18 +30,28 @@ for (let i = 0; i < 26; i++) { for (let i = 0; i <= 9; i++) { KEY_MAP[String(i)] = { code: `Digit${i}`, key: String(i) }; } +KEY_MAP["1"].shiftedKey = "!"; +KEY_MAP["2"].shiftedKey = "@"; +KEY_MAP["3"].shiftedKey = "#"; +KEY_MAP["4"].shiftedKey = "$"; +KEY_MAP["5"].shiftedKey = "%"; +KEY_MAP["6"].shiftedKey = "^"; +KEY_MAP["7"].shiftedKey = "&"; +KEY_MAP["8"].shiftedKey = "*"; +KEY_MAP["9"].shiftedKey = "("; +KEY_MAP["0"].shiftedKey = ")"; KEY_MAP["Digit"] = { code: "Digit" }; -KEY_MAP["\\"] = { code: "Backslash" }; -KEY_MAP["["] = { code: "BracketLeft", key: "[" }; -KEY_MAP["]"] = { code: "BracketRight", key: "]" }; -KEY_MAP[","] = { code: "Comma", key: "," }; -KEY_MAP["."] = { code: "Period", key: "." }; -KEY_MAP["`"] = { code: "Backquote", key: "`" }; -KEY_MAP["/"] = { code: "Slash" }; +KEY_MAP["\\"] = { code: "Backslash", key: "\\", shiftedKey: "|" }; +KEY_MAP["["] = { code: "BracketLeft", key: "[", shiftedKey: "{" }; +KEY_MAP["]"] = { code: "BracketRight", key: "]", shiftedKey: "}" }; +KEY_MAP[","] = { code: "Comma", key: ",", shiftedKey: "<" }; +KEY_MAP["."] = { code: "Period", key: ".", shiftedKey: ">" }; +KEY_MAP["`"] = { code: "Backquote", key: "`", shiftedKey: "~" }; +KEY_MAP["/"] = { code: "Slash", key: "/", shiftedKey: "?" }; KEY_MAP["?"] = { code: "Slash", key: "?" }; -KEY_MAP["Space"] = { code: "Space", key: " " }; -KEY_MAP["Enter"] = { code: "Enter", key: "Enter" }; +KEY_MAP["Space"] = { code: "Space", key: " ", codeFallback: true }; +KEY_MAP["Enter"] = { code: "Enter", key: "Enter", codeFallback: true }; KEY_MAP["Backspace"] = { code: "Backspace" }; KEY_MAP["Escape"] = { code: "Escape" }; KEY_MAP["ArrowLeft"] = { code: "ArrowLeft" }; @@ -110,6 +124,12 @@ export function parseShortcutString(s: string): KeyCombo { combo.code = mapping.code; if (mapping.key !== undefined) { combo.key = mapping.key; + if (mapping.shiftedKey !== undefined) { + combo.shiftedKey = mapping.shiftedKey; + } + if (mapping.codeFallback === true) { + combo.codeFallback = true; + } } return combo;