-
Notifications
You must be signed in to change notification settings - Fork 1.7k
fix(hotkeys): report shortcuts the OS refuses instead of silently dropping them #2041
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -129,8 +129,13 @@ pub fn init(app: &AppHandle) { | |||||||||
| }; | ||||||||||
|
|
||||||||||
| let global_shortcut = app.global_shortcut(); | ||||||||||
| for hotkey in store.hotkeys.values() { | ||||||||||
| global_shortcut.register(Shortcut::from(*hotkey)).ok(); | ||||||||||
| for (action, hotkey) in store.hotkeys.iter() { | ||||||||||
| // A shortcut stored on a previous run can stop being registrable, e.g. | ||||||||||
| // 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}"); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| app.manage(Mutex::new(store)); | ||||||||||
|
|
@@ -258,27 +263,63 @@ async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), Strin | |||||||||
| #[tauri::command(async)] | ||||||||||
| #[specta::specta] | ||||||||||
| #[instrument(skip(app))] | ||||||||||
| pub fn set_hotkey(app: AppHandle, action: HotkeyAction, hotkey: Option<Hotkey>) -> Result<(), ()> { | ||||||||||
| pub fn set_hotkey( | ||||||||||
| app: AppHandle, | ||||||||||
| action: HotkeyAction, | ||||||||||
| hotkey: Option<Hotkey>, | ||||||||||
| ) -> Result<(), String> { | ||||||||||
| let global_shortcut = app.global_shortcut(); | ||||||||||
| let state = app.state::<HotkeysState>(); | ||||||||||
| let mut store = state.lock().unwrap(); | ||||||||||
|
|
||||||||||
| let prev = store.hotkeys.get(&action).cloned(); | ||||||||||
|
|
||||||||||
| // Apply to the store first so the "is this combination still used by | ||||||||||
| // another action?" check below sees the post-change state, then reconcile | ||||||||||
| // the OS registrations. | ||||||||||
| if let Some(hotkey) = hotkey { | ||||||||||
| store.hotkeys.insert(action, hotkey); | ||||||||||
| } else { | ||||||||||
| store.hotkeys.remove(&action); | ||||||||||
| } | ||||||||||
|
|
||||||||||
| // Release the previous combination before registering the new one: the | ||||||||||
| // underlying global-hotkey layer rejects a shortcut that is already | ||||||||||
| // registered, so re-binding an action to a combination another action just | ||||||||||
| // gave up would otherwise fail. | ||||||||||
| if let Some(prev) = prev | ||||||||||
| && !store.hotkeys.values().any(|h| h == &prev) | ||||||||||
| { | ||||||||||
| global_shortcut.unregister(Shortcut::from(prev)).ok(); | ||||||||||
| if let Err(e) = global_shortcut.unregister(Shortcut::from(prev)) { | ||||||||||
| // Not fatal on its own; log it so a stale registration that keeps | ||||||||||
| // firing the old combination is diagnosable. | ||||||||||
| tracing::warn!(?action, prev = ?prev, "failed to unregister previous hotkey: {e}"); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if let Some(hotkey) = hotkey { | ||||||||||
| global_shortcut.register(Shortcut::from(hotkey)).ok(); | ||||||||||
| // The OS can refuse a shortcut: reserved by the system (PrintScreen on | ||||||||||
| // Windows) or already claimed by another application. Previously this | ||||||||||
| // was `.ok()`, so the binding was stored and shown in Settings while | ||||||||||
| // pressing it did nothing. | ||||||||||
| if let Err(e) = global_shortcut.register(Shortcut::from(hotkey)) { | ||||||||||
| tracing::warn!(?action, ?hotkey, "failed to register hotkey: {e}"); | ||||||||||
|
|
||||||||||
| // Roll the store back so it matches what is actually registered. | ||||||||||
| match prev { | ||||||||||
| Some(prev) => { | ||||||||||
| store.hotkeys.insert(action, prev); | ||||||||||
| global_shortcut.register(Shortcut::from(prev)).ok(); | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the rollback path,
Suggested change
|
||||||||||
| } | ||||||||||
| None => { | ||||||||||
| store.hotkeys.remove(&action); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| return Err(format!( | ||||||||||
| "Could not register this shortcut. It may already be in use by another application, or reserved by the system. ({e})" | ||||||||||
| )); | ||||||||||
| } | ||||||||||
| } | ||||||||||
|
|
||||||||||
| Ok(()) | ||||||||||
|
|
||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -12,6 +12,7 @@ import { | |||||||||||||||||
| Switch, | ||||||||||||||||||
| } from "solid-js"; | ||||||||||||||||||
| import { createStore } from "solid-js/store"; | ||||||||||||||||||
| import toast from "solid-toast"; | ||||||||||||||||||
| import { hotkeysStore } from "~/store"; | ||||||||||||||||||
|
|
||||||||||||||||||
| import { | ||||||||||||||||||
|
|
@@ -141,14 +142,29 @@ function Inner(props: { initialStore: HotkeysStore | null }) { | |||||||||||||||||
| class="w-fit" | ||||||||||||||||||
| type="button" | ||||||||||||||||||
| onBlur={(e) => console.log(e)} | ||||||||||||||||||
| onClick={(e) => { | ||||||||||||||||||
| onClick={async (e) => { | ||||||||||||||||||
| e.stopPropagation(); | ||||||||||||||||||
|
|
||||||||||||||||||
| const action = item(); | ||||||||||||||||||
| const binding = hotkeys[action] ?? null; | ||||||||||||||||||
| const previous = listening()?.prev; | ||||||||||||||||||
|
|
||||||||||||||||||
| setListening(); | ||||||||||||||||||
| commands.setHotkey( | ||||||||||||||||||
| item(), | ||||||||||||||||||
| hotkeys[item()] ?? null, | ||||||||||||||||||
| ); | ||||||||||||||||||
|
|
||||||||||||||||||
| try { | ||||||||||||||||||
| await commands.setHotkey(action, binding); | ||||||||||||||||||
| } catch (error) { | ||||||||||||||||||
| // The OS can refuse a shortcut (reserved, or | ||||||||||||||||||
| // already taken by another app). Roll the row | ||||||||||||||||||
| // 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the rejection comes back as an
Suggested change
|
||||||||||||||||||
| typeof error === "string" | ||||||||||||||||||
| ? error | ||||||||||||||||||
| : "Could not register this shortcut.", | ||||||||||||||||||
| ); | ||||||||||||||||||
| } | ||||||||||||||||||
| }} | ||||||||||||||||||
| > | ||||||||||||||||||
| <IconCapCircleCheck class="transition-colors text-gray-12 hover:text-gray-10 size-5" /> | ||||||||||||||||||
|
|
||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would you mind logging the error as a structured field here (easier to search/alert on than interpolating into the message)?