1pub use keyboard_types::{Code, Modifiers};
31use std::{borrow::Borrow, fmt::Display, hash::Hash, str::FromStr};
32
33#[cfg(target_os = "macos")]
34pub const CMD_OR_CTRL: Modifiers = Modifiers::SUPER;
35#[cfg(not(target_os = "macos"))]
36pub const CMD_OR_CTRL: Modifiers = Modifiers::CONTROL;
37
38#[derive(thiserror::Error, Debug)]
39pub enum HotKeyParseError {
40 #[error("Couldn't recognize \"{0}\" as a valid key for hotkey, if you feel like it should be, please report this to https://github.com/tauri-apps/muda")]
41 UnsupportedKey(String),
42 #[error("Found empty token while parsing hotkey: {0}")]
43 EmptyToken(String),
44 #[error("Invalid hotkey format: \"{0}\", an hotkey should have the modifiers first and only one main key, for example: \"Shift + Alt + K\"")]
45 InvalidFormat(String),
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub struct HotKey {
53 pub mods: Modifiers,
55 pub key: Code,
57 pub id: u32,
59}
60
61#[cfg(feature = "serde")]
62impl<'de> serde::Deserialize<'de> for HotKey {
63 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
64 where
65 D: serde::Deserializer<'de>,
66 {
67 let hotkey = String::deserialize(deserializer)?;
68 hotkey
69 .parse()
70 .map_err(|e: HotKeyParseError| serde::de::Error::custom(e.to_string()))
71 }
72}
73
74#[cfg(feature = "serde")]
75impl serde::Serialize for HotKey {
76 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
77 where
78 S: serde::Serializer,
79 {
80 self.to_string().serialize(serializer)
81 }
82}
83
84impl HotKey {
85 pub fn new(mods: Option<Modifiers>, key: Code) -> Self {
88 let mut mods = mods.unwrap_or_else(Modifiers::empty);
89 if mods.contains(Modifiers::META) {
90 mods.remove(Modifiers::META);
91 mods.insert(Modifiers::SUPER);
92 }
93
94 Self {
95 mods,
96 key,
97 id: (mods.bits() << 16) | key as u32,
98 }
99 }
100
101 pub fn id(&self) -> u32 {
104 self.id
105 }
106
107 pub fn matches(&self, modifiers: impl Borrow<Modifiers>, key: impl Borrow<Code>) -> bool {
109 let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER;
111 let modifiers = modifiers.borrow();
112 let key = key.borrow();
113 self.mods == *modifiers & base_mods && self.key == *key
114 }
115
116 pub fn into_string(self) -> String {
118 let mut hotkey = String::new();
119 if self.mods.contains(Modifiers::SHIFT) {
120 hotkey.push_str("shift+")
121 }
122 if self.mods.contains(Modifiers::CONTROL) {
123 hotkey.push_str("control+")
124 }
125 if self.mods.contains(Modifiers::ALT) {
126 hotkey.push_str("alt+")
127 }
128 if self.mods.contains(Modifiers::SUPER) {
129 hotkey.push_str("super+")
130 }
131 hotkey.push_str(&self.key.to_string());
132 hotkey
133 }
134}
135
136impl Display for HotKey {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138 write!(f, "{}", self.into_string())
139 }
140}
141
142impl FromStr for HotKey {
146 type Err = HotKeyParseError;
147 fn from_str(hotkey_string: &str) -> Result<Self, Self::Err> {
148 parse_hotkey(hotkey_string)
149 }
150}
151
152impl TryFrom<&str> for HotKey {
153 type Error = HotKeyParseError;
154
155 fn try_from(value: &str) -> Result<Self, Self::Error> {
156 parse_hotkey(value)
157 }
158}
159
160impl TryFrom<String> for HotKey {
161 type Error = HotKeyParseError;
162
163 fn try_from(value: String) -> Result<Self, Self::Error> {
164 parse_hotkey(&value)
165 }
166}
167
168fn parse_hotkey(hotkey: &str) -> Result<HotKey, HotKeyParseError> {
169 let tokens = hotkey.split('+').collect::<Vec<&str>>();
170
171 let mut mods = Modifiers::empty();
172 let mut key = None;
173
174 match tokens.len() {
175 1 => {
177 key = Some(parse_key(tokens[0])?);
178 }
179 _ => {
181 for raw in tokens {
182 let token = raw.trim();
183
184 if token.is_empty() {
185 return Err(HotKeyParseError::EmptyToken(hotkey.to_string()));
186 }
187
188 if key.is_some() {
189 return Err(HotKeyParseError::InvalidFormat(hotkey.to_string()));
196 }
197
198 match token.to_uppercase().as_str() {
199 "OPTION" | "ALT" => {
200 mods |= Modifiers::ALT;
201 }
202 "CONTROL" | "CTRL" => {
203 mods |= Modifiers::CONTROL;
204 }
205 "COMMAND" | "CMD" | "SUPER" => {
206 mods |= Modifiers::SUPER;
207 }
208 "SHIFT" => {
209 mods |= Modifiers::SHIFT;
210 }
211 #[cfg(target_os = "macos")]
212 "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => {
213 mods |= Modifiers::SUPER;
214 }
215 #[cfg(not(target_os = "macos"))]
216 "COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => {
217 mods |= Modifiers::CONTROL;
218 }
219 _ => {
220 key = Some(parse_key(token)?);
221 }
222 }
223 }
224 }
225 }
226
227 Ok(HotKey::new(
228 Some(mods),
229 key.ok_or_else(|| HotKeyParseError::InvalidFormat(hotkey.to_string()))?,
230 ))
231}
232
233fn parse_key(key: &str) -> Result<Code, HotKeyParseError> {
234 use Code::*;
235 match key.to_uppercase().as_str() {
236 "BACKQUOTE" | "`" => Ok(Backquote),
237 "BACKSLASH" | "\\" => Ok(Backslash),
238 "BRACKETLEFT" | "[" => Ok(BracketLeft),
239 "BRACKETRIGHT" | "]" => Ok(BracketRight),
240 "PAUSE" | "PAUSEBREAK" => Ok(Pause),
241 "COMMA" | "," => Ok(Comma),
242 "DIGIT0" | "0" => Ok(Digit0),
243 "DIGIT1" | "1" => Ok(Digit1),
244 "DIGIT2" | "2" => Ok(Digit2),
245 "DIGIT3" | "3" => Ok(Digit3),
246 "DIGIT4" | "4" => Ok(Digit4),
247 "DIGIT5" | "5" => Ok(Digit5),
248 "DIGIT6" | "6" => Ok(Digit6),
249 "DIGIT7" | "7" => Ok(Digit7),
250 "DIGIT8" | "8" => Ok(Digit8),
251 "DIGIT9" | "9" => Ok(Digit9),
252 "EQUAL" | "=" => Ok(Equal),
253 "KEYA" | "A" => Ok(KeyA),
254 "KEYB" | "B" => Ok(KeyB),
255 "KEYC" | "C" => Ok(KeyC),
256 "KEYD" | "D" => Ok(KeyD),
257 "KEYE" | "E" => Ok(KeyE),
258 "KEYF" | "F" => Ok(KeyF),
259 "KEYG" | "G" => Ok(KeyG),
260 "KEYH" | "H" => Ok(KeyH),
261 "KEYI" | "I" => Ok(KeyI),
262 "KEYJ" | "J" => Ok(KeyJ),
263 "KEYK" | "K" => Ok(KeyK),
264 "KEYL" | "L" => Ok(KeyL),
265 "KEYM" | "M" => Ok(KeyM),
266 "KEYN" | "N" => Ok(KeyN),
267 "KEYO" | "O" => Ok(KeyO),
268 "KEYP" | "P" => Ok(KeyP),
269 "KEYQ" | "Q" => Ok(KeyQ),
270 "KEYR" | "R" => Ok(KeyR),
271 "KEYS" | "S" => Ok(KeyS),
272 "KEYT" | "T" => Ok(KeyT),
273 "KEYU" | "U" => Ok(KeyU),
274 "KEYV" | "V" => Ok(KeyV),
275 "KEYW" | "W" => Ok(KeyW),
276 "KEYX" | "X" => Ok(KeyX),
277 "KEYY" | "Y" => Ok(KeyY),
278 "KEYZ" | "Z" => Ok(KeyZ),
279 "MINUS" | "-" => Ok(Minus),
280 "PERIOD" | "." => Ok(Period),
281 "QUOTE" | "'" => Ok(Quote),
282 "SEMICOLON" | ";" => Ok(Semicolon),
283 "SLASH" | "/" => Ok(Slash),
284 "BACKSPACE" => Ok(Backspace),
285 "CAPSLOCK" => Ok(CapsLock),
286 "ENTER" => Ok(Enter),
287 "SPACE" => Ok(Space),
288 "TAB" => Ok(Tab),
289 "DELETE" => Ok(Delete),
290 "END" => Ok(End),
291 "HOME" => Ok(Home),
292 "INSERT" => Ok(Insert),
293 "PAGEDOWN" => Ok(PageDown),
294 "PAGEUP" => Ok(PageUp),
295 "PRINTSCREEN" => Ok(PrintScreen),
296 "SCROLLLOCK" => Ok(ScrollLock),
297 "ARROWDOWN" | "DOWN" => Ok(ArrowDown),
298 "ARROWLEFT" | "LEFT" => Ok(ArrowLeft),
299 "ARROWRIGHT" | "RIGHT" => Ok(ArrowRight),
300 "ARROWUP" | "UP" => Ok(ArrowUp),
301 "NUMLOCK" => Ok(NumLock),
302 "NUMPAD0" | "NUM0" => Ok(Numpad0),
303 "NUMPAD1" | "NUM1" => Ok(Numpad1),
304 "NUMPAD2" | "NUM2" => Ok(Numpad2),
305 "NUMPAD3" | "NUM3" => Ok(Numpad3),
306 "NUMPAD4" | "NUM4" => Ok(Numpad4),
307 "NUMPAD5" | "NUM5" => Ok(Numpad5),
308 "NUMPAD6" | "NUM6" => Ok(Numpad6),
309 "NUMPAD7" | "NUM7" => Ok(Numpad7),
310 "NUMPAD8" | "NUM8" => Ok(Numpad8),
311 "NUMPAD9" | "NUM9" => Ok(Numpad9),
312 "NUMPADADD" | "NUMADD" | "NUMPADPLUS" | "NUMPLUS" => Ok(NumpadAdd),
313 "NUMPADDECIMAL" | "NUMDECIMAL" => Ok(NumpadDecimal),
314 "NUMPADDIVIDE" | "NUMDIVIDE" => Ok(NumpadDivide),
315 "NUMPADENTER" | "NUMENTER" => Ok(NumpadEnter),
316 "NUMPADEQUAL" | "NUMEQUAL" => Ok(NumpadEqual),
317 "NUMPADMULTIPLY" | "NUMMULTIPLY" => Ok(NumpadMultiply),
318 "NUMPADSUBTRACT" | "NUMSUBTRACT" => Ok(NumpadSubtract),
319 "ESCAPE" | "ESC" => Ok(Escape),
320 "F1" => Ok(F1),
321 "F2" => Ok(F2),
322 "F3" => Ok(F3),
323 "F4" => Ok(F4),
324 "F5" => Ok(F5),
325 "F6" => Ok(F6),
326 "F7" => Ok(F7),
327 "F8" => Ok(F8),
328 "F9" => Ok(F9),
329 "F10" => Ok(F10),
330 "F11" => Ok(F11),
331 "F12" => Ok(F12),
332 "AUDIOVOLUMEDOWN" | "VOLUMEDOWN" => Ok(AudioVolumeDown),
333 "AUDIOVOLUMEUP" | "VOLUMEUP" => Ok(AudioVolumeUp),
334 "AUDIOVOLUMEMUTE" | "VOLUMEMUTE" => Ok(AudioVolumeMute),
335 "MEDIAPLAY" => Ok(MediaPlay),
336 "MEDIAPAUSE" => Ok(MediaPause),
337 "MEDIAPLAYPAUSE" => Ok(MediaPlayPause),
338 "MEDIASTOP" => Ok(MediaStop),
339 "MEDIATRACKNEXT" => Ok(MediaTrackNext),
340 "MEDIATRACKPREV" | "MEDIATRACKPREVIOUS" => Ok(MediaTrackPrevious),
341 "F13" => Ok(F13),
342 "F14" => Ok(F14),
343 "F15" => Ok(F15),
344 "F16" => Ok(F16),
345 "F17" => Ok(F17),
346 "F18" => Ok(F18),
347 "F19" => Ok(F19),
348 "F20" => Ok(F20),
349 "F21" => Ok(F21),
350 "F22" => Ok(F22),
351 "F23" => Ok(F23),
352 "F24" => Ok(F24),
353
354 _ => Err(HotKeyParseError::UnsupportedKey(key.to_string())),
355 }
356}
357
358#[test]
359fn test_parse_hotkey() {
360 macro_rules! assert_parse_hotkey {
361 ($key:literal, $lrh:expr) => {
362 let r = parse_hotkey($key).unwrap();
363 let l = $lrh;
364 assert_eq!(r.mods, l.mods);
365 assert_eq!(r.key, l.key);
366 };
367 }
368
369 assert_parse_hotkey!(
370 "KeyX",
371 HotKey {
372 mods: Modifiers::empty(),
373 key: Code::KeyX,
374 id: 0,
375 }
376 );
377
378 assert_parse_hotkey!(
379 "CTRL+KeyX",
380 HotKey {
381 mods: Modifiers::CONTROL,
382 key: Code::KeyX,
383 id: 0,
384 }
385 );
386
387 assert_parse_hotkey!(
388 "SHIFT+KeyC",
389 HotKey {
390 mods: Modifiers::SHIFT,
391 key: Code::KeyC,
392 id: 0,
393 }
394 );
395
396 assert_parse_hotkey!(
397 "SHIFT+KeyC",
398 HotKey {
399 mods: Modifiers::SHIFT,
400 key: Code::KeyC,
401 id: 0,
402 }
403 );
404
405 assert_parse_hotkey!(
406 "super+ctrl+SHIFT+alt+ArrowUp",
407 HotKey {
408 mods: Modifiers::SUPER | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT,
409 key: Code::ArrowUp,
410 id: 0,
411 }
412 );
413 assert_parse_hotkey!(
414 "Digit5",
415 HotKey {
416 mods: Modifiers::empty(),
417 key: Code::Digit5,
418 id: 0,
419 }
420 );
421 assert_parse_hotkey!(
422 "KeyG",
423 HotKey {
424 mods: Modifiers::empty(),
425 key: Code::KeyG,
426 id: 0,
427 }
428 );
429
430 assert_parse_hotkey!(
431 "SHiFT+F12",
432 HotKey {
433 mods: Modifiers::SHIFT,
434 key: Code::F12,
435 id: 0,
436 }
437 );
438
439 assert_parse_hotkey!(
440 "CmdOrCtrl+Space",
441 HotKey {
442 #[cfg(target_os = "macos")]
443 mods: Modifiers::SUPER,
444 #[cfg(not(target_os = "macos"))]
445 mods: Modifiers::CONTROL,
446 key: Code::Space,
447 id: 0,
448 }
449 );
450
451 if HotKey::from_str("Shift+Ctrl").is_ok() {
454 panic!("This is not a valid hotkey");
455 }
456}
457
458#[test]
459fn test_equality() {
460 let h1 = parse_hotkey("Shift+KeyR").unwrap();
461 let h2 = parse_hotkey("Shift+KeyR").unwrap();
462 let h3 = HotKey::new(Some(Modifiers::SHIFT), Code::KeyR);
463 let h4 = parse_hotkey("Alt+KeyR").unwrap();
464 let h5 = parse_hotkey("Alt+KeyR").unwrap();
465 let h6 = parse_hotkey("KeyR").unwrap();
466
467 assert!(h1 == h2 && h2 == h3 && h3 != h4 && h4 == h5 && h5 != h6);
468 assert!(
469 h1.id() == h2.id()
470 && h2.id() == h3.id()
471 && h3.id() != h4.id()
472 && h4.id() == h5.id()
473 && h5.id() != h6.id()
474 );
475}