1use alloc::string::String;
36use alloc::vec::Vec;
37use core::sync::atomic::{AtomicBool, Ordering};
38
39use rt::sync::SpinLock;
40
41use crate::{MOD_ALT, MOD_CTRL, MOD_SHIFT, MOD_SUPER};
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
46pub struct Chord {
47 pub vk: u32,
48 pub mods: u8,
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
53#[non_exhaustive]
54pub enum HotkeyError {
55 Unavailable,
57 Denied,
59 Invalid,
61 Conflict,
63 Dismissed,
65 Failed(String),
67}
68
69impl core::fmt::Display for HotkeyError {
70 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
71 match self {
72 HotkeyError::Unavailable => f.write_str("global shortcuts are not available here"),
73 HotkeyError::Denied => f.write_str("the app's permissions do not allow global shortcuts"),
74 HotkeyError::Invalid => f.write_str("invalid shortcut"),
75 HotkeyError::Conflict => f.write_str("the shortcut is taken"),
76 HotkeyError::Dismissed => f.write_str("the shortcut was not bound"),
77 HotkeyError::Failed(s) => write!(f, "global shortcut: {s}"),
78 }
79 }
80}
81
82#[derive(Clone, Debug, PartialEq, Eq)]
84pub enum HotkeyEvent {
85 Pressed(u32),
87 Refused(u32, HotkeyError),
89}
90
91static KEYS: &[(&str, u32, &str)] = &[
93 ("space", 0x20, "space"),
94 ("enter", 0x0D, "Return"),
95 ("return", 0x0D, "Return"),
96 ("tab", 0x09, "Tab"),
97 ("esc", 0x1B, "Escape"),
98 ("escape", 0x1B, "Escape"),
99 ("backspace", 0x08, "BackSpace"),
100 ("delete", 0x2E, "Delete"),
101 ("del", 0x2E, "Delete"),
102 ("insert", 0x2D, "Insert"),
103 ("ins", 0x2D, "Insert"),
104 ("home", 0x24, "Home"),
105 ("end", 0x23, "End"),
106 ("pageup", 0x21, "Page_Up"),
107 ("pgup", 0x21, "Page_Up"),
108 ("pagedown", 0x22, "Page_Down"),
109 ("pgdn", 0x22, "Page_Down"),
110 ("left", 0x25, "Left"),
111 ("up", 0x26, "Up"),
112 ("right", 0x27, "Right"),
113 ("down", 0x28, "Down"),
114 ("print", 0x2C, "Print"),
115 ("pause", 0x13, "Pause"),
116 ("numadd", 0x6B, "KP_Add"),
117 ("numsub", 0x6D, "KP_Subtract"),
118 ("nummul", 0x6A, "KP_Multiply"),
119 ("numdiv", 0x6F, "KP_Divide"),
120 ("numdec", 0x6E, "KP_Decimal"),
121 ("minus", 0xBD, "minus"),
122 ("plus", 0xBB, "plus"),
123 ("comma", 0xBC, "comma"),
124 ("period", 0xBE, "period"),
125];
126
127fn key_display(vk: u32) -> Option<String> {
129 let s = match vk {
130 0x41..=0x5A | 0x30..=0x39 => return Some(String::from(char::from(vk as u8))),
131 0x70..=0x87 => return Some(alloc::format!("F{}", vk - 0x6F)),
132 0x60..=0x69 => return Some(alloc::format!("Num{}", vk - 0x60)),
133 0x20 => "Space",
134 0x0D => "Enter",
135 0x09 => "Tab",
136 0x1B => "Esc",
137 0x08 => "Backspace",
138 0x2E => "Delete",
139 0x2D => "Insert",
140 0x24 => "Home",
141 0x23 => "End",
142 0x21 => "PageUp",
143 0x22 => "PageDown",
144 0x25 => "Left",
145 0x26 => "Up",
146 0x27 => "Right",
147 0x28 => "Down",
148 0x2C => "Print",
149 0x13 => "Pause",
150 0x6B => "NumAdd",
151 0x6D => "NumSub",
152 0x6A => "NumMul",
153 0x6F => "NumDiv",
154 0x6E => "NumDec",
155 0xBD => "Minus",
156 0xBB => "Plus",
157 0xBC => "Comma",
158 0xBE => "Period",
159 _ => return None,
160 };
161 Some(String::from(s))
162}
163
164impl Chord {
165 pub fn parse(s: &str) -> Option<Chord> {
168 let mut c = Chord::default();
169 for part in s.split('+') {
170 let p = part.trim().to_ascii_lowercase();
171 if p.is_empty() {
172 return None;
173 }
174 let m = match p.as_str() {
175 "ctrl" | "control" | "strg" => MOD_CTRL,
176 "alt" => MOD_ALT,
177 "shift" => MOD_SHIFT,
178 "super" | "win" | "meta" | "logo" | "cmd" => MOD_SUPER,
179 _ => 0,
180 };
181 if m != 0 {
182 c.mods |= m;
183 continue;
184 }
185 if c.vk != 0 {
186 return None; }
188 let b = p.as_bytes();
189 c.vk = if b.len() == 1 && b[0].is_ascii_alphanumeric() {
190 b[0].to_ascii_uppercase() as u32
191 } else if let Some(n) = p.strip_prefix('f').and_then(|n| n.parse::<u32>().ok()).filter(|n| (1..=24).contains(n)) {
192 0x6F + n
193 } else if let Some(n) = p.strip_prefix("num").and_then(|n| n.parse::<u32>().ok()).filter(|n| *n <= 9) {
194 0x60 + n
195 } else {
196 KEYS.iter().find(|k| k.0 == p).map(|k| k.1)?
197 };
198 }
199 (c.vk != 0).then_some(c)
200 }
201
202 pub fn text(&self) -> String {
204 let mut s = String::new();
205 for (m, n) in [(MOD_CTRL, "Ctrl+"), (MOD_ALT, "Alt+"), (MOD_SHIFT, "Shift+"), (MOD_SUPER, "Super+")] {
206 if self.mods & m != 0 {
207 s.push_str(n);
208 }
209 }
210 s.push_str(&key_display(self.vk).unwrap_or_else(|| alloc::format!("0x{:02X}", self.vk)));
211 s
212 }
213
214 pub fn trigger(&self) -> Option<String> {
217 let key = match self.vk {
218 0x41..=0x5A => String::from(char::from(self.vk as u8 + 32)),
219 0x30..=0x39 => String::from(char::from(self.vk as u8)),
220 0x70..=0x87 => alloc::format!("F{}", self.vk - 0x6F),
221 0x60..=0x69 => alloc::format!("KP_{}", self.vk - 0x60),
222 v => String::from(KEYS.iter().find(|k| k.1 == v)?.2),
223 };
224 let mut s = String::new();
225 for (m, n) in [(MOD_CTRL, "CTRL+"), (MOD_ALT, "ALT+"), (MOD_SHIFT, "SHIFT+"), (MOD_SUPER, "LOGO+")] {
226 if self.mods & m != 0 {
227 s.push_str(n);
228 }
229 }
230 s.push_str(&key);
231 Some(s)
232 }
233}
234
235static REG: SpinLock<Vec<(u32, Chord, String)>> = SpinLock::new(Vec::new());
237static EVENTS: SpinLock<Vec<HotkeyEvent>> = SpinLock::new(Vec::new());
238static DRY: AtomicBool = AtomicBool::new(false);
239const EVENTS_MAX: usize = 64;
240
241#[allow(dead_code)]
243pub(crate) fn push(e: HotkeyEvent) {
244 {
245 let mut q = EVENTS.lock();
246 if q.len() == EVENTS_MAX {
247 q.remove(0);
248 }
249 q.push(e);
250 }
251 crate::wake();
252}
253
254#[allow(dead_code)]
256pub(crate) fn refused(id: u32, why: HotkeyError) {
257 REG.lock().retain(|r| r.0 != id);
258 push(HotkeyEvent::Refused(id, why));
259}
260
261#[allow(dead_code)]
263pub(crate) fn registered(id: u32) -> bool {
264 REG.lock().iter().any(|r| r.0 == id)
265}
266
267pub fn hotkey_take() -> Vec<HotkeyEvent> {
269 core::mem::take(&mut *EVENTS.lock())
270}
271
272pub fn set_dry_run(on: bool) {
274 DRY.store(on, Ordering::Relaxed);
275}
276
277fn dry_run() -> bool {
278 DRY.load(Ordering::Relaxed)
279}
280
281pub fn hotkey_available() -> bool {
283 dry_run() || native::available()
284}
285
286pub fn hotkey_register(id: u32, chord: Chord, description: &str) -> Result<(), HotkeyError> {
289 if id == 0 || chord.vk == 0 || chord.vk > 0xFF || description.contains('\0') || chord.trigger().is_none() && !cfg!(windows) {
290 return Err(HotkeyError::Invalid);
291 }
292 if !crate::policy::input_global_allowed() {
293 return Err(HotkeyError::Denied);
294 }
295 if REG.lock().iter().any(|r| r.0 != id && r.1 == chord) {
296 return Err(HotkeyError::Conflict); }
298 if REG.lock().iter().any(|r| r.0 == id) {
299 hotkey_unregister(id);
300 }
301 REG.lock().push((id, chord, String::from(description)));
303 if !dry_run() {
304 if let Err(e) = native::register(id, chord, description) {
305 REG.lock().retain(|r| r.0 != id);
306 return Err(e);
307 }
308 }
309 Ok(())
310}
311
312pub fn hotkey_unregister(id: u32) -> bool {
314 let had = {
315 let mut r = REG.lock();
316 let n = r.len();
317 r.retain(|e| e.0 != id);
318 r.len() != n
319 };
320 if had && !dry_run() {
321 native::unregister(id);
322 }
323 had
324}
325
326pub fn hotkey_clear() {
328 let ids: Vec<u32> = REG.lock().iter().map(|r| r.0).collect();
329 for id in ids {
330 hotkey_unregister(id);
331 }
332}
333
334pub fn hotkey_list() -> Vec<(u32, Chord)> {
336 REG.lock().iter().map(|r| (r.0, r.1)).collect()
337}
338
339#[allow(dead_code)]
341pub(crate) fn hotkey_entries() -> Vec<(u32, Chord, String)> {
342 REG.lock().clone()
343}
344
345pub fn hotkey_test_press(id: u32) -> bool {
348 if !registered(id) {
349 return false;
350 }
351 push(HotkeyEvent::Pressed(id));
352 true
353}
354
355#[cfg(windows)]
356use crate::win_hotkey as native;
357
358#[cfg(target_os = "linux")]
359use crate::linux_hotkey as native;
360
361#[cfg(target_os = "macos")]
362use crate::apple::mac::services::hotkey as native;
363
364#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
365mod native {
366 use super::{Chord, HotkeyError};
367 pub fn available() -> bool {
368 false
369 }
370 pub fn register(_id: u32, _c: Chord, _d: &str) -> Result<(), HotkeyError> {
371 Err(HotkeyError::Unavailable)
372 }
373 pub fn unregister(_id: u32) {}
374}