fix(hotkeys): report shortcuts the OS refuses instead of silently dropping them#2041
Open
DPS0340 wants to merge 1 commit into
Open
fix(hotkeys): report shortcuts the OS refuses instead of silently dropping them#2041DPS0340 wants to merge 1 commit into
DPS0340 wants to merge 1 commit into
Conversation
…pping them Every global-shortcut registration discarded its result with `.ok()`, so a combination the OS rejects was still written to the store and rendered in Settings as bound. Pressing it did nothing, with no error anywhere. That is CapSoftware#1883: PrintScreen is reserved by Windows, so `register()` fails and the binding silently never fires. The reporter read this as "can't bind the print key", and separately concluded key combinations were unsupported -- they are (modifiers are captured and displayed), but any combination another application already owns fails the same silent way. - `set_hotkey` returns `Result<(), String>` and reports the failure. On error the store is rolled back to the previous binding so it matches what is actually registered with the OS. - Unregister the previous combination *before* registering the new one. The underlying global-hotkey layer rejects an already-registered shortcut, so re-binding to a combination another action just released would otherwise fail. - Startup registration logs failures rather than dropping them, so a shortcut claimed by another app since last run is diagnosable. - Settings surfaces the error as a toast and reverts the row. `Result<(), String>` generates the same `Promise<null>` binding as the existing `Result<(), ()>` (matching `set_mic_input`), so tauri.ts is unchanged; the promise now rejects instead of resolving. Closes CapSoftware#1883
| // another application claimed it since. Surface it in the log rather | ||
| // than starting up with a binding the user believes is active. | ||
| if let Err(e) = global_shortcut.register(Shortcut::from(*hotkey)) { | ||
| tracing::warn!(?action, ?hotkey, "failed to register stored hotkey: {e}"); |
There was a problem hiding this comment.
Would you mind logging the error as a structured field here (easier to search/alert on than interpolating into the message)?
Suggested change
| tracing::warn!(?action, ?hotkey, "failed to register stored hotkey: {e}"); | |
| tracing::warn!(?action, ?hotkey, error = %e, "failed to register stored hotkey"); |
| match prev { | ||
| Some(prev) => { | ||
| store.hotkeys.insert(action, prev); | ||
| global_shortcut.register(Shortcut::from(prev)).ok(); |
There was a problem hiding this comment.
In the rollback path, register(prev).ok() can fail silently and bring back the same "Settings shows bound but OS didn’t take it" mismatch. Worth at least logging the restore failure.
Suggested change
| global_shortcut.register(Shortcut::from(prev)).ok(); | |
| if let Err(restore_err) = global_shortcut.register(Shortcut::from(prev)) { | |
| tracing::warn!(?action, prev = ?prev, "failed to restore previous hotkey after rollback: {restore_err}"); | |
| } |
| // back so it doesn't display as bound when | ||
| // pressing it will do nothing. | ||
| setHotkeys(action, previous); | ||
| toast.error( |
There was a problem hiding this comment.
If the rejection comes back as an Error, it might be nicer to show error.message (right now non-string errors always fall back).
Suggested change
| toast.error( | |
| toast.error( | |
| typeof error === "string" | |
| ? error | |
| : error instanceof Error | |
| ? error.message | |
| : "Could not register this shortcut.", | |
| ); |
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.
Closes #1883.
What @rlnt hit
Two complaints in that issue, and they turn out to be one bug plus one misreading:
register()fails. But the binding was still stored and still shown in Settings as bound. Nothing surfaced the failure, so the only observable behaviour was "I set it and it does nothing".ctrl/shift/alt/meta) and rendered byHotkeyText. What almost certainly happened is that the combination they tried was already owned by another application, which fails the same silent way and is indistinguishable from "combinations aren't supported".Root cause
Every registration threw its result away:
set_hotkeywas-> Result<(), ()>and unconditionally returnedOk(()), so the frontend had nothing to react to either.Fix
set_hotkeyreturnsResult<(), String>and reports the failure. On error the store is rolled back to the previous binding (and that binding re-registered), so what Settings shows matches what the OS actually has.global-hotkeyrejects an already-registered shortcut, so registering-then-unregistering would have broken re-binding an action to a combination another action had just released. I caught this by readingtauri-plugin-global-shortcut'sregister_internalafter writing the first version the other way round.On the generated bindings
tauri.tsis unchanged and that is expected, not an oversight: specta mapsResult<(), String>to the samePromise<null>it already produced forResult<(), ()>— same shape asset_mic_input, which is alsoResult<(), String>. The difference is that the promise now rejects instead of resolving, which is what the newtry/catchhandles.Verification
npx tsc --noEmit -p apps/desktop/tsconfig.json→ 0 errors.biome checkclean;cargo fmtapplied.solid-toastis a real dependency ofapps/desktopand<Toaster>is mounted inapp.tsx, so the error actually renders in the settings window rather than going nowhere.What I could not do: I don't have a Windows machine, so I have not pressed PrintScreen against a running build — the OS-refusal path is reasoned from the
global-hotkeyAPI contract, not observed.cargo checkdoesn't complete in my environment either, for a reason unrelated to this change: the build script needsbinaries/cap-muxer-aarch64-apple-darwinandtarget/native-deps/Spacedrive.framework, and it fails identically on unmodifiedmain— I verified that by stashing. So the Rust side is compile-checked by review only, and worth a real build before merge.