fix(whispering): hotkey overhaul (gating + Alt+Space default + layout-independent capture)#1774
Open
mrummuka wants to merge 5 commits into
Open
fix(whispering): hotkey overhaul (gating + Alt+Space default + layout-independent capture)#1774mrummuka wants to merge 5 commits into
mrummuka wants to merge 5 commits into
Conversation
Adds shortcuts.local.enabled (default false) and shortcuts.global.enabled (default true) as master switches. The local listener mounts only when enabled; global registrations are unregistered system-wide when disabled. Toggles are surfaced as Switches on each shortcuts settings page and take effect immediately via reactive $effects. Existing users with at least one configured local shortcut are grandfathered to local=true (detected via the prior migration marker or the presence of the legacy whispering-settings blob). Fresh installs default to local=false, which resolves the recorder-vs-local-listener interference for the common case. Refs: specs/20260518T121956-hotkey-overhaul.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the US-layout-specific defaults (Cmd+Shift+;, Cmd+Shift+', Cmd+Shift+X, Cmd+Shift+R, Cmd+Alt+Shift+D) with one cross-layout default: Alt+Space for toggleManualRecording. Tauri's plugin-global-shortcut accepts Alt and routes it to the Option key on macOS automatically; spacebar position is layout-independent. Adds migrateGlobalToggleDefaultToOptionSpace(), a one-shot migration that seeds Alt+Space when the toggle is unset (fresh install) or exactly equals the old default (existing users on stock settings). Custom user values are preserved. Other formerly-defaulted bindings (pushToTalk, cancelManualRecording, openTransformationPicker, runTransformationOnClipboard) are no longer seeded; users add what they actually want. Refs: specs/20260518T121956-hotkey-overhaul.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The recorder previously stored e.key.toLowerCase() for every pressed key, which captures the character produced by the current keyboard layout. On FI ISO the physical Semicolon-position key reports e.key === 'Ö'; that value is not in the supported-key set and was silently dropped, so FI users could not record any shortcut whose non-modifier key was a non-letter. Switch non-modifier capture to e.code-derived values via a new codeToLogicalKey() helper that maps W3C codes (Semicolon, Minus, KeyA, Digit0, Space, F1..F24, arrows, navigation, editing, numpad) to the canonical lowercase form. Same translation is applied in keydown and keyup so stored and removed keys match. Modifier keys keep using e.key because that name is already platform-canonical. The OPTION_KEY_CHARACTER_MAP fallback survives for unmapped codes; the macOS Option-dead-key recorder warning is removed because e.code bypasses dead-key behavior. Manual entry of shortcuts now goes through parseManualShortcut(), which accepts user-friendly aliases (ctrl|cmd|command|option|opt|shift|space| esc|enter|tab|...) and surfaces unrecognized tokens as a validation error toast. Previously "ctrl+cmd+-" silently dropped the modifiers because the case-sensitive convertToModifier switch only knew 'control'/'meta', and the user was left with a bare "-" accelerator and no feedback. Stop swallowing tauriRegister errors in global-shortcut-manager: the previous comment claimed the underlying plugin emitted false-positive errors for valid registrations, but the result was that real failures (OS-reserved combos, accelerator parse errors) silently produced a "shortcut saved" UX that did not bind anything. Errors now propagate to the existing toast plumbing in syncGlobalShortcutsWithSettings. DEV-gated console.debug log on keydown captures key/code/modifier state to help diagnose any layout that the e.code translation table misses. Refs: specs/20260518T121956-hotkey-overhaul.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the per-phase commit refs, what landed, post-implementation review findings with their dispositions, deliberate non-scope, the pending user verification checklist, and follow-up work that did not fit this branch. Refs: specs/20260518T121956-hotkey-overhaul.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rate The Rust workspace resolves tauri-plugin-http to 2.5.4 (constrained by the resolved tauri crate version). The JS package was declared as ^2.5.1, which bun resolved to 2.5.8. The two versions disagree on the fetch_read_body IPC payload shape: 2.5.8 sends one key name, 2.5.4 expects another. The skew surfaced at runtime as: Failed to download model invalid args 'streamChannel' for command 'fetch_read_body': command fetch_read_body missing required key streamChannel Tried bumping Rust to 2.5.8 first; that cascaded tauri to 2.10.3 and broke tauri-runtime-wry 2.10.1 (trait impl + Sync constraint mismatch). Pinning the JS side down is the minimal change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this PR exists
The Whispering hotkey system had three independent bugs that made the app effectively unusable on a Finnish (ISO) keyboard on macOS Tahoe, plus one runtime bug that prevented the local Parakeet model from downloading at all. Symptoms reported during testing:
Each has a distinct root cause; this PR addresses all four. Full design rationale, decisions log, post-implementation review, and follow-up scope are in
specs/20260518T121956-hotkey-overhaul.md.Architecture: before and after
The recorder and the local listener were both bound to
windowand both fired for the same keydown. Recording any global combo also triggered any local shortcut that matched the keys being recorded.Before
After
Changes in detail
1. Subsystem toggles (Phase 1)
Two new settings, both in synced user settings (alongside
analytics.enabled):The local listener now only mounts when enabled:
syncLocalShortcutsWithSettings()early-returns when local is off.syncGlobalShortcutsWithSettings()callsdesktopRpc.globalShortcuts.unregisterAll()and returns when global is off, so flipping global off actually frees the OS hotkeys system-wide. Both sync functions re-run via reactive$effects on toggle change, so no app restart needed.UI: a
<Switch>at the top of each shortcuts settings page. When OFF, the table below is dimmed and the "Reset to defaults" button is disabled. Existing-user grandfathering uses a one-shot localStorage marker and detects pre-existing installs via the prior monolithic-to-per-key migration marker OR presence of the legacywhispering-settingsblob (handles the async race withmigrateOldSettings).2. Sensible default (Phase 2)
Drops the entire US-punctuation-default set, ships one cross-layout combo:
Rationale: Tauri's
plugin-global-shortcutacceptsAltand routes to the Option key on macOS automatically. Spacebar is layout-independent. Matches Superwhisper / Wispr Flow conventions (single default combo, users add the rest).A one-shot migration
migrateGlobalToggleDefaultToOptionSpace()seedsAlt+Spacewhen the toggle is unset (fresh install) or exactly matches the literal old default (Command+Shift+;/Control+Shift+;). Custom user values are preserved.3. Layout-independent capture via
e.code(Phase 3)The core fix for the Finnish keyboard bug. The recorder previously read
e.key.toLowerCase()for every pressed key, which is the layout-dependent character. On FI ISO, the physical Semicolon-position key reportse.key === 'Ö'. That value is not in the supported-key set and was silently dropped, so FI users could not record any shortcut whose non-modifier key was a non-letter.New helper:
Both keydown and keyup in
createPressedKeys.svelte.tstranslate non-modifier keys through this; modifier keys still usee.keybecause that name is already platform-canonical. TheOPTION_KEY_CHARACTER_MAPis now a defensive fallback for unmapped codes.Side benefit: bypasses the macOS Option dead-key recording bug (Option+E producing
e.key === 'Dead') becausee.code === 'KeyE'regardless. The recorder UI warning about Option dead keys was removed since it no longer applies.4. Honest manual entry (Phase 3)
Old behavior:
New behavior:
The recorder onsubmit now uses this and shows an error toast naming any invalid tokens:
const { keys, invalidTokens } = parseManualShortcut(manualValue); if (invalidTokens.length > 0) { rpc.notify.error({ title: 'Invalid shortcut', description: `Could not recognize: ${invalidTokens.join(', ')}. Try aliases like ctrl, cmd, option, shift, space, or a single letter / digit / punctuation key.`, }); return; }5. Surface registration errors (Phase 3)
The previous swallow was speculative ("we often get"). The cost was that real failures (OS-reserved combos like
Cmd+Space, accelerator parse errors, conflicts) produced a misleading "shortcut saved" UX that bound nothing. If a false-positive recurs in practice, we will gate per-error rather than swallow the whole class.6. Pin
@tauri-apps/plugin-httpto 2.5.4The Rust workspace resolved
tauri-plugin-httpto 2.5.4 (constrained by the resolved Tauri crate version). The JS package was declared as^2.5.1, which bun resolved to 2.5.8. The two versions disagree on thefetch_read_bodyIPC payload shape: 2.5.8 sends one key name, 2.5.4 expects another. Manifested at runtime when downloading any local transcription model (Parakeet, Moonshine, Whisper C++):Tried bumping the Rust side to 2.5.8 first; that cascaded Tauri to 2.10.3 and broke
tauri-runtime-wry 2.10.1(trait impl +Syncconstraint mismatch). Pinning the JS side down to 2.5.4 is the minimal change.Migration behavior (what existing users see)
shortcuts.local.enabled = false.shortcuts.global.enabled = true. No global shortcut bound; first launch of the global settings page lets the user record one or use the newAlt+Spacedefault after a reset.shortcuts.local.enabled = true(grandfathered). Existing local shortcuts continue working.Cmd+Shift+;orControl+Shift+;Alt+Spaceon first boot. Other formerly-defaulted global bindings (pushToTalk, etc.) untouched.No data is deleted; the local subsystem code path is still present and re-enables instantly.
Test plan
Smoke (manual)
Cmd+Shift+;auto-flipped toAlt+Spaceon first boot. Custom values preserved. PressOption+Space→ recording toggles.Cmd+Shift+<physical-Semicolon-position-key>(labeledÖon FI ISO) → stored accelerator isCommand+Shift+;→ triggering the same physical combo fires the shortcut.ctrl+shift+a→ registers. Typecmd+option+space→ registers. Typegarbage+foo→ red toast naming the invalid tokens.Cmd+Spaceon macOS → error toast appears (no silent "saved").streamChannelIPC error.Automated
bun run typecheckclean forapps/whisperingon every commit. 21 pre-existing errors in unrelated files (transformations, migrations) remain; zero new errors or warnings introduced.bun tauri buildsucceeds end-to-end on macOS aarch64 (unsigned, used for local verification).Risk assessment
e.codetranslator misses a key on a layout we did not testconsole.debuglog emits{key, code, modifiers}on every keydown. Falls back toe.keywhencodeToLogicalKeyreturns null.tauriRegistererrors floods users with toastsCommits
Each phase is one commit and one logical unit; any can be reverted independently if needed.
Out of scope (follow-ups noted in the spec)
console.debugdiagnostic increatePressedKeys.svelte.tsonce layout coverage is confirmed.e.codetranslation tolocal-shortcut-manager.ts(still uses rawe.key; only matters for users who re-enable local).OPTION_DEAD_KEYSset inmacos-option-key-map.ts(only documented an unaddressed bug, never imported).shell:allow-execute, narrowfs:scope, narrowassetProtocol.scope) is a separate effort on its own branch.🤖 Generated with Claude Code