Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.
transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
-> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
as immutable audit trail). Includes architecture-map references
to magnotia_core::*, magnotia_storage::*, etc. now pointing at
lumotia_*; dev-setup.md tracing output examples (lumotia_startup
target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
("lumotia_task_sync"); magnotia_locale i18n localStorage key
renamed + migration shim added; CSS keyframe names
magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
to "lumotia era" earlier — restored).
Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
legacy detection strings in legacy_and_target_paths() so the
migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
— migration call sites reference the legacy magnotia keys
deliberately.
- docs/handovers/ — historical audit trail.
cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.2 KiB
name, type, slice, last_verified
| name | type | slice | last_verified |
|---|---|---|---|
| Hotkey crate (Linux evdev) | architecture-map-page | 05-core-storage-hotkey-build | 2026/05/09 |
Hotkey crate (Linux evdev)
Where you are: Architecture map → Core, Storage, Hotkey, Build → Hotkey crate (Linux evdev)
Plain English summary. A global hotkey listener for Linux that reads keypresses straight from /dev/input/event* rather than going through a display server. That makes it work on both X11 and Wayland, with no compositor-specific protocol negotiation. macOS and Windows fall back to Tauri's global-shortcut plugin; this crate compiles to a no-op on those platforms.
At a glance
- Crate:
lumotia-hotkey. - LOC: 632 (
lib.rs177,linux.rs426,stub.rs29). - External deps:
tokio 1(rt + sync + macros + time),serde 1,log 0.4, plus Linux-only:evdev 0.12(withtokiofeature),notify 7(default features off,macos_fseventonly),nix 0.29(fsfeature). - Public surface:
HotkeyCombo,HotkeyEvent,EvdevHotkeyListener,check_evdev_access. Plus the parserHotkeyCombo::from_tauri_str. - Consumers: slice 2 (
src-tauri/src/commands/hotkey.rs) is the only caller in production. The Tauri command holds anEvdevHotkeyListenerintauri::Stateand forwardsHotkeyEventovermpscto the live-session command.
Public surface
HotkeyCombo — crates/hotkey/src/lib.rs:33
Cross-platform shape. Lives in lib.rs so both linux and stub modules can use it.
pub struct HotkeyCombo {
pub ctrl: bool,
pub shift: bool,
pub alt: bool,
pub super_key: bool,
pub key_code: u16, // evdev key code
pub label: String, // user-facing, eg "Ctrl+Shift+R"
}
impl HotkeyCombo {
pub fn from_tauri_str(s: &str) -> Option<Self>;
}
from_tauri_str parser — crates/hotkey/src/lib.rs:50
Splits on +, accepts ctrl|control, shift, alt, super|meta|cmd|command, plus a single trigger key. The key-name → evdev-code mapping at lib.rs:79 covers A-Z, 0-9, F1-F12, plus arrows, modifiers, and the standard punctuation set. Returns None on unmappable input.
HotkeyEvent — crates/hotkey/src/linux.rs:23 / stub.rs:11
Defined in both backends:
pub enum HotkeyEvent { Pressed, Released }
Released matters for push-to-talk.
EvdevHotkeyListener — crates/hotkey/src/linux.rs:32
pub struct EvdevHotkeyListener { /* hotkey_tx, shutdown_tx */ }
impl EvdevHotkeyListener {
pub fn start(combo: HotkeyCombo, event_tx: mpsc::Sender<HotkeyEvent>) -> Self;
pub fn set_hotkey(&self, combo: HotkeyCombo);
pub async fn stop(&self);
}
start spawns:
- One async task per input device that supports the configured trigger key (initial scan).
- A watcher task that listens on
notifyfor new files in/dev/input/and attaches per-device listeners with a 5-attempt 1-second-backoff loop (udev permissions propagate asynchronously afterinotify CREATE).
set_hotkey updates a tokio::sync::watch::Sender<Option<HotkeyCombo>>. All listener tasks pick up the change; future devices use the new combo.
stop sends None on the watch channel and signals the shutdown channel. Per-device tasks exit on the next hotkey_rx.changed() poll.
check_evdev_access() — crates/hotkey/src/lib.rs:166
Probes whether the current user can read evdev devices. On permission denied returns:
Permission denied reading /dev/input/eventN. Add your user to the 'input' group:
sudo usermod -aG input $USER (then log out and back in)
The hotkey command in slice 2 calls this at startup; the message becomes a non-modal toast on first run.
Linux backend internals
Device hotplug — crates/hotkey/src/linux.rs:64-136
Uses notify::recommended_watcher against /dev/input/. On inotify Create(_) events with a path that starts event*, it spawns a 5-attempt retry to attach a per-device listener. Retries are 1-second sleeps; udev permissions propagate over a window of a few hundred milliseconds after device creation.
Failure modes:
recommended_watcherreturnsErr(rare; minimal containers, BSD pretending to be Linux). Logs and degrades to "no hotplug detection" — the initial scan still picks up devices that exist at startup. Non-fatal.watcher.watch("/dev/input")returnsErr(eg/dev/inputitself missing). Same fallback.
Per-device listener — crates/hotkey/src/linux.rs:274
device.into_event_stream() gives an async stream of evdev::InputEvent. The listener tracks four modifier flags (ctrl_held, shift_held, alt_held, super_held) by watching KEY_LEFT* and KEY_RIGHT* press / release. When the trigger key matches and modifier state matches, sends HotkeyEvent::Pressed or HotkeyEvent::Released on event_tx.
device_supports_combo — crates/hotkey/src/linux.rs:368
Filters out devices whose reported EV_KEY capability does not include the configured trigger. Replaces the RB-12 hard-coded KEY_A || KEY_R filter from the original whisper-overlay port. See Existing in-repo docs.
fn device_supports_combo(supported: Option<&AttributeSetRef<Key>>, combo: &HotkeyCombo) -> bool {
supported.is_some_and(|keys| keys.contains(Key::new(combo.key_code)))
}
Channel close handling — crates/hotkey/src/linux.rs:328
When event_tx.send(...) fails, the listener exits cleanly: receiver was dropped, no point spinning. Logged at WARN once per device.
Tests
crates/hotkey/src/linux.rs:372-426:
attaches_when_device_supports_configured_trigger— happy path.rejects_when_device_lacks_configured_trigger— wrong key.rejects_when_device_reports_no_keys—Nonecapability set.attaches_for_non_a_non_r_trigger— RB-12 regression: aCtrl+Shift+Dbinding now attaches to a device that reports KEY_D and rejects a device that only reports KEY_R.
Watch-outs
- Linux only. The crate compiles to a no-op (
stub.rs) on macOS and Windows where Tauri'splugin-global-shortcuthandles hotkeys natively. Feature parity is maintained across platforms but by two distinct mechanisms — including, for example, hotplug detection (Linux only). - Requires user in the
inputgroup. Documented in thecheck_evdev_accesserror message. Without group membership, no devices open. - Modifier tracking is per-device. A user pressing
Ctrlon the physical keyboard and the trigger on the laptop's built-in keyboard would not match — modifier state is local to one device's stream. In practice users press all keys on the same keyboard so this is fine, but worth knowing for split keyboard rigs. notifylistens on/dev/inputnon-recursively. Sub-directories would be missed. None exist today.- Hotplug retry attempts (5 × 1 s) can stack. A USB hub that announces 10 keyboards in a burst spawns 10 retry tasks; benign but worth knowing.
Existing in-repo docs
docs/issues/hotkey-linux-device-filter.md— RB-12. The hard-codedKEY_A || KEY_Rfilter the currentdevice_supports_comboreplaced.
See also
- Slice 2 hotkey command — the only caller.
- Slice 1 preferences (hotkey configuration) — the UI that produces a Tauri-style hotkey string.