Skip to main content

nex_core/
hotkey_runtime.rs

1use crate::hotkey::parse_hotkey;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum HotkeyRegistration {
5    Native(i32),
6    Noop(String),
7}
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum HotkeyRuntimeError {
11    InvalidHotkey(String),
12    RegistrationFailed(String),
13    EventLoopFailed(String),
14    UnsupportedPlatform,
15}
16
17pub trait HotkeyRegistrar: Send {
18    fn register_hotkey(&mut self, hotkey: &str) -> Result<HotkeyRegistration, HotkeyRuntimeError>;
19    fn unregister_all(&mut self) -> Result<(), HotkeyRuntimeError>;
20}
21
22#[derive(Default)]
23pub struct MockHotkeyRegistrar {
24    registrations: Vec<String>,
25}
26
27impl MockHotkeyRegistrar {
28    pub fn registrations(&self) -> &[String] {
29        &self.registrations
30    }
31}
32
33impl HotkeyRegistrar for MockHotkeyRegistrar {
34    fn register_hotkey(&mut self, hotkey: &str) -> Result<HotkeyRegistration, HotkeyRuntimeError> {
35        parse_hotkey(hotkey).map_err(HotkeyRuntimeError::InvalidHotkey)?;
36        self.registrations.push(hotkey.to_string());
37        Ok(HotkeyRegistration::Noop(hotkey.to_string()))
38    }
39
40    fn unregister_all(&mut self) -> Result<(), HotkeyRuntimeError> {
41        self.registrations.clear();
42        Ok(())
43    }
44}
45
46#[cfg(not(target_os = "windows"))]
47#[derive(Default)]
48pub struct NoopHotkeyRegistrar {
49    registrations: Vec<String>,
50}
51
52#[cfg(not(target_os = "windows"))]
53impl NoopHotkeyRegistrar {
54    pub fn registrations(&self) -> &[String] {
55        &self.registrations
56    }
57}
58
59#[cfg(not(target_os = "windows"))]
60impl HotkeyRegistrar for NoopHotkeyRegistrar {
61    fn register_hotkey(&mut self, hotkey: &str) -> Result<HotkeyRegistration, HotkeyRuntimeError> {
62        parse_hotkey(hotkey).map_err(HotkeyRuntimeError::InvalidHotkey)?;
63        self.registrations.push(hotkey.to_string());
64        Ok(HotkeyRegistration::Noop(hotkey.to_string()))
65    }
66
67    fn unregister_all(&mut self) -> Result<(), HotkeyRuntimeError> {
68        self.registrations.clear();
69        Ok(())
70    }
71}
72
73#[cfg(target_os = "windows")]
74pub struct WindowsHotkeyRegistrar {
75    next_id: i32,
76    registered_ids: Vec<i32>,
77}
78
79#[cfg(target_os = "windows")]
80impl Default for WindowsHotkeyRegistrar {
81    fn default() -> Self {
82        Self {
83            next_id: 1,
84            registered_ids: Vec::new(),
85        }
86    }
87}
88
89#[cfg(target_os = "windows")]
90impl HotkeyRegistrar for WindowsHotkeyRegistrar {
91    fn register_hotkey(&mut self, hotkey: &str) -> Result<HotkeyRegistration, HotkeyRuntimeError> {
92        use windows_sys::Win32::UI::Input::KeyboardAndMouse::{
93            RegisterHotKey, MOD_ALT, MOD_CONTROL, MOD_SHIFT, MOD_WIN, VK_F1, VK_F10, VK_F11,
94            VK_F12, VK_F2, VK_F3, VK_F4, VK_F5, VK_F6, VK_F7, VK_F8, VK_F9, VK_SPACE,
95        };
96
97        let parsed = parse_hotkey(hotkey).map_err(HotkeyRuntimeError::InvalidHotkey)?;
98
99        let mut modifiers = 0_u32;
100        for modifier in &parsed.modifiers {
101            match modifier.to_ascii_lowercase().as_str() {
102                "alt" => modifiers |= MOD_ALT,
103                "ctrl" | "control" => modifiers |= MOD_CONTROL,
104                "shift" => modifiers |= MOD_SHIFT,
105                "win" | "meta" | "super" => modifiers |= MOD_WIN,
106                _ => {
107                    return Err(HotkeyRuntimeError::InvalidHotkey(format!(
108                        "unsupported modifier: {modifier}"
109                    )))
110                }
111            }
112        }
113
114        let key_upper = parsed.key.to_ascii_uppercase();
115        let vk: u32 = match key_upper.as_str() {
116            "SPACE" => VK_SPACE as u32,
117            "F1" => VK_F1 as u32,
118            "F2" => VK_F2 as u32,
119            "F3" => VK_F3 as u32,
120            "F4" => VK_F4 as u32,
121            "F5" => VK_F5 as u32,
122            "F6" => VK_F6 as u32,
123            "F7" => VK_F7 as u32,
124            "F8" => VK_F8 as u32,
125            "F9" => VK_F9 as u32,
126            "F10" => VK_F10 as u32,
127            "F11" => VK_F11 as u32,
128            "F12" => VK_F12 as u32,
129            _ if key_upper.len() == 1 => key_upper.as_bytes()[0] as u32,
130            _ => {
131                return Err(HotkeyRuntimeError::InvalidHotkey(format!(
132                    "unsupported key: {}",
133                    parsed.key
134                )))
135            }
136        };
137
138        let id = self.next_id;
139        self.next_id += 1;
140
141        let ok = unsafe { RegisterHotKey(std::ptr::null_mut(), id, modifiers, vk) };
142        if ok == 0 {
143            return Err(HotkeyRuntimeError::RegistrationFailed(format!(
144                "RegisterHotKey failed for '{hotkey}'"
145            )));
146        }
147
148        self.registered_ids.push(id);
149        Ok(HotkeyRegistration::Native(id))
150    }
151
152    fn unregister_all(&mut self) -> Result<(), HotkeyRuntimeError> {
153        use windows_sys::Win32::UI::Input::KeyboardAndMouse::UnregisterHotKey;
154
155        for id in self.registered_ids.drain(..) {
156            unsafe {
157                UnregisterHotKey(std::ptr::null_mut(), id);
158            }
159        }
160        Ok(())
161    }
162}
163
164pub fn default_hotkey_registrar() -> Box<dyn HotkeyRegistrar> {
165    #[cfg(target_os = "windows")]
166    {
167        Box::new(WindowsHotkeyRegistrar::default())
168    }
169
170    #[cfg(not(target_os = "windows"))]
171    {
172        Box::new(NoopHotkeyRegistrar::default())
173    }
174}
175
176#[cfg(target_os = "windows")]
177pub fn run_message_loop<F>(mut on_hotkey: F) -> Result<(), HotkeyRuntimeError>
178where
179    F: FnMut(i32),
180{
181    use windows_sys::Win32::UI::WindowsAndMessaging::{
182        DispatchMessageW, GetMessageW, TranslateMessage, MSG, WM_HOTKEY,
183    };
184
185    let mut msg: MSG = unsafe { std::mem::zeroed() };
186    loop {
187        let status = unsafe { GetMessageW(&mut msg, std::ptr::null_mut(), 0, 0) };
188        if status == -1 {
189            return Err(HotkeyRuntimeError::EventLoopFailed(
190                "GetMessageW returned -1".to_string(),
191            ));
192        }
193
194        if status == 0 {
195            return Ok(());
196        }
197
198        if msg.message == WM_HOTKEY {
199            on_hotkey(msg.wParam as i32);
200        }
201
202        unsafe {
203            TranslateMessage(&msg);
204            DispatchMessageW(&msg);
205        }
206    }
207}
208
209#[cfg(not(target_os = "windows"))]
210pub fn run_message_loop<F>(_on_hotkey: F) -> Result<(), HotkeyRuntimeError>
211where
212    F: FnMut(i32),
213{
214    Err(HotkeyRuntimeError::UnsupportedPlatform)
215}