feat(app): shortcuts show your keyboard's keys, ⌘Å and ⌘¨ for Back and Forward on Norwegian

ST
20h ago
+733-41

Summary

Back and Forward (⌘[ / ⌘]) are matched by the key's place (e.code), so on a layout where [ and ] need ⌥ they are ⌘ and the keys right of P. The UI never said so. Now every place that shows a shortcut's keys shows the key in that place on the layout in use: ⌘Å / ⌘¨ on Norwegian, ⌘Ü / ⌘+ on German, still ⌘[ / ⌘] on US. No key binding changes.

shortcuts.json   back: keys "⌘[", code "BracketLeft"   (code = matched by place)
lib/key-layout.ts        layoutKeys("⌘[", "BracketLeft", {BracketLeft: "Å"}) → "⌘Å"   (pure, tested)
lib/keyboard-layout.ts   labels ← navigator.keyboard.getLayoutMap()       (Chromium)
                                ← invoke("key_layout")                   (Mac app: keys.rs, UCKeyTranslate on the input source in use)
                                ← a bare key pressed in one of the places
                         asked again on focus, on layoutchange, and as a shortcut list opens
lib/shortcuts.ts         keysOf / keysFor apply it → tooltip, sheet, Settings → Shortcuts, ⌘K, the ⌘⌥ menu hints

Unknown layout → a US keyboard's keys, as before. The ⌘⌥ letter shortcuts (⌘⌥D/C/S/I) go by place too, so they follow the layout as well (⌘⌥E on Dvorak).

Also:

  • Shorter descriptions. Each shortcut's description is one short clause that fits its line in the sheet (17 were cut off with "…"). The details they held (mouse buttons, editor outdent, ↵/⌥↵/⌘↵ in Go to file, …) are in a new guide, docs/guides/keyboard-shortcuts.mdx. Settings → Shortcuts also searches by the menu name and by the keys either way (⌘å and ⌘[ both find Back).
  • Tooltips on Back/Forward show on hover. Tip wraps a disabled child in a span; when the arrow became enabled the trigger element changed and hover never opened it (keyboard focus did). Tip now remounts its tooltip when disabled flips.
  • Verify skill: drive.mjs takes a scenario init (an init script, here a stubbed layout); new keyboard-layout and keyboard-layout-us scenarios; navigation-history.md has history-layout.

Evidence

Driven with the verify skill (keyboard-layout, keyboard-layout-us): the production build on the mock fixtures in Chromium, with getLayoutMap() stubbed, no console errors.

Before (origin/main): hover Back, no tooltipAfter, Norwegian: "Back (⌘Å)"After, US: "Back (⌘[)"
before hoverBack ⌘Å![Back ⌘
Before: the sheet, 17 descriptions cut offAfter, Norwegian: ⌘Å / ⌘¨, none cut offAfter, US
before sheetafter sheetafter sheet US
Forward (⌘¨)⌘K "back"Settings: "⌘å" finds BackSettings: "go to file" by its menu nameLinux app keys, none cut off
fwdpalettesearch keyssearch labellinux
- open http://localhost:1435/?mock=1
- Norwegian layout, ⌘J to the dashboard: Back's tooltip reads "Back (⌘Å)"
- the keys are as they were: ⌘ and the key right of P goes back, and Forward's tooltip reads its key
- ⌘/ opens Keyboard shortcuts: Back is ⌘Å, Forward ⌘¨, and no description is cut off
-   39 descriptions, none cut off; the longest: "Open a sidebar row's or a tab's menu"
- Search them in Settings: "⌘å" finds Back, and "go to file" finds Go to file… by its menu name
- ⌘K, "back": its Back item shows ⌘Å
OK

The Mac side, on a real Mac (macOS 27, the Mac mini): cargo test --lib passes all 7, including the new keys::tests::keys_in_their_place_on_each_layout, which reads the installed layouts through the same UCKeyTranslate path without switching to them:

US         BracketLeft "["  BracketRight "]"  KeyD "d"
Norwegian  BracketLeft "å"  BracketRight "¨"  KeyD "d"
German     BracketLeft "ü"  BracketRight "+"  KeyD "d"
Dvorak     BracketLeft "/"  BracketRight "="  KeyD "e"

Checks:

  • pnpm typecheck:plugins, check:titles, check:type, check:grid, check:csp and check:themes pass. So do pnpm test (580, with the new key-layout.test.ts and a 40-character cap on descriptions), pnpm build, npx tsc -p e2e and (cd docs-site && pnpm build).
  • In the browser, e2e/back-forward.spec.ts passes, with a new Norwegian test. That test switches to US with the window focused the whole time and checks the sheet follows. keyboard.spec.ts, compare.spec.ts and navigation-history pass too.
  • Three tests already fail on origin/main and fail the same way here: keyboard.spec.ts:173 and two in sidebar-fleet.spec.ts. navigation-history failed once on a 5px dashboard scroll difference, then passed 3 more times.
  • Linux cargo check couldn't run on the devbox (no GTK dev libraries). keys.rs compiles for Linux on its own with -D warnings, and its non-macOS path returns an empty map.

Review: Codex review and adversarial review found two things, both fixed:

  • ⌘K didn't follow layout changes.
  • Switching input source while the window stayed focused left the old labels.

A simplify pass replaced the hard-coded ⌘⌥D/C/S hints with keysFor. It also added a test tying each table code to the key handler, and limits the native layout call to the Mac app.

A person checks the Mac app (the mock stubs the layout):

  1. On a Norwegian input source, hover the ← arrow after moving somewhere: it reads "Back (⌘Å)", and → reads "Forward (⌘¨)".
  2. ⌘/: the sheet shows ⌘Å and ⌘¨.
  3. Go menu: macOS 12+ localizes menu key equivalents for the layout (NSMenuItem.allowsAutomaticKeyEquivalentLocalization, on by default, and muda leaves it on). The items still hand muda CmdOrCtrl+[, so what it reads (⌘Å or ⌘[) is up to macOS. muda can't take a layout key like Å as an accelerator.
  4. ⌘Å goes back and ⌘¨ forward. Then switch the input source to ABC/US and reopen ⌘/: it reads ⌘[ / ⌘].

Merge Danger

Door: two-way

Display, copy and docs. No key bindings, no stored data. Reverting the PR undoes it.

Blast Radius: small

  • What a shortcut list shows on non-US layouts. On German, Forward reads ⌘+, the same as Zoom in. That's accurate: the place-matched handler already takes that key for Forward, a pre-existing overlap this PR only makes visible.
  • Tip remounts its tooltip when its child's disabled flips, so a tooltip open at that moment closes.
  • A new Tauri command, key_layout (read-only, sync, main thread).

🤖 Generated with Claude Code