use crate::error::*;
use crate::types::{Key, KeyState};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex, RwLock};
#[derive(Eq, Hash, PartialEq, Clone, Debug)]
pub struct Hotkey {
pub modifiers: Vec<Key>,
pub key: Key,
}
impl Hotkey {
pub fn new(key: Key) -> Self {
Self {
modifiers: Vec::new(),
key,
}
}
pub fn with_ctrl(self) -> Self {
self.add_modifier(Key::ControlLeft)
}
pub fn with_ctrl_right(self) -> Self {
self.add_modifier(Key::ControlRight)
}
pub fn with_shift(self) -> Self {
self.add_modifier(Key::ShiftLeft)
}
pub fn with_shift_right(self) -> Self {
self.add_modifier(Key::ShiftRight)
}
pub fn with_alt(self) -> Self {
self.add_modifier(Key::AltLeft)
}
pub fn with_alt_right(self) -> Self {
self.add_modifier(Key::AltRight)
}
pub fn with_meta(self) -> Self {
self.add_modifier(Key::MetaLeft)
}
pub fn with_meta_right(self) -> Self {
self.add_modifier(Key::MetaRight)
}
fn add_modifier(mut self, modifier: Key) -> Self {
if !self.modifiers.contains(&modifier) {
self.modifiers.push(modifier);
}
self
}
}
pub type HotkeyId = String;
pub type HotkeyCallback = Box<dyn Fn() + Send + Sync>;
pub(crate) struct HotkeyRegistry {
hotkeys: HashMap<Hotkey, (HotkeyId, HotkeyCallback)>,
ids: HashMap<HotkeyId, Hotkey>,
}
impl Default for HotkeyRegistry {
fn default() -> Self {
Self::new()
}
}
impl HotkeyRegistry {
pub(crate) fn new() -> Self {
Self {
hotkeys: HashMap::new(),
ids: HashMap::new(),
}
}
pub(crate) fn register(
&mut self,
hotkey: Hotkey,
id: HotkeyId,
callback: HotkeyCallback,
) -> Result<()> {
if self.hotkeys.contains_key(&hotkey) {
return Err(KeyflowError::HotkeyExists(hotkey));
}
if self.ids.contains_key(&id) {
return Err(KeyflowError::HotkeyIdExists(id));
}
self.hotkeys.insert(hotkey.clone(), (id.clone(), callback));
self.ids.insert(id, hotkey);
Ok(())
}
pub(crate) fn unregister(&mut self, id: &str) -> Result<()> {
if let Some(combo) = self.ids.remove(id) {
self.hotkeys.remove(&combo);
Ok(())
} else {
Err(KeyflowError::HotkeyNotFound(id.to_string()))
}
}
pub(crate) fn get_callback(&self, hotkey: &Hotkey) -> Option<&HotkeyCallback> {
self.hotkeys.get(hotkey).map(|(_, cb)| cb)
}
#[cfg(test)]
pub(crate) fn contains_id(&self, id: &str) -> bool {
self.ids.contains_key(id)
}
#[cfg(test)]
pub(crate) fn contains_combo(&self, combo: &Hotkey) -> bool {
self.hotkeys.contains_key(combo)
}
}
pub(crate) struct HotkeyListener {
registry: Arc<RwLock<HotkeyRegistry>>,
active_modifiers: Mutex<HashSet<Key>>,
pub(crate) running: Arc<AtomicBool>,
}
impl HotkeyListener {
pub fn new(registry: Arc<RwLock<HotkeyRegistry>>) -> Self {
Self {
registry,
active_modifiers: Mutex::new(HashSet::new()),
running: Arc::new(AtomicBool::new(true)),
}
}
pub fn on_key_event(&self, key: Key, state: KeyState) {
let mut modifiers = self.active_modifiers.lock().unwrap();
if is_modifier(key) {
if state == KeyState::Pressed {
modifiers.insert(key);
} else if state == KeyState::Released {
modifiers.remove(&key);
}
}
if state == KeyState::Pressed && !is_modifier(key) {
let combo_modifiers: Vec<Key> = modifiers.iter().copied().collect();
let combo = Hotkey {
modifiers: combo_modifiers,
key,
};
if let Some(callback) = self.registry.read().unwrap().get_callback(&combo) {
callback()
}
}
}
}
fn is_modifier(key: Key) -> bool {
matches!(
key,
Key::ShiftLeft
| Key::ShiftRight
| Key::ControlLeft
| Key::ControlRight
| Key::AltLeft
| Key::AltRight
| Key::MetaLeft
| Key::MetaRight
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Key;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[test]
fn test_registration() {
let mut registry = HotkeyRegistry::new();
let hk = Hotkey::new(Key::A).with_ctrl();
let called = Arc::new(AtomicBool::new(false));
let c = called.clone();
let result = registry.register(
hk.clone(),
"test-hotkey".into(),
Box::new(move || {
c.store(true, Ordering::Relaxed);
}),
);
assert!(result.is_ok());
assert!(registry.contains_id("test-hotkey"));
assert!(registry.contains_combo(&hk));
}
#[test]
fn test_duplicate_id() {
let mut registry = HotkeyRegistry::new();
let hk1 = Hotkey::new(Key::A).with_ctrl();
let hk2 = Hotkey::new(Key::B).with_ctrl_right();
registry
.register(hk1, "duplicate".into(), Box::new(|| {}))
.unwrap();
let result = registry.register(hk2, "duplicate".into(), Box::new(|| {}));
assert!(result.is_err());
}
#[test]
fn test_duplicate_hotkey() {
let mut registry = HotkeyRegistry::new();
let hotkey = Hotkey::new(Key::A).with_ctrl();
registry
.register(hotkey.clone(), "first".into(), Box::new(|| {}))
.unwrap();
let result = registry.register(hotkey, "second".into(), Box::new(|| {}));
assert!(result.is_err());
}
#[test]
fn test_unregistration() {
let mut registry = HotkeyRegistry::new();
let hotkey = Hotkey::new(Key::A).with_ctrl();
registry
.register(hotkey.clone(), "test".into(), Box::new(|| {}))
.unwrap();
assert!(registry.contains_id("test"));
registry.unregister("test").unwrap();
assert!(!registry.contains_id("test"));
assert!(!registry.contains_combo(&hotkey));
}
#[test]
fn test_unregister_nonexistent() {
let mut registry = HotkeyRegistry::new();
let result = registry.unregister("nonexistent");
assert!(result.is_err());
}
#[test]
fn test_combo_matching() {
let mut registry = HotkeyRegistry::new();
let called = Arc::new(AtomicBool::new(false));
let c = called.clone();
let hotkey = Hotkey::new(Key::S).with_ctrl().with_shift();
registry
.register(
hotkey.clone(),
"save".into(),
Box::new(move || {
c.store(true, Ordering::Relaxed);
}),
)
.unwrap();
if let Some(callback) = registry.get_callback(&hotkey) {
callback();
}
assert!(called.load(Ordering::Relaxed));
}
}