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