From f2e55ba50fac3182ef836831a9ab6b1ec2009d3c Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sun, 26 Jul 2026 23:17:20 -0700 Subject: [PATCH] fix(dictation): run hotkey commands off the main thread MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit enable_hotkey/disable_hotkey/update_chord_bindings were sync commands, which Tauri runs on the main thread. update_bindings joins the dispatcher thread, and the dispatcher's chord-effect path blocks on main-thread window calls (outer_size, current_monitor, set_position) — toggling the hotkey while an effect was in flight could deadlock. Async commands run on the runtime pool, so the join no longer blocks the thread the dispatcher is waiting on. --- tauri/src-tauri/src/main.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index a17d217a..56f607d9 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -1193,9 +1193,14 @@ fn build_chord_bindings( /// On macOS this is the call that triggers the "Voicebox would like to receive /// keystrokes from any application" TCC prompt, since keytap's `Tap` creates /// the CGEventTap inside `HotkeyMonitor::spawn`. +/// +/// This command (and `disable_hotkey` / `update_chord_bindings`) must stay +/// `async` so it runs off the main thread: `update_bindings` joins the +/// dispatcher, and the dispatcher's chord-effect path blocks on main-thread +/// window calls — a sync command would deadlock with an effect in flight. #[cfg(desktop)] #[command] -fn enable_hotkey( +async fn enable_hotkey( app: tauri::AppHandle, state: State<'_, HotkeyState>, push_to_talk: Vec, @@ -1240,7 +1245,7 @@ fn enable_hotkey( /// without re-prompting for Input Monitoring permission. #[cfg(desktop)] #[command] -fn disable_hotkey(state: State<'_, HotkeyState>) -> Result<(), String> { +async fn disable_hotkey(state: State<'_, HotkeyState>) -> Result<(), String> { let mut slot = state.monitor.lock().map_err(|e| e.to_string())?; if let Some(monitor) = slot.as_mut() { monitor.update_bindings(hotkey_monitor::Bindings::new()); @@ -1259,7 +1264,7 @@ fn disable_hotkey(state: State<'_, HotkeyState>) -> Result<(), String> { /// dropping it from the chord. #[cfg(desktop)] #[command] -fn update_chord_bindings( +async fn update_chord_bindings( state: State<'_, HotkeyState>, push_to_talk: Vec, toggle_to_talk: Vec,