Skip to main content

global_hotkey/
hotkey.rs

1// Copyright 2022-2022 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5//! HotKeys describe keyboard global shortcuts.
6//!
7//! [`HotKey`s](crate::hotkey::HotKey) are used to define a keyboard shortcut consisting
8//! of an optional combination of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and
9//! one key ([`Code`](crate::hotkey::Code)).
10//!
11//! # Examples
12//! They can be created directly
13//! ```no_run
14//! # use global_hotkey::hotkey::{HotKey, Modifiers, Code};
15//! let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyQ);
16//! let hotkey_without_mods = HotKey::new(None, Code::KeyQ);
17//! ```
18//! or from `&str`, note that all modifiers
19//! have to be listed before the non-modifier key, `shift+alt+KeyQ` is legal,
20//! whereas `shift+q+alt` is not.
21//! ```no_run
22//! # use global_hotkey::hotkey::{HotKey};
23//! let hotkey: HotKey = "shift+alt+KeyQ".parse().unwrap();
24//! # // This assert exists to ensure a test breaks once the
25//! # // statement above about ordering is no longer valid.
26//! # assert!("shift+KeyQ+alt".parse::<HotKey>().is_err());
27//! ```
28//!
29
30pub 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/// A keyboard shortcut that consists of an optional combination
49/// of modifier keys (provided by [`Modifiers`](crate::hotkey::Modifiers)) and
50/// one key ([`Code`](crate::hotkey::Code)).
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
52pub struct HotKey {
53    /// The hotkey modifiers.
54    pub mods: Modifiers,
55    /// The hotkey key.
56    pub key: Code,
57    /// The hotkey id.
58    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    /// Creates a new hotkey to define keyboard shortcuts throughout your application.
86    /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::SUPER`]
87    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    /// Returns the id associated with this hotKey
102    /// which is a hash of the string represention of modifiers and key within this hotKey.
103    pub fn id(&self) -> u32 {
104        self.id
105    }
106
107    /// Returns `true` if this [`Code`] and [`Modifiers`] matches this hotkey.
108    pub fn matches(&self, modifiers: impl Borrow<Modifiers>, key: impl Borrow<Code>) -> bool {
109        // Should be a const but const bit_or doesn't work here.
110        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    /// Converts this hotkey into a string.
117    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
142// HotKey::from_str is available to be backward
143// compatible with tauri and it also open the option
144// to generate hotkey from string
145impl 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        // single key hotkey
176        1 => {
177            key = Some(parse_key(tokens[0])?);
178        }
179        // modifiers and key comobo hotkey
180        _ => {
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                    // At this point we have parsed the modifiers and a main key, so by reaching
190                    // this code, the function either received more than one main key or
191                    //  the hotkey is not in the right order
192                    // examples:
193                    // 1. "Ctrl+Shift+C+A" => only one main key should be allowd.
194                    // 2. "Ctrl+C+Shift" => wrong order
195                    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    // Ensure that if it is just multiple modifiers, we do not panic.
452    // This would be a regression if this happened.
453    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}