//! Wayland-compatible global hotkey listener for Magnotia. //! //! On Linux, reads `/dev/input/event*` devices via the `evdev` crate to capture //! global hotkeys without any display-server dependency. This works on both X11 //! and Wayland, but requires the user to be in the `input` group (or have read //! access to `/dev/input/`). //! //! On non-Linux platforms, this crate is a no-op — the Tauri global-shortcut //! plugin handles hotkeys there. //! //! Architecture stolen from oddlama/whisper-overlay and adapted for Magnotia. #[cfg(target_os = "linux")] mod linux; #[cfg(target_os = "linux")] pub use linux::*; #[cfg(not(target_os = "linux"))] mod stub; #[cfg(not(target_os = "linux"))] pub use stub::*; use serde::{Deserialize, Serialize}; /// A hotkey combination: one or more modifiers + a trigger key. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct HotkeyCombo { pub ctrl: bool, pub shift: bool, pub alt: bool, pub super_key: bool, /// The evdev key code for the trigger key (e.g. KEY_R = 19). /// On the frontend, this is mapped from the key name. pub key_code: u16, /// Human-readable label for display (e.g. "Ctrl+Shift+R"). pub label: String, } impl HotkeyCombo { /// Parse a Tauri-style hotkey string like "Ctrl+Shift+R" into a HotkeyCombo. /// Returns None if the string can't be parsed. pub fn from_tauri_str(s: &str) -> Option { let parts: Vec<&str> = s.split('+').map(|p| p.trim()).collect(); if parts.is_empty() { return None; } let mut ctrl = false; let mut shift = false; let mut alt = false; let mut super_key = false; let mut trigger: Option<&str> = None; for part in &parts { match part.to_lowercase().as_str() { "ctrl" | "control" => ctrl = true, "shift" => shift = true, "alt" => alt = true, "super" | "meta" | "cmd" | "command" => super_key = true, _ => trigger = Some(part), } } let key_name = trigger?; let key_code = key_name_to_evdev_code(key_name)?; Some(Self { ctrl, shift, alt, super_key, key_code, label: s.to_string(), }) } } /// Map a key name (from the frontend) to an evdev key code. /// Covers the keys likely to be used in hotkey combos. fn key_name_to_evdev_code(name: &str) -> Option { // evdev key codes from linux/input-event-codes.h Some(match name.to_uppercase().as_str() { "A" => 30, "B" => 48, "C" => 46, "D" => 32, "E" => 18, "F" => 33, "G" => 34, "H" => 35, "I" => 23, "J" => 36, "K" => 37, "L" => 38, "M" => 50, "N" => 49, "O" => 24, "P" => 25, "Q" => 16, "R" => 19, "S" => 31, "T" => 20, "U" => 22, "V" => 47, "W" => 17, "X" => 45, "Y" => 21, "Z" => 44, "1" => 2, "2" => 3, "3" => 4, "4" => 5, "5" => 6, "6" => 7, "7" => 8, "8" => 9, "9" => 10, "0" => 11, "F1" => 59, "F2" => 60, "F3" => 61, "F4" => 62, "F5" => 63, "F6" => 64, "F7" => 65, "F8" => 66, "F9" => 67, "F10" => 68, "F11" => 87, "F12" => 88, "SPACE" | " " => 57, "ESCAPE" | "ESC" => 1, "TAB" => 15, "BACKSPACE" => 14, "ENTER" | "RETURN" => 28, "DELETE" => 111, "HOME" => 102, "END" => 107, "PAGEUP" => 104, "PAGEDOWN" => 109, "UP" | "ARROWUP" => 103, "DOWN" | "ARROWDOWN" => 108, "LEFT" | "ARROWLEFT" => 105, "RIGHT" | "ARROWRIGHT" => 106, "INSERT" => 110, "PAUSE" => 119, "SCROLLLOCK" => 70, "PRINTSCREEN" => 99, "`" | "BACKQUOTE" => 41, "-" | "MINUS" => 12, "=" | "EQUAL" => 13, "[" | "BRACKETLEFT" => 26, "]" | "BRACKETRIGHT" => 27, "\\" | "BACKSLASH" => 43, ";" | "SEMICOLON" => 39, "'" | "QUOTE" => 40, "," | "COMMA" => 51, "." | "PERIOD" => 52, "/" | "SLASH" => 53, _ => return None, }) } /// Check whether the current user can read evdev devices. /// Returns a diagnostic message if not. pub fn check_evdev_access() -> Result<(), String> { #[cfg(target_os = "linux")] { linux::check_access() } #[cfg(not(target_os = "linux"))] { Err("evdev hotkeys are only supported on Linux".to_string()) } }