use rdev::{Event, EventType, Key};
use std::collections::HashSet;
use std::sync::{Arc, Mutex, MutexGuard};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum HotkeyParseError {
#[error("Empty hotkey string")]
Empty,
#[error("No main key specified in hotkey")]
NoMainKey,
#[error("Unknown key: {0}")]
UnknownKey(String),
}
pub fn lock_or_recover<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
mutex
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn create_grab_callback<FPress, FRelease>(
hotkey: Hotkey,
on_trigger: FPress,
on_release: FRelease,
) -> impl Fn(Event) -> Option<Event> + Send
where
FPress: Fn() + Send + 'static,
FRelease: Fn() + Send + 'static,
{
let pressed_keys: Arc<Mutex<HashSet<Key>>> = Arc::new(Mutex::new(HashSet::new()));
let hotkey_triggered: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
let main_key = hotkey.key;
move |event: Event| -> Option<Event> {
match event.event_type {
EventType::KeyPress(key) => {
let mut keys = lock_or_recover(&pressed_keys);
keys.insert(key);
let mut triggered = lock_or_recover(&hotkey_triggered);
if *triggered {
return Some(event); }
if hotkey.is_pressed(&keys) {
*triggered = true;
on_trigger();
return None; }
Some(event)
}
EventType::KeyRelease(key) => {
let mut keys = lock_or_recover(&pressed_keys);
keys.remove(&key);
if key == main_key {
let mut triggered = lock_or_recover(&hotkey_triggered);
if *triggered {
*triggered = false;
on_release();
}
}
Some(event)
}
_ => Some(event),
}
}
}
macro_rules! key_mappings {
($input:expr; $($name:pat => $key:ident),* $(,)?) => {
match $input {
$($name => Ok(Key::$key),)*
other => Err(HotkeyParseError::UnknownKey(other.to_string())),
}
};
}
macro_rules! key_to_str {
($key:expr; $($variant:ident => $name:expr),* $(,)?) => {
match $key {
$(Key::$variant => $name,)*
_ => "?",
}
};
}
#[derive(Debug, Clone)]
pub struct Hotkey {
pub ctrl: bool,
pub shift: bool,
pub alt: bool,
pub super_key: bool,
pub key: Key,
}
impl Hotkey {
pub fn parse(s: &str) -> Result<Self, HotkeyParseError> {
let lower = s.to_lowercase();
let parts: Vec<&str> = lower.split('+').map(|p| p.trim()).collect();
if parts.is_empty() {
return Err(HotkeyParseError::Empty);
}
let mut ctrl = false;
let mut shift = false;
let mut alt = false;
let mut super_key = false;
let mut main_key: Option<Key> = None;
for part in parts {
match part {
"ctrl" | "control" => ctrl = true,
"shift" => shift = true,
"alt" | "option" => alt = true,
"super" | "meta" | "win" | "cmd" => super_key = true,
key_str => {
main_key = Some(parse_key(key_str)?);
}
}
}
let key = main_key.ok_or(HotkeyParseError::NoMainKey)?;
Ok(Hotkey {
ctrl,
shift,
alt,
super_key,
key,
})
}
pub fn is_pressed(&self, pressed: &HashSet<Key>) -> bool {
let ctrl_ok = !self.ctrl
|| pressed.contains(&Key::ControlLeft)
|| pressed.contains(&Key::ControlRight);
let shift_ok =
!self.shift || pressed.contains(&Key::ShiftLeft) || pressed.contains(&Key::ShiftRight);
let alt_ok = !self.alt || pressed.contains(&Key::Alt);
let super_ok = !self.super_key
|| pressed.contains(&Key::MetaLeft)
|| pressed.contains(&Key::MetaRight);
let key_ok = pressed.contains(&self.key);
ctrl_ok && shift_ok && alt_ok && super_ok && key_ok
}
pub fn to_normalized_string(&self) -> String {
let mut parts = Vec::new();
if self.ctrl {
parts.push("Ctrl");
}
if self.alt {
parts.push("Alt");
}
if self.shift {
parts.push("Shift");
}
if self.super_key {
parts.push("Super");
}
parts.push(key_to_string(&self.key));
parts.join("+")
}
}
pub fn parse_key(s: &str) -> Result<Key, HotkeyParseError> {
let s = if s.starts_with("key") && s.len() == 4 {
&s[3..] } else {
s
};
key_mappings!(s;
"a" => KeyA, "b" => KeyB, "c" => KeyC, "d" => KeyD, "e" => KeyE,
"f" => KeyF, "g" => KeyG, "h" => KeyH, "i" => KeyI, "j" => KeyJ,
"k" => KeyK, "l" => KeyL, "m" => KeyM, "n" => KeyN, "o" => KeyO,
"p" => KeyP, "q" => KeyQ, "r" => KeyR, "s" => KeyS, "t" => KeyT,
"u" => KeyU, "v" => KeyV, "w" => KeyW, "x" => KeyX, "y" => KeyY,
"z" => KeyZ,
"0" => Num0, "1" => Num1, "2" => Num2, "3" => Num3, "4" => Num4,
"5" => Num5, "6" => Num6, "7" => Num7, "8" => Num8, "9" => Num9,
"f1" => F1, "f2" => F2, "f3" => F3, "f4" => F4, "f5" => F5,
"f6" => F6, "f7" => F7, "f8" => F8, "f9" => F9, "f10" => F10,
"f11" => F11, "f12" => F12,
"space" => Space,
"enter" | "return" => Return,
"escape" | "esc" => Escape,
"tab" => Tab,
"backspace" => Backspace,
"delete" | "del" => Delete,
"insert" | "ins" => Insert,
"home" => Home,
"end" => End,
"pageup" | "pgup" => PageUp,
"pagedown" | "pgdn" => PageDown,
"up" => UpArrow,
"down" => DownArrow,
"left" => LeftArrow,
"right" => RightArrow,
)
}
pub fn key_to_string(key: &Key) -> &'static str {
key_to_str!(key;
KeyA => "A", KeyB => "B", KeyC => "C", KeyD => "D", KeyE => "E",
KeyF => "F", KeyG => "G", KeyH => "H", KeyI => "I", KeyJ => "J",
KeyK => "K", KeyL => "L", KeyM => "M", KeyN => "N", KeyO => "O",
KeyP => "P", KeyQ => "Q", KeyR => "R", KeyS => "S", KeyT => "T",
KeyU => "U", KeyV => "V", KeyW => "W", KeyX => "X", KeyY => "Y",
KeyZ => "Z",
Num0 => "0", Num1 => "1", Num2 => "2", Num3 => "3", Num4 => "4",
Num5 => "5", Num6 => "6", Num7 => "7", Num8 => "8", Num9 => "9",
F1 => "F1", F2 => "F2", F3 => "F3", F4 => "F4", F5 => "F5",
F6 => "F6", F7 => "F7", F8 => "F8", F9 => "F9", F10 => "F10",
F11 => "F11", F12 => "F12",
Space => "Space",
Return => "Enter",
Escape => "Escape",
Tab => "Tab",
Backspace => "Backspace",
Delete => "Delete",
Insert => "Insert",
Home => "Home",
End => "End",
PageUp => "PageUp",
PageDown => "PageDown",
UpArrow => "Up",
DownArrow => "Down",
LeftArrow => "Left",
RightArrow => "Right",
)
}