Skip to content

Commit 1484c4f

Browse files
committed
M6: Global hotkey ⌃⌥⌘J
- Add `tauri-plugin-global-shortcut` 2.3.1 (Cargo only — FE registers via the backend) with the matching `global-shortcut:default` capability on the main window. - New `downloads::global_shortcut`: typed `RegistrationError` (`Conflict`/`InvalidBinding`/`PluginError`) + `RegistrationStatus` state machine on top of a `Registrar` trait so the unit tests use an in-memory fake. The plugin's English error strings get mapped to typed errors at one chokepoint inside the production registrar. - `set_global_reveal_shortcut(enabled, binding)` IPC so the Settings UI can flip the live registration; backend startup + main-window focus events re-evaluate the FDA gate via `refresh_global_reveal_shortcut`. - Backend `binding_to_accelerator` adapter mirrors the FE one (`'⌃⌥⌘J'` → `'Control+Alt+Meta+J'`). - FE bridge in `lib/downloads/global-shortcut-bridge.svelte.ts`: one `global-shortcut-fired` listener, calls `revealLatestDownload`, and on the first un-acknowledged trigger flips `acknowledged = true` BEFORE opening the persistent warn toast (so back-to-back presses don't queue duplicates). - `GlobalShortcutWarnToastContent.svelte`: "Keep it on" / "Turn it off"; the latter flips `enabled = false` AND calls `setGlobalRevealShortcut(false, binding)` so the OS combo releases immediately. - Settings registry gains the three `behavior.fileSystemWatching.globalRevealShortcut.{enabled,binding,acknowledged}` entries; `setGlobalRevealBinding` resets `acknowledged` to `false` so each fresh combo gets its own first-trigger warning. - Stub row in `DriveIndexingSection.svelte` exposing the on/off toggle, the binding text input, and the inline status string (Registered / Couldn't register: in use by another app / Cmdr needs Full Disk Access). M7 will polish + rename the section. - Both `CLAUDE.md` files (FE + BE) gain a "Global reveal hotkey" subsection.
1 parent 32f7573 commit 1484c4f

23 files changed

Lines changed: 1314 additions & 18 deletions

Cargo.lock

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ tauri-plugin-mcp-bridge = "0.11"
3939
tauri-plugin-store = "2"
4040
tauri-plugin-dialog = "2.6"
4141
tauri-plugin-notification = "2.3.3"
42+
# Global system-wide keyboard shortcut (default ⌃⌥⌘J for "reveal latest download"). On
43+
# macOS, the plugin uses Carbon's `RegisterEventHotKey` so it needs no extra TCC grant
44+
# (Accessibility/Input Monitoring not required). Pinned at 2.3.1 (published 2025-10-27,
45+
# ~7 months old; the 2.3.2 release from 2026-05-28 is still within the 14-day age policy).
46+
tauri-plugin-global-shortcut = "=2.3.1"
4247
serde = { version = "1", features = ["derive"] }
4348
serde_json = "1"
4449
notify = "8"

apps/desktop/src-tauri/capabilities/default.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"updater:default",
3737
"process:allow-restart",
3838
"dialog:allow-ask",
39-
"notification:default"
39+
"notification:default",
40+
"global-shortcut:default"
4041
]
4142
}

apps/desktop/src-tauri/src/downloads/CLAUDE.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,11 @@ a partial-suffix file to a final-name file). FDA-gated.
1919
- `runtime.rs``Mutex<Option<DownloadsWatcher>>`. `refresh_runtime(&app)` aligns the handle
2020
with `desired_running(fda_pending)`. Idempotent.
2121
- `commands.rs` — IPC surface: `reveal_latest_download`, `downloads_watcher_status`,
22-
`recheck_downloads_watcher_gate`.
22+
`recheck_downloads_watcher_gate`, `set_global_reveal_shortcut`.
23+
- `global_shortcut.rs` — Wrapper around `tauri-plugin-global-shortcut`. Typed `RegistrationError`
24+
(`Conflict | InvalidBinding | PluginError`) + `RegistrationStatus` (`Registered | NotRegistered |
25+
Conflict`). The state machine sits in `GlobalShortcutManager<R: Registrar>`; production uses
26+
`TauriRegistrar` (owned `AppHandle`), tests use an in-memory `FakeRegistrar`.
2327

