pub use keyboard_types::{Code, Modifiers};
use std::{borrow::Borrow, fmt::Display, hash::Hash, str::FromStr};
#[cfg(target_os = "macos")]
pub const CMD_OR_CTRL: Modifiers = Modifiers::SUPER;
#[cfg(not(target_os = "macos"))]
pub const CMD_OR_CTRL: Modifiers = Modifiers::CONTROL;
#[derive(thiserror::Error, Debug)]
pub enum HotKeyParseError {
#[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")]
UnsupportedKey(String),
#[error("Found empty token while parsing hotkey: {0}")]
EmptyToken(String),
#[error("Invalid hotkey format: \"{0}\", an hotkey should have the modifiers first and only one main key, for example: \"Shift + Alt + K\"")]
InvalidFormat(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct HotKey {
pub mods: Modifiers,
pub key: Code,
pub id: u32,
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for HotKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let hotkey = String::deserialize(deserializer)?;
hotkey
.parse()
.map_err(|e: HotKeyParseError| serde::de::Error::custom(e.to_string()))
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for HotKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.to_string().serialize(serializer)
}
}
impl HotKey {
pub fn new(mods: Option<Modifiers>, key: Code) -> Self {
let mut mods = mods.unwrap_or_else(Modifiers::empty);
if mods.contains(Modifiers::META) {
mods.remove(Modifiers::META);
mods.insert(Modifiers::SUPER);
}
Self {
mods,
key,
id: mods.bits() << 16 | key as u32,
}
}
pub fn id(&self) -> u32 {
self.id
}
pub fn matches(&self, modifiers: impl Borrow<Modifiers>, key: impl Borrow<Code>) -> bool {
let base_mods = Modifiers::SHIFT | Modifiers::CONTROL | Modifiers::ALT | Modifiers::SUPER;
let modifiers = modifiers.borrow();
let key = key.borrow();
self.mods == *modifiers & base_mods && self.key == *key
}
pub fn into_string(self) -> String {
let mut hotkey = String::new();
if self.mods.contains(Modifiers::SHIFT) {
hotkey.push_str("shift+")
}
if self.mods.contains(Modifiers::CONTROL) {
hotkey.push_str("control+")
}
if self.mods.contains(Modifiers::ALT) {
hotkey.push_str("alt+")
}
if self.mods.contains(Modifiers::SUPER) {
hotkey.push_str("super+")
}
hotkey.push_str(&self.key.to_string());
hotkey
}
}
impl Display for HotKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.into_string())
}
}
impl FromStr for HotKey {
type Err = HotKeyParseError;
fn from_str(hotkey_string: &str) -> Result<Self, Self::Err> {
parse_hotkey(hotkey_string)
}
}
impl TryFrom<&str> for HotKey {
type Error = HotKeyParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
parse_hotkey(value)
}
}
impl TryFrom<String> for HotKey {
type Error = HotKeyParseError;
fn try_from(value: String) -> Result<Self, Self::Error> {
parse_hotkey(&value)
}
}
fn parse_hotkey(hotkey: &str) -> Result<HotKey, HotKeyParseError> {
let tokens = hotkey.split('+').collect::<Vec<&str>>();
let mut mods = Modifiers::empty();
let mut key = None;
match tokens.len() {
1 => {
key = Some(parse_key(tokens[0])?);
}
_ => {
for raw in tokens {
let token = raw.trim();
if token.is_empty() {
return Err(HotKeyParseError::EmptyToken(hotkey.to_string()));
}
if key.is_some() {
return Err(HotKeyParseError::InvalidFormat(hotkey.to_string()));
}
match token.to_uppercase().as_str() {
"OPTION" | "ALT" => {
mods |= Modifiers::ALT;
}
"CONTROL" | "CTRL" => {
mods |= Modifiers::CONTROL;
}
"COMMAND" | "CMD" | "SUPER" => {
mods |= Modifiers::SUPER;
}
"SHIFT" => {
mods |= Modifiers::SHIFT;
}
#[cfg(target_os = "macos")]
"COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => {
mods |= Modifiers::SUPER;
}
#[cfg(not(target_os = "macos"))]
"COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => {
mods |= Modifiers::CONTROL;
}
_ => {
key = Some(parse_key(token)?);
}
}
}
}
}
Ok(HotKey::new(
Some(mods),
key.ok_or_else(|| HotKeyParseError::InvalidFormat(hotkey.to_string()))?,
))
}
fn parse_key(key: &str) -> Result<Code, HotKeyParseError> {
use Code::*;
match key.to_uppercase().as_str() {
"BACKQUOTE" | "`" => Ok(Backquote),
"BACKSLASH" | "\\" => Ok(Backslash),
"BRACKETLEFT" | "[" => Ok(BracketLeft),
"BRACKETRIGHT" | "]" => Ok(BracketRight),
"COMMA" | "," => Ok(Comma),
"DIGIT0" | "0" => Ok(Digit0),
"DIGIT1" | "1" => Ok(Digit1),
"DIGIT2" | "2" => Ok(Digit2),
"DIGIT3" | "3" => Ok(Digit3),
"DIGIT4" | "4" => Ok(Digit4),
"DIGIT5" | "5" => Ok(Digit5),
"DIGIT6" | "6" => Ok(Digit6),
"DIGIT7" | "7" => Ok(Digit7),
"DIGIT8" | "8" => Ok(Digit8),
"DIGIT9" | "9" => Ok(Digit9),
"EQUAL" | "=" => Ok(Equal),
"KEYA" | "A" => Ok(KeyA),
"KEYB" | "B" => Ok(KeyB),
"KEYC" | "C" => Ok(KeyC),
"KEYD" | "D" => Ok(KeyD),
"KEYE" | "E" => Ok(KeyE),
"KEYF" | "F" => Ok(KeyF),
"KEYG" | "G" => Ok(KeyG),
"KEYH" | "H" => Ok(KeyH),
"KEYI" | "I" => Ok(KeyI),
"KEYJ" | "J" => Ok(KeyJ),
"KEYK" | "K" => Ok(KeyK),
"KEYL" | "L" => Ok(KeyL),
"KEYM" | "M" => Ok(KeyM),
"KEYN" | "N" => Ok(KeyN),
"KEYO" | "O" => Ok(KeyO),
"KEYP" | "P" => Ok(KeyP),
"KEYQ" | "Q" => Ok(KeyQ),
"KEYR" | "R" => Ok(KeyR),
"KEYS" | "S" => Ok(KeyS),
"KEYT" | "T" => Ok(KeyT),
"KEYU" | "U" => Ok(KeyU),
"KEYV" | "V" => Ok(KeyV),
"KEYW" | "W" => Ok(KeyW),
"KEYX" | "X" => Ok(KeyX),
"KEYY" | "Y" => Ok(KeyY),
"KEYZ" | "Z" => Ok(KeyZ),
"MINUS" | "-" => Ok(Minus),
"PERIOD" | "." => Ok(Period),
"QUOTE" | "'" => Ok(Quote),
"SEMICOLON" | ";" => Ok(Semicolon),
"SLASH" | "/" => Ok(Slash),
"BACKSPACE" => Ok(Backspace),
"CAPSLOCK" => Ok(CapsLock),
"ENTER" => Ok(Enter),
"SPACE" => Ok(Space),
"TAB" => Ok(Tab),
"DELETE" => Ok(Delete),
"END" => Ok(End),
"HOME" => Ok(Home),
"INSERT" => Ok(Insert),
"PAGEDOWN" => Ok(PageDown),
"PAGEUP" => Ok(PageUp),
"PRINTSCREEN" => Ok(PrintScreen),
"SCROLLLOCK" => Ok(ScrollLock),
"ARROWDOWN" | "DOWN" => Ok(ArrowDown),
"ARROWLEFT" | "LEFT" => Ok(ArrowLeft),
"ARROWRIGHT" | "RIGHT" => Ok(ArrowRight),
"ARROWUP" | "UP" => Ok(ArrowUp),
"NUMLOCK" => Ok(NumLock),
"NUMPAD0" | "NUM0" => Ok(Numpad0),
"NUMPAD1" | "NUM1" => Ok(Numpad1),
"NUMPAD2" | "NUM2" => Ok(Numpad2),
"NUMPAD3" | "NUM3" => Ok(Numpad3),
"NUMPAD4" | "NUM4" => Ok(Numpad4),
"NUMPAD5" | "NUM5" => Ok(Numpad5),
"NUMPAD6" | "NUM6" => Ok(Numpad6),
"NUMPAD7" | "NUM7" => Ok(Numpad7),
"NUMPAD8" | "NUM8" => Ok(Numpad8),
"NUMPAD9" | "NUM9" => Ok(Numpad9),
"NUMPADADD" | "NUMADD" | "NUMPADPLUS" | "NUMPLUS" => Ok(NumpadAdd),
"NUMPADDECIMAL" | "NUMDECIMAL" => Ok(NumpadDecimal),
"NUMPADDIVIDE" | "NUMDIVIDE" => Ok(NumpadDivide),
"NUMPADENTER" | "NUMENTER" => Ok(NumpadEnter),
"NUMPADEQUAL" | "NUMEQUAL" => Ok(NumpadEqual),
"NUMPADMULTIPLY" | "NUMMULTIPLY" => Ok(NumpadMultiply),
"NUMPADSUBTRACT" | "NUMSUBTRACT" => Ok(NumpadSubtract),
"ESCAPE" | "ESC" => Ok(Escape),
"F1" => Ok(F1),
"F2" => Ok(F2),
"F3" => Ok(F3),
"F4" => Ok(F4),
"F5" => Ok(F5),
"F6" => Ok(F6),
"F7" => Ok(F7),
"F8" => Ok(F8),
"F9" => Ok(F9),
"F10" => Ok(F10),
"F11" => Ok(F11),
"F12" => Ok(F12),
"AUDIOVOLUMEDOWN" | "VOLUMEDOWN" => Ok(AudioVolumeDown),
"AUDIOVOLUMEUP" | "VOLUMEUP" => Ok(AudioVolumeUp),
"AUDIOVOLUMEMUTE" | "VOLUMEMUTE" => Ok(AudioVolumeMute),
"MEDIAPLAY" => Ok(MediaPlay),
"MEDIAPAUSE" => Ok(MediaPause),
"MEDIAPLAYPAUSE" => Ok(MediaPlayPause),
"MEDIASTOP" => Ok(MediaStop),
"MEDIATRACKNEXT" => Ok(MediaTrackNext),
"MEDIATRACKPREV" | "MEDIATRACKPREVIOUS" => Ok(MediaTrackPrevious),
"F13" => Ok(F13),
"F14" => Ok(F14),
"F15" => Ok(F15),
"F16" => Ok(F16),
"F17" => Ok(F17),
"F18" => Ok(F18),
"F19" => Ok(F19),
"F20" => Ok(F20),
"F21" => Ok(F21),
"F22" => Ok(F22),
"F23" => Ok(F23),
"F24" => Ok(F24),
_ => Err(HotKeyParseError::UnsupportedKey(key.to_string())),
}
}
#[test]
fn test_parse_hotkey() {
macro_rules! assert_parse_hotkey {
($key:literal, $lrh:expr) => {
let r = parse_hotkey($key).unwrap();
let l = $lrh;
assert_eq!(r.mods, l.mods);
assert_eq!(r.key, l.key);
};
}
assert_parse_hotkey!(
"KeyX",
HotKey {
mods: Modifiers::empty(),
key: Code::KeyX,
id: 0,
}
);
assert_parse_hotkey!(
"CTRL+KeyX",
HotKey {
mods: Modifiers::CONTROL,
key: Code::KeyX,
id: 0,
}
);
assert_parse_hotkey!(
"SHIFT+KeyC",
HotKey {
mods: Modifiers::SHIFT,
key: Code::KeyC,
id: 0,
}
);
assert_parse_hotkey!(
"SHIFT+KeyC",
HotKey {
mods: Modifiers::SHIFT,
key: Code::KeyC,
id: 0,
}
);
assert_parse_hotkey!(
"super+ctrl+SHIFT+alt+ArrowUp",
HotKey {
mods: Modifiers::SUPER | Modifiers::CONTROL | Modifiers::SHIFT | Modifiers::ALT,
key: Code::ArrowUp,
id: 0,
}
);
assert_parse_hotkey!(
"Digit5",
HotKey {
mods: Modifiers::empty(),
key: Code::Digit5,
id: 0,
}
);
assert_parse_hotkey!(
"KeyG",
HotKey {
mods: Modifiers::empty(),
key: Code::KeyG,
id: 0,
}
);
assert_parse_hotkey!(
"SHiFT+F12",
HotKey {
mods: Modifiers::SHIFT,
key: Code::F12,
id: 0,
}
);
assert_parse_hotkey!(
"CmdOrCtrl+Space",
HotKey {
#[cfg(target_os = "macos")]
mods: Modifiers::SUPER,
#[cfg(not(target_os = "macos"))]
mods: Modifiers::CONTROL,
key: Code::Space,
id: 0,
}
);
if HotKey::from_str("Shift+Ctrl").is_ok() {
panic!("This is not a valid hotkey");
}
}
#[test]
fn test_equality() {
let h1 = parse_hotkey("Shift+KeyR").unwrap();
let h2 = parse_hotkey("Shift+KeyR").unwrap();
let h3 = HotKey::new(Some(Modifiers::SHIFT), Code::KeyR);
let h4 = parse_hotkey("Alt+KeyR").unwrap();
let h5 = parse_hotkey("Alt+KeyR").unwrap();
let h6 = parse_hotkey("KeyR").unwrap();
assert!(h1 == h2 && h2 == h3 && h3 != h4 && h4 == h5 && h5 != h6);
assert!(
h1.id() == h2.id()
&& h2.id() == h3.id()
&& h3.id() != h4.id()
&& h4.id() == h5.id()
&& h5.id() != h6.id()
);
}