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 n >= 1 && n <= 24 {
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 = Some(Key::from_token(other).ok_or_else(|| format!("unknown key: {tok}"))?);
197                }
198            }
199        }
200        let key = key.ok_or_else(|| "hotkey requires a non-modifier key".to_string())?;
201        Ok(Hotkey { modifiers, key })
202    }
203}
204
205/// Callback invoked when a registered hotkey fires.
206pub trait HotkeyHandler: Send + Sync {
207    fn on_hotkey(&self, id: u32, hotkey: &Hotkey);
208}
209
210/// Platform-independent manager for global hotkeys.
211pub struct HotkeyManager {
212    handler: Arc<dyn HotkeyHandler>,
213    inner: Mutex<Option<PlatformHotkey>>,
214}
215
216impl HotkeyManager {
217    pub fn new(handler: Arc<dyn HotkeyHandler>) -> Self {
218        Self {
219            handler,
220            inner: Mutex::new(None),
221        }
222    }
223
224    /// Register a hotkey with the given application-defined `id`.
225    pub fn register(&self, id: u32, hotkey: &Hotkey) -> Result<()> {
226        let mut guard = self.inner.lock().unwrap();
227        match guard.as_ref() {
228            Some(p) => p.register(id, hotkey),
229            None => {
230                let p = PlatformHotkey::start(self.handler.clone())?;
231                p.register(id, hotkey)?;
232                *guard = Some(p);
233                Ok(())
234            }
235        }
236    }
237
238    /// Unregister a previously registered hotkey by `id`.
239    pub fn unregister(&self, id: u32) -> Result<()> {
240        if let Some(p) = self.inner.lock().unwrap().as_ref() {
241            p.unregister(id)?;
242        }
243        Ok(())
244    }
245}
246
247impl Drop for HotkeyManager {
248    fn drop(&mut self) {
249        if let Some(p) = self.inner.lock().unwrap().take() {
250            p.stop();
251        }
252    }
253}
254
255// ── Platform implementations ──────────────────────────────────────────────
256
257enum PlatformHotkey {
258    #[cfg(windows)]
259    Windows(crate::hotkeys_win::WinHotkey),
260    #[cfg(target_os = "macos")]
261    Macos(crate::hotkeys_mac::MacHotkey),
262    #[cfg(not(any(windows, target_os = "macos")))]
263    Unsupported,
264}
265
266impl PlatformHotkey {
267    fn start(handler: Arc<dyn HotkeyHandler>) -> Result<Self> {
268        #[cfg(windows)]
269        {
270            Ok(PlatformHotkey::Windows(crate::hotkeys_win::WinHotkey::start(handler)?))
271        }
272        #[cfg(target_os = "macos")]
273        {
274            Ok(PlatformHotkey::Macos(crate::hotkeys_mac::MacHotkey::start(handler)?))
275        }
276        #[cfg(not(any(windows, target_os = "macos")))]
277        {
278            let _ = handler;
279            Err(crate::error::RdesktopError::UnsupportedPlatform(
280                "global hotkeys are only supported on Windows and macOS".into(),
281            ))
282        }
283    }
284
285    fn register(&self, id: u32, hotkey: &Hotkey) -> Result<()> {
286        match self {
287            #[cfg(windows)]
288            PlatformHotkey::Windows(w) => w.register(id, hotkey),
289            #[cfg(target_os = "macos")]
290            PlatformHotkey::Macos(m) => m.register(id, hotkey),
291            #[cfg(not(any(windows, target_os = "macos")))]
292            PlatformHotkey::Unsupported => Ok(()),
293        }
294    }
295
296    fn unregister(&self, id: u32) -> Result<()> {
297        match self {
298            #[cfg(windows)]
299            PlatformHotkey::Windows(w) => w.unregister(id),
300            #[cfg(target_os = "macos")]
301            PlatformHotkey::Macos(m) => m.unregister(id),
302            #[cfg(not(any(windows, target_os = "macos")))]
303            PlatformHotkey::Unsupported => Ok(()),
304        }
305    }
306
307    fn stop(self) {
308        match self {
309            #[cfg(windows)]
310            PlatformHotkey::Windows(w) => w.stop(),
311            #[cfg(target_os = "macos")]
312            PlatformHotkey::Macos(m) => m.stop(),
313            #[cfg(not(any(windows, target_os = "macos")))]
314            PlatformHotkey::Unsupported => {}
315        }
316    }
317}