keyflow 0.1.0

Cross-platform input simulation library for keyboard, mouse and hotkeys.
Documentation
use crate::error::*;
use crate::types::{Key, KeyState};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex, RwLock};

/// Hotkey combination
#[derive(Eq, Hash, PartialEq, Clone, Debug)]
pub struct Hotkey {
    // TODO HashSet?
    pub modifiers: Vec<Key>,
    pub key: Key,
}

impl Hotkey {
    pub fn new(key: Key) -> Self {
        Self {
            modifiers: Vec::new(),
            key,
        }
    }

    /// Add [`Key::ControlLeft`] as modifier.
    pub fn with_ctrl(self) -> Self {
        self.add_modifier(Key::ControlLeft)
    }

    /// Add [`Key::ControlRight`] as modifier.
    pub fn with_ctrl_right(self) -> Self {
        self.add_modifier(Key::ControlRight)
    }

    /// Add [`Key::ShiftLeft`] as modifier.
    pub fn with_shift(self) -> Self {
        self.add_modifier(Key::ShiftLeft)
    }

    /// Add [`Key::ShiftRight`] as modifier.
    pub fn with_shift_right(self) -> Self {
        self.add_modifier(Key::ShiftRight)
    }

    /// Add [`Key::AltLeft`] as modifier.
    pub fn with_alt(self) -> Self {
        self.add_modifier(Key::AltLeft)
    }

    /// Add [`Key::AltRight`] as modifier.
    pub fn with_alt_right(self) -> Self {
        self.add_modifier(Key::AltRight)
    }

    /// Add [`Key::MetaLeft`] as modifier.
    pub fn with_meta(self) -> Self {
        self.add_modifier(Key::MetaLeft)
    }

    /// Add [`Key::MetaRight`] as modifier.
    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>;

/// Registry mapping hotkeys to callbacks
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)
    }
}

/// The HotkeyListener checks the pressed keys which get sent by the backend
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 the pressed key is a modifier, it gets added to the HashSet
        // doesn't check for Repeated yet
        if is_modifier(key) {
            if state == KeyState::Pressed {
                modifiers.insert(key);
            } else if state == KeyState::Released {
                modifiers.remove(&key);
            }
        }

        // Check for hotkey match on press
        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));
    }
}