--- name: Hotkey bridge type: architecture-map-page slice: 02-tauri-runtime last_verified: 2026/05/09 --- # `commands::hotkey` > **Where you are:** [Architecture map](../../README.md) → [Tauri runtime](../README.md) → [Commands](README.md) → Hotkey bridge **Plain English summary.** The Linux Wayland-compatible global hotkey backend. Tauri's `tauri-plugin-global-shortcut` works on macOS / Windows and on X11 Linux but fails silently on Wayland (the protocol forbids unprivileged keystroke grabs). Magnotia's bespoke evdev backend reads `/dev/input/event*` directly, parses key combos, and emits `magnotia:hotkey-pressed` / `magnotia:hotkey-released` events. Settings on Linux uses these commands instead of the global-shortcut plugin; everywhere else the plugin path is canonical. ## At a glance - Path: `src-tauri/src/commands/hotkey.rs`. - LOC: 105. - Tauri commands exposed (5 total): - `is_wayland_session() -> bool`. Pure env probe. - `check_hotkey_access() -> Result<(), String>`. Probes evdev access (user in `input` group, etc.). - `start_evdev_hotkey(app, state, hotkey: String) -> Result<(), String>`. Parses the Tauri-style combo string, stops any existing listener, starts a new one, spawns a forwarder task that converts evdev events to Tauri events. - `update_evdev_hotkey(state, hotkey: String) -> Result<(), String>`. Updates the combo on a running listener. - `stop_evdev_hotkey(state) -> Result<(), String>`. Stops cleanly. - Events emitted: `magnotia:hotkey-pressed` (no payload), `magnotia:hotkey-released` (no payload). Fired from the forwarder task in `start_evdev_hotkey` (`src-tauri/src/commands/hotkey.rs:67`, `:70`). - Depends on: `magnotia_hotkey::{EvdevHotkeyListener, HotkeyCombo, HotkeyEvent, check_evdev_access}`. Plus `tokio::sync::{mpsc, Mutex}`. - Called from frontend at: Settings → Hotkey on Linux. Other platforms call the `tauri-plugin-global-shortcut` plugin's JS API directly. ## What's in here ### `HotkeyState` (`src-tauri/src/commands/hotkey.rs:9`) Tauri-managed: `listener: Arc>>`. `tokio::sync::Mutex` because operations await across `.lock()`. ### `is_wayland_session` (`:23`) Returns true if `WAYLAND_DISPLAY` is set or `XDG_SESSION_TYPE=wayland`. Used by Settings to decide whether to display the Linux-only hotkey UI. ### `check_hotkey_access` (`:32`) Defers to `magnotia_hotkey::check_evdev_access` — that helper checks the user can read `/dev/input/event*` (typically requires being in the `input` group, or a udev rule). On failure, returns the actionable error string. ### `start_evdev_hotkey` (`:41`) 1. Parse the hotkey string (e.g. `"Shift+Cmd+Space"`) via `HotkeyCombo::from_tauri_str`. Errors become `"Cannot parse hotkey: ..."`. 2. Lock the listener mutex; if a listener is already running, stop it and await termination. 3. Build a 64-deep `tokio::sync::mpsc` channel for `HotkeyEvent`s. 4. Call `EvdevHotkeyListener::start(combo, event_tx)`, stash the listener. 5. Spawn a `tokio::spawn` forwarder that reads from the event_rx and emits `magnotia:hotkey-pressed` / `magnotia:hotkey-released` per event variant. ### `update_evdev_hotkey` (`:81`) Re-parses the combo and calls `listener.set_hotkey(combo)` if a listener is running. Returns `"Hotkey listener not running"` otherwise. Avoids the cost of stopping and restarting the evdev thread. ### `stop_evdev_hotkey` (`:99`) Take the listener out, call `listener.stop().await`. Idempotent — calling stop with no listener returns Ok silently. ## Data flow ``` Settings -> is_wayland_session() -> bool Settings -> check_hotkey_access() -> Ok or "join the input group / install udev rule" Settings -> start_evdev_hotkey("Shift+Cmd+Space") -> parse combo -> stop any prior listener -> EvdevHotkeyListener spawns its own thread reading /dev/input/event* -> forwarder spawn: HotkeyEvent -> Tauri event frontend listens on 'magnotia:hotkey-pressed' / '...-released' and starts/stops dictation Settings -> update_evdev_hotkey("Ctrl+Alt+M") (when user changes binding) Settings -> stop_evdev_hotkey() (when user disables) ``` ## Watch-outs - **No `ensure_main_window` guard.** Settings is in the main window. If you ever expose hotkey re-binding in a secondary window, add the guard. - **Linux only.** macOS and Windows code paths in the frontend talk to `tauri-plugin-global-shortcut` directly — no Rust commands needed because the plugin handles register / unregister via JS. This module compiles and runs on every desktop OS but only *works* on Linux because evdev is Linux-specific (the workspace crate `magnotia_hotkey` has the platform shim). - **`/dev/input/event*` access requires either:** (a) the user is in the `input` group, or (b) a udev rule grants Magnotia explicit access. The Settings UI walks the user through option (a) on first run; option (b) is documented in `docs/dev-setup.md` for power users. - **Channel size of 64** is enough for a key smash burst. If a worker stalls and the channel fills, evdev events would be dropped silently. Consider logging a warning when the channel is at capacity. - **The forwarder spawn is detached.** No way to stop the spawn task explicitly; it exits when `event_rx.recv().await` returns None (which happens when the listener is dropped). Acceptable. - **No power assertion.** The evdev listener thread is pinned by the kernel via the file handle; idle CPU is near zero. ## See also - [App lifecycle](../app-lifecycle.md) — `HotkeyState::new()` is stashed in setup. - [Diagnostics](diagnostics.md) — `OsInfo.custom_hotkey_backend` flags Linux as the evdev path. - [Paste](paste.md) — the consumer of "user pressed dictation hotkey": holds focus on the target window for the eventual paste. - [Live transcription](live.md) — the dictation flow that the hotkey starts.