Skip to content

fix: resolve position-invariant keys from the physical code - #1

Merged
karamouche merged 1 commit into
gladiaio:mainfrom
rmarquet21:fix/opt-space-hotkey-capture
Aug 3, 2026
Merged

fix: resolve position-invariant keys from the physical code#1
karamouche merged 1 commit into
gladiaio:mainfrom
rmarquet21:fix/opt-space-hotkey-capture

Conversation

@rmarquet21

Copy link
Copy Markdown
Contributor

Problem

Opt+Space cannot be bound as a dictation shortcut on macOS. The recorder shows followed by a blank key chip, and saving the shortcut fails to register.

macOS treats Option as a character-composing modifier, so Opt+Space is how you type a no-break space. The browser correctly reports:

code = "Space"
key  = " "    // U+00A0 NO-BREAK SPACE, not U+0020

normalizeCapturedKey read event.key first. LOGICAL_KEY_FROM_EVENT_KEY maps " " (U+0020) to "Space", so the lookup missed and execution fell through to the generic single-character branch, which returned the no-break space itself as the logical key token. The recorded shortcut was Alt+<U+00A0>.

Nothing surfaced the problem: formatKeySymbol has no mapping for U+00A0 so the key chip rendered blank, and canSaveCapture saw one modifier plus one non-modifier and enabled the Save button.

If saved, parse_combo_shortcut passed " " to resolve_logical_key, which fell through stable_logical_to_code to resolve_char_in_current_layout. That scans the active layout via UCKeyTranslate for a key producing the character unmodified or with Shift — no key produces U+00A0 without Option, by definition — so registration failed.

The same root cause affects every Alt-composed printable key, which is noted below but not addressed here.

Approach

Resolve position-invariant keys from KeyboardEvent.code before consulting KeyboardEvent.key:

const stable = STABLE_LOGICAL_FROM_CODE[code];
if (stable) return stable;
if (/^F\d{1,2}$/.test(code)) return code;

The table covers only keys whose physical position is identical on every layout: Space, Enter, Tab, Backspace, Delete, Escape, the four arrows, and F1–F12. Every token it emits is already accepted by stable_logical_to_code in hotkey_layout.rs, so these shortcuts bypass the character round-trip through the layout entirely.

Punctuation and digits deliberately stay on the event.key path. Semicolon produces m on AZERTY and Digit1 produces &, so resolving those from the physical code would store the wrong logical key and register the wrong physical key on non-QWERTY layouts. A test locks that behaviour in.

The change is behaviour-preserving for every case that already worked: bare Space, Enter, Tab, arrows and F-keys resolve to the identical token via the code path.

Testing

macOS 15.6, French layout, MacBook Pro internal keyboard.

  • npm test — 120 Rust tests, 28 frontend tests, tsc --noEmit, all passing
  • npm run build — passing
  • cargo clippy --manifest-path src-tauri/Cargo.toml --all-targets -- -D warnings — clean
  • prettier --check on both changed files — clean

Verified end to end in tauri:dev with Accessibility granted: capturing Opt+Space now logs token=Space then recorded=Alt+Space, registration logs resolved stable logical key "Space" -> physical Space, and a full dictation session ran on the bound shortcut.

New test fixtures use the events the browser actually sends (key: " ", code: "Space", altKey: true). The pre-existing fixture used a real U+0020, which is the one input this path never receives, so it passed both before and after the fix.

Not covered

Opt+<letter> remains broken for the same underlying reason. Opt+E/I/N/U report key: "Dead", so normalizeCapturedKey returns null, the keydown is dropped and the recorder stays on Alt indefinitely; Opt+A reports æ on a French layout, which no layout resolves. Fixing that needs the base character for a physical code, which the frontend cannot obtain in WKWebView (navigator.keyboard.getLayoutMap is Chromium-only). A Tauri command prefetching a code → base character map at startCapture would keep the keydown handler synchronous. Happy to open a separate issue or PR if that direction is wanted.

macOS composes Option with the pressed key, so Opt+Space reports
KeyboardEvent.key as U+00A0 rather than " ". The captured token became
the no-break space itself, producing an "Alt+<U+00A0>" shortcut that no
layout can resolve back to a physical key, so registration failed.

Space, Enter, Tab, Backspace, Delete, Escape, the arrows and F1-F12 now
come from KeyboardEvent.code, whose position is identical on every
layout. Punctuation and digits still come from KeyboardEvent.key so
shortcuts keep following the active layout.
@karamouche
karamouche self-requested a review August 3, 2026 16:22
@karamouche

Copy link
Copy Markdown
Collaborator

thanks for the contribution @rmarquet21! It looks good, approving the merge 👍

@karamouche
karamouche merged commit b2d019e into gladiaio:main Aug 3, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants