Skip to main content

rdesktop_core/
hotkeys.rs

1//! Global hotkey manager — registers OS-level key combinations that fire even
2//! when the application window is not focused.
3//!
4//! This goes beyond Tauri v2's `global-shortcut`, which only covers a fixed
5//! subset. rdesktop owns the platform integration directly:
6//!
7//! - **Windows**: `RegisterHotKey` + a dedicated message-pump thread that
8//!   receives `WM_HOTKEY` and dispatches to the handler.
9//! - **macOS**: a `CGEventTap` on `kCGHIDEventTap` that recognises the combo
10//!   and consumes the event so it never reaches other apps.
11//!
12//! Both paths feed the same [`HotkeyHandler`] callback, so application code is
13//! platform-agnostic.
14
15use crate::error::Result;
16use serde::{Deserialize, Serialize};
17use std::fmt;
18use std::str::FromStr;
19use std::sync::{Arc, Mutex};
20
21/// Active modifier keys for a hotkey / input event.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
23pub struct Modifiers {
24    pub ctrl: bool,
25    pub alt: bool,
26    pub shift: bool,
27    /// Windows key (Win) on Windows, Command (⌘) on macOS.
28    pub meta: bool,
29}
30
31impl Modifiers {
32    pub fn is_empty(&self) -> bool {
33        !(self.ctrl || self.alt || self.shift || self.meta)
34    }
35
36    /// Build from a Windows `MOD_*` bitmask.
37    pub fn from_raw_win(m: u32) -> Self {
38        Self {
39            ctrl: m & 0x0002 != 0,
40            alt: m & 0x0001 != 0,
41            shift: m & 0x0004 != 0,
42            meta: m & 0x0008 != 0,
43        }
44    }
45
46    /// Build from a macOS `CGEventFlags` bitmask.
47    pub fn from_raw_mac(f: u64) -> Self {
48        const CMD: u64 = 0x100000; // kCGEventFlagMaskCommand
49        const SHIFT: u64 = 0x20000; // kCGEventFlagMaskShift
50        const ALT: u64 = 0x80000; // kCGEventFlagMaskAlternate
51        const CTRL: u64 = 0x40000; // kCGEventFlagMaskControl
52        Self {
53            ctrl: f & CTRL != 0,
54            alt: f & ALT != 0,
55            shift: f & SHIFT != 0,
56            meta: f & CMD != 0,
57        }
58    }
59}
60
61/// A platform-independent key.
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
63pub enum Key {
64    Letter(char),
65    Digit(u8),
66    F(u8),
67    Space,
68    Enter,
69    Escape,
70    Tab,
71    Backspace,
72    Delete,
73    ArrowUp,
74    ArrowDown,
75    ArrowLeft,
76    ArrowRight,
77    Home,
78    End,
79    PageUp,
80    PageDown,
81    Insert,
82}
83
84impl Key {
85    /// Parse a single key token (e.g. `"K"`, `"F5"`, `"Space"`, `"Up"`).
86    pub fn from_token(tok: &str) -> Option<Key> {
87        let t = tok.trim();
88        if t.len() == 1 {
89            let c = t.as_bytes()[0];
90            if c.is_ascii_alphabetic() {
91                return Some(Key::Letter(c.to_ascii_lowercase() as char));
92            }
93            if c.is_ascii_digit() {
94                return Some(Key::Digit(c - b'0'));
95            }
96        }
97        match t.to_ascii_lowercase().as_str() {
98            "space" => Some(Key::Space),
99            "enter" | "return" => Some(Key::Enter),
100            "escape" | "esc" => Some(Key::Escape),
101            "tab" => Some(Key::Tab),
102            "backspace" | "back" => Some(Key::Backspace),
103            "delete" | "del" => Some(Key::Delete),
104            "up" | "arrowup" => Some(Key::ArrowUp),
105            "down" | "arrowdown" => Some(Key::ArrowDown),
106            "left" | "arrowleft" => Some(Key::ArrowLeft),
107            "right" | "arrowright" => Some(Key::ArrowRight),
108            "home" => Some(Key::Home),
109            "end" => Some(Key::End),
110            "pageup" | "prior" => Some(Key::PageUp),
111            "pagedown" | "next" => Some(Key::PageDown),
112            "insert" | "ins" => Some(Key::Insert),
113            _ => {
114                if t.len() > 1 && t.starts_with('f') {
115                    if let Ok(n) = t[1..].parse::<u8>() {
116                        if (1..=24).contains(&n) {
117                            return Some(Key::F(n));
118                        }
119                    }
120                }
121                None
122            }
123        }
124    }
125}
126
127impl fmt::Display for Key {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        match self {
130            Key::Letter(c) => write!(f, "{}", c.to_ascii_uppercase()),
131            Key::Digit(d) => write!(f, "{}", d),
132            Key::F(n) => write!(f, "F{}", n),
133            Key::Space => write!(f, "Space"),
134            Key::Enter => write!(f, "Enter"),
135            Key::Escape => write!(f, "Escape"),
136            Key::Tab => write!(f, "Tab"),
137            Key::Backspace => write!(f, "Backspace"),
138            Key::Delete => write!(f, "Delete"),
139            Key::ArrowUp => write!(f, "Up"),
140            Key::ArrowDown => write!(f, "Down"),
141            Key::ArrowLeft => write!(f, "Left"),
142            Key::ArrowRight => write!(f, "Right"),
143            Key::Home => write!(f, "Home"),
144            Key::End => write!(f, "End"),
145            Key::PageUp => write!(f, "PageUp"),
146            Key::PageDown => write!(f, "PageDown"),
147            Key::Insert => write!(f, "Insert"),
148        }
149    }
150}
151
152/// A global hotkey: a set of modifiers plus a primary key.
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
154pub struct Hotkey {
155    pub modifiers: Modifiers,
156    pub key: Key,
157}
158
159impl fmt::Display for Hotkey {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        let mut parts: Vec<String> = Vec::new();
162        if self.modifiers.ctrl {
163            parts.push("Ctrl".into());
164        }
165        if self.modifiers.alt {
166            parts.push("Alt".into());
167        }
168        if self.modifiers.shift {
169            parts.push("Shift".into());
170        }
171        if self.modifiers.meta {
172            parts.push("Meta".into());
173        }
174        parts.push(self.key.to_string());
175        write!(f, "{}", parts.join("+"))
176    }
177}
178
179impl FromStr for Hotkey {
180    type Err = String;
181    /// Parse `"Ctrl+Shift+K"`, `"Alt+F4"`, `"Meta+Space"`, etc.
182    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
183        let mut modifiers = Modifiers::default();
184        let mut key: Option<Key> = None;
185        for tok in s.split('+') {
186            let tok = tok.trim();
187            if tok.is_empty() {
188                continue;
189            }
190            match tok.to_ascii_lowercase().as_str() {
191                "ctrl" | "control" => modifiers.ctrl = true,
192                "alt" | "option" => modifiers.alt = true,
193                "shift" => modifiers.shift = true,
194                "meta" | "win" | "cmd" | "super" | "command" => modifiers.meta = true,
195                other => {
196                    key =
197                        Some(Key::from_token(other).ok_or_else(|| format!("unknown key: {tok}"))?);
198                }
199            }
200        }
201        let key = key.ok_or_else(|| "hotkey requires a non-modifier key".to_string())?;
202        Ok(Hotkey { modifiers, key })
203    }
204}
205
206/// Callback invoked when a registered hotkey fires.
207pub trait HotkeyHandler: Send + Sync {
208    fn on_hotkey(&self, id: u32, hotkey: &Hotkey);
209}
210
211/// Platform-independent manager for global hotkeys.
212pub struct HotkeyManager {
213    handler: Arc<dyn HotkeyHandler>,
214    inner: Mutex<Option<PlatformHotkey>>,
215}
216
217impl HotkeyManager {
218    pub fn new(handler: Arc<dyn HotkeyHandler>) -> Self {
219        Self {
220            handler,
221            inner: Mutex::new(None),
222        }
223    }
224
225    /// Register a hotkey with the given application-defined `id`.
226    pub fn register(&self, id: u32, hotkey: &Hotkey) -> Result<()> {
227        let mut guard = self.inner.lock().unwrap();
228        match guard.as_ref() {
229            Some(p) => p.register(id, hotkey),
230            None => {
231                let p = PlatformHotkey::start(self.handler.clone())?;
232                p.register(id, hotkey)?;
233                *guard = Some(p);
234                Ok(())
235            }
236        }
237    }
238
239    /// Unregister a previously registered hotkey by `id`.
240    pub fn unregister(&self, id: u32) -> Result<()> {
241        if let Some(p) = self.inner.lock().unwrap().as_ref() {
242            p.unregister(id)?;
243        }
244        Ok(())
245    }
246}
247
248impl Drop for HotkeyManager {
249    fn drop(&mut self) {
250        if let Some(p) = self.inner.lock().unwrap().take() {
251            p.stop();
252        }
253    }
254}
255
256// ── Platform implementations ──────────────────────────────────────────────
257
258enum PlatformHotkey {
259    #[cfg(windows)]
260    Windows(crate::hotkeys_win::WinHotkey),
261    #[cfg(target_os = "macos")]
262    Macos(crate::hotkeys_mac::MacHotkey),
263    #[cfg(not(any(windows, target_os = "macos")))]
264    Unsupported,
265}
266
267impl PlatformHotkey {
268    fn start(handler: Arc<dyn HotkeyHandler>) -> Result<Self> {
269        #[cfg(windows)]
270        {
271            Ok(PlatformHotkey::Windows(
272                crate::hotkeys_win::WinHotkey::start(handler)?,
273            ))
274        }
275        #[cfg(target_os = "macos")]
276        {
277            Ok(PlatformHotkey::Macos(crate::hotkeys_mac::MacHotkey::start(
278                handler,
279            )?))
280        }
281        #[cfg(not(any(windows, target_os = "macos")))]
282        {
283            let _ = handler;
284            Err(crate::error::RdesktopError::UnsupportedPlatform(
285                "global hotkeys are only supported on Windows and macOS".into(),
286            ))
287        }
288    }
289
290    fn register(&self, id: u32, hotkey: &Hotkey) -> Result<()> {
291        match self {
292            #[cfg(windows)]
293            PlatformHotkey::Windows(w) => w.register(id, hotkey),
294            #[cfg(target_os = "macos")]
295            PlatformHotkey::Macos(m) => m.register(id, hotkey),
296            #[cfg(not(any(windows, target_os = "macos")))]
297            PlatformHotkey::Unsupported => Ok(()),
298        }
299    }
300
301    fn unregister(&self, id: u32) -> Result<()> {
302        match self {
303            #[cfg(windows)]
304            PlatformHotkey::Windows(w) => w.unregister(id),
305            #[cfg(target_os = "macos")]
306            PlatformHotkey::Macos(m) => m.unregister(id),
307            #[cfg(not(any(windows, target_os = "macos")))]
308            PlatformHotkey::Unsupported => Ok(()),
309        }
310    }
311
312    fn stop(self) {
313        match self {
314            #[cfg(windows)]
315            PlatformHotkey::Windows(w) => w.stop(),
316            #[cfg(target_os = "macos")]
317            PlatformHotkey::Macos(m) => m.stop(),
318            #[cfg(not(any(windows, target_os = "macos")))]
319            PlatformHotkey::Unsupported => {}
320        }
321    }
322}