2428
## FDA gating contract
2529

@@ -57,6 +61,29 @@ the ignore set. Cmdr never writes `.crdownload` files; always register the final
5761
The bulk variant `note_pending_writes(paths, ttl)` exists for transfer-driver paths that know
5862
their destination set up front.
5963

64+
## Global reveal hotkey (M6)
65+
66+
The default global combo is `⌃⌥⌘J`. `apps/desktop/src-tauri/src/lib.rs` calls
67+
`downloads::refresh_global_reveal_shortcut(app)` at:
68+
69+
1. **Startup**, after `set_fda_pending(...)`, alongside the watcher refresh.
70+
2. **Every main-window `Focused(true)` event** — covers the FDA flip path.
71+
3. **Settings UI flip** via the `set_global_reveal_shortcut(enabled, binding)` IPC command,
72+
which the FE calls from the Settings row's change handlers.
73+
74+
The trigger handler (`global_shortcut::plugin_builder`) emits a `global-shortcut-fired` Tauri
75+
event on every key-down. The FE bridge in `lib/downloads/global-shortcut-bridge.svelte.ts`
76+
subscribes and routes through `revealLatestDownload`. The first-trigger warn toast logic
77+
lives FE-side, keyed on the `acknowledged` settings flag.
78+
79+
The plugin uses Carbon's `RegisterEventHotKey` on macOS, so no Accessibility / Input
80+
Monitoring TCC grant is needed; the user sees no extra prompt for the hotkey.
81+
82+
The `register/unregister` state machine in `GlobalShortcutManager` is idempotent: re-registering
83+
the same binding is a no-op, swapping to a new binding unregisters the previous one first, and
84+
a `Conflict` error stays remembered until the next successful register so the Settings row can
85+
surface "Couldn't register: in use by another app." without re-attempting.
86+
6087
## Browser-style rename target
6188

6289
v1 scopes to browser-style downloads: a direct create of a final-name file, or a rename from a

apps/desktop/src-tauri/src/downloads/commands.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,34 @@ pub async fn recheck_downloads_watcher_gate(app: AppHandle) -> Result<(), String
131131
super::runtime::refresh_runtime(&app).map_err(|e| e.to_string())
132132
}
133133

134+
/// Result of [`set_global_reveal_shortcut`]: the new status the Settings row
135+
/// should display. The FE caches this until the next register/unregister, so
136+
/// the row's "Registered" / "Couldn't register" indicator stays in sync
137+
/// without an extra round trip.
138+
#[derive(Debug, Clone, Serialize, Deserialize, specta::Type)]
139+
#[serde(rename_all = "camelCase")]
140+
pub struct GlobalRevealShortcutState {
141+
pub status: super::global_shortcut::RegistrationStatus,
142+
pub binding: String,
143+
pub enabled: bool,
144+
}
145+
146+
/// Apply a Settings change (toggle + binding) to the live global-shortcut
147+
/// registration. Idempotent; safe to call repeatedly with the same args.
148+
///
149+
/// Returns the resulting status so the FE row can render the indicator
150+
/// without another round trip. Errors are wrapped in the typed
151+
/// [`super::global_shortcut::RegistrationError`] enum.
152+
#[tauri::command]
153+
#[specta::specta]
154+
pub async fn set_global_reveal_shortcut(
155+
app: AppHandle,
156+
enabled: bool,
157+
binding: String,
158+
) -> Result<GlobalRevealShortcutState, super::global_shortcut::RegistrationError> {
159+
super::runtime::apply_global_reveal_shortcut(&app, enabled, &binding)
160+
}
161+
134162
#[cfg(test)]
135163
mod tests {
136164
//! Tests for the `reveal_latest_download` branches. The process-global

0 commit comments

Comments
 (0)