use std::collections::{HashMap, HashSet};
use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use crate::error::{Error, Result};
use crate::listener::{BlockingHotkeys, KeyboardListener};
use crate::types::{Hotkey, HotkeyEvent, HotkeyId, HotkeyState, KeyEvent};
struct ManagerState {
hotkeys: HashMap<HotkeyId, Hotkey>,
next_id: u32,
pressed_hotkeys: HashSet<HotkeyId>,
}
impl ManagerState {
fn new() -> Self {
Self {
hotkeys: HashMap::new(),
next_id: 0,
pressed_hotkeys: HashSet::new(),
}
}
fn process_event(&mut self, event: &KeyEvent) -> Vec<HotkeyEvent> {
let mut results = Vec::new();
if event.is_key_down {
let to_press: Vec<HotkeyId> = self
.hotkeys
.iter()
.filter(|(&id, hotkey)| {
hotkey.modifiers.matches(event.modifiers)
&& hotkey.key == event.key
&& !self.pressed_hotkeys.contains(&id)
})
.map(|(&id, _)| id)
.collect();
for id in to_press {
self.pressed_hotkeys.insert(id);
results.push(HotkeyEvent {
id,
state: HotkeyState::Pressed,
});
}
} else {
let to_release: Vec<HotkeyId> = self
.hotkeys
.iter()
.filter(|(&id, hotkey)| {
self.pressed_hotkeys.contains(&id)
&& ((event.key.is_some() && hotkey.key == event.key)
|| (event.key.is_none() && !hotkey.modifiers.matches(event.modifiers)))
})
.map(|(&id, _)| id)
.collect();
for id in to_release {
self.pressed_hotkeys.remove(&id);
results.push(HotkeyEvent {
id,
state: HotkeyState::Released,
});
}
}
results
}
}
pub struct HotkeyManager {
state: Arc<Mutex<ManagerState>>,
event_receiver: Receiver<HotkeyEvent>,
_thread_handle: Option<JoinHandle<()>>,
running: Arc<std::sync::atomic::AtomicBool>,
blocking_hotkeys: Option<BlockingHotkeys>,
}
impl HotkeyManager {
pub fn new() -> Result<Self> {
let listener = KeyboardListener::new()?;
let (tx, rx) = mpsc::channel();
let state = Arc::new(Mutex::new(ManagerState::new()));
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
let thread_state = Arc::clone(&state);
let thread_running = Arc::clone(&running);
let handle = thread::spawn(move || {
Self::event_loop(listener, thread_state, tx, thread_running);
});
Ok(Self {
state,
event_receiver: rx,
_thread_handle: Some(handle),
running,
blocking_hotkeys: None,
})
}
pub fn new_with_blocking() -> Result<Self> {
let blocking_hotkeys: BlockingHotkeys = Arc::new(Mutex::new(HashSet::new()));
let listener = KeyboardListener::new_with_blocking(blocking_hotkeys.clone())?;
let (tx, rx) = mpsc::channel();
let state = Arc::new(Mutex::new(ManagerState::new()));
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
let thread_state = Arc::clone(&state);
let thread_running = Arc::clone(&running);
let handle = thread::spawn(move || {
Self::event_loop(listener, thread_state, tx, thread_running);
});
Ok(Self {
state,
event_receiver: rx,
_thread_handle: Some(handle),
running,
blocking_hotkeys: Some(blocking_hotkeys),
})
}
fn event_loop(
listener: KeyboardListener,
state: Arc<Mutex<ManagerState>>,
sender: Sender<HotkeyEvent>,
running: Arc<std::sync::atomic::AtomicBool>,
) {
const RECV_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(100);
while running.load(std::sync::atomic::Ordering::SeqCst) {
match listener.recv_timeout(RECV_TIMEOUT) {
Ok(key_event) => {
if let Ok(mut state) = state.lock() {
let hotkey_events = state.process_event(&key_event);
for event in hotkey_events {
if sender.send(event).is_err() {
return;
}
}
}
}
Err(crate::error::Error::Timeout) => {
}
Err(_) => {
return;
}
}
}
}
pub fn register(&self, hotkey: Hotkey) -> Result<HotkeyId> {
let mut state = self.state.lock().map_err(|_| Error::MutexPoisoned)?;
for (id, existing) in &state.hotkeys {
if existing == &hotkey {
return Err(Error::HotkeyAlreadyRegistered(format!(
"{} (id: {:?})",
hotkey, id
)));
}
}
let id = HotkeyId(state.next_id);
state.next_id += 1;
state.hotkeys.insert(id, hotkey);
if let Some(blocking_hotkeys) = &self.blocking_hotkeys {
if let Ok(mut blocking) = blocking_hotkeys.lock() {
blocking.insert(hotkey);
}
}
Ok(id)
}
pub fn unregister(&self, id: HotkeyId) -> Result<()> {
let mut state = self.state.lock().map_err(|_| Error::MutexPoisoned)?;
let hotkey = state.hotkeys.remove(&id);
if hotkey.is_none() {
return Err(Error::HotkeyNotFound(id));
}
if let Some(blocking_hotkeys) = &self.blocking_hotkeys {
if let Some(hotkey) = hotkey {
if let Ok(mut blocking) = blocking_hotkeys.lock() {
blocking.remove(&hotkey);
}
}
}
Ok(())
}
pub fn get_hotkey(&self, id: HotkeyId) -> Option<Hotkey> {
let state = self.state.lock().ok()?;
state.hotkeys.get(&id).copied()
}
pub fn recv(&self) -> Result<HotkeyEvent> {
self.event_receiver
.recv()
.map_err(|_| Error::EventLoopNotRunning)
}
pub fn try_recv(&self) -> Option<HotkeyEvent> {
match self.event_receiver.try_recv() {
Ok(event) => Some(event),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => None,
}
}
pub fn hotkey_count(&self) -> usize {
let state = if let Ok(s) = self.state.lock() {
s
} else {
return 0;
};
state.hotkeys.len()
}
}
impl Drop for HotkeyManager {
fn drop(&mut self) {
self.running
.store(false, std::sync::atomic::Ordering::SeqCst);
if let Some(handle) = self._thread_handle.take() {
let _ = handle.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Key, Modifiers};
fn make_key_event(modifiers: Modifiers, key: Option<Key>, is_key_down: bool) -> KeyEvent {
KeyEvent {
modifiers,
key,
is_key_down,
changed_modifier: None,
}
}
fn make_modifier_event(
modifiers: Modifiers,
is_key_down: bool,
changed: Modifiers,
) -> KeyEvent {
KeyEvent {
modifiers,
key: None,
is_key_down,
changed_modifier: Some(changed),
}
}
mod manager_state {
use super::*;
#[test]
fn register_and_lookup_hotkey() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD, Key::K).unwrap();
let id = HotkeyId(state.next_id);
state.next_id += 1;
state.hotkeys.insert(id, hotkey);
assert_eq!(state.hotkeys.get(&id), Some(&hotkey));
assert_eq!(state.hotkeys.len(), 1);
}
#[test]
fn hotkey_press_generates_event() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD, Key::K).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_key_event(Modifiers::CMD_LEFT, Some(Key::K), true);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, id);
assert_eq!(results[0].state, HotkeyState::Pressed);
assert!(state.pressed_hotkeys.contains(&id));
}
#[test]
fn hotkey_release_generates_event() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD, Key::K).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_key_event(Modifiers::CMD_LEFT, Some(Key::K), true);
state.process_event(&event);
let event = make_key_event(Modifiers::CMD_LEFT, Some(Key::K), false);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, id);
assert_eq!(results[0].state, HotkeyState::Released);
assert!(!state.pressed_hotkeys.contains(&id));
}
#[test]
fn no_duplicate_press_events() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD, Key::K).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_key_event(Modifiers::CMD_LEFT, Some(Key::K), true);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
let results = state.process_event(&event);
assert_eq!(results.len(), 0);
}
#[test]
fn modifier_release_triggers_hotkey_release() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD, Key::K).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_key_event(Modifiers::CMD_LEFT, Some(Key::K), true);
state.process_event(&event);
assert!(state.pressed_hotkeys.contains(&id));
let event = make_modifier_event(Modifiers::empty(), false, Modifiers::CMD_LEFT);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].state, HotkeyState::Released);
assert!(!state.pressed_hotkeys.contains(&id));
}
#[test]
fn wrong_modifiers_dont_trigger() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD, Key::K).unwrap();
state.hotkeys.insert(HotkeyId(0), hotkey);
let event = make_key_event(Modifiers::SHIFT_LEFT, Some(Key::K), true);
let results = state.process_event(&event);
assert_eq!(results.len(), 0);
}
#[test]
fn modifier_only_hotkey() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD | Modifiers::SHIFT, None).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_modifier_event(
Modifiers::CMD_LEFT | Modifiers::SHIFT_LEFT,
true,
Modifiers::SHIFT_LEFT,
);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].state, HotkeyState::Pressed);
}
#[test]
fn multiple_hotkeys_same_key() {
let mut state = ManagerState::new();
let hotkey1 = Hotkey::new(Modifiers::CMD, Key::K).unwrap();
let hotkey2 = Hotkey::new(Modifiers::CTRL, Key::K).unwrap();
let id1 = HotkeyId(0);
let id2 = HotkeyId(1);
state.hotkeys.insert(id1, hotkey1);
state.hotkeys.insert(id2, hotkey2);
let event = make_key_event(Modifiers::CMD_LEFT, Some(Key::K), true);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, id1);
state.pressed_hotkeys.clear();
let event = make_key_event(Modifiers::CTRL_LEFT, Some(Key::K), true);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].id, id2);
}
#[test]
fn key_only_hotkey() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::empty(), Key::F1).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_key_event(Modifiers::empty(), Some(Key::F1), true);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].state, HotkeyState::Pressed);
state.pressed_hotkeys.clear();
let event = make_key_event(Modifiers::CMD_LEFT, Some(Key::F1), true);
let results = state.process_event(&event);
assert_eq!(results.len(), 0);
}
#[test]
fn modifier_only_hotkey_not_released_by_unrelated_modifier() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD, None).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_modifier_event(Modifiers::CMD_LEFT, true, Modifiers::CMD_LEFT);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].state, HotkeyState::Pressed);
let event = make_modifier_event(
Modifiers::CMD_LEFT | Modifiers::SHIFT_LEFT,
true,
Modifiers::SHIFT_LEFT,
);
assert_eq!(state.process_event(&event).len(), 0);
let event = make_modifier_event(Modifiers::CMD_LEFT, false, Modifiers::SHIFT_LEFT);
assert_eq!(state.process_event(&event).len(), 0);
assert!(state.pressed_hotkeys.contains(&id));
let event = make_modifier_event(Modifiers::empty(), false, Modifiers::CMD_LEFT);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].state, HotkeyState::Released);
assert!(!state.pressed_hotkeys.contains(&id));
}
#[test]
fn modifier_only_hotkey_releases_on_own_modifier_release() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD, None).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_modifier_event(Modifiers::CMD_LEFT, true, Modifiers::CMD_LEFT);
state.process_event(&event);
assert!(state.pressed_hotkeys.contains(&id));
let event = make_modifier_event(Modifiers::empty(), false, Modifiers::CMD_LEFT);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].state, HotkeyState::Released);
}
#[test]
fn compound_modifier_only_hotkey_releases_on_partial_release() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD | Modifiers::SHIFT, None).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_modifier_event(Modifiers::CMD_LEFT, true, Modifiers::CMD_LEFT);
assert_eq!(state.process_event(&event).len(), 0);
let event = make_modifier_event(
Modifiers::CMD_LEFT | Modifiers::SHIFT_LEFT,
true,
Modifiers::SHIFT_LEFT,
);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].state, HotkeyState::Pressed);
let event = make_modifier_event(Modifiers::SHIFT_LEFT, false, Modifiers::CMD_LEFT);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].state, HotkeyState::Released);
}
#[test]
fn keyed_hotkey_not_released_by_unrelated_key_release() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD, Key::K).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_key_event(Modifiers::CMD_LEFT, Some(Key::K), true);
state.process_event(&event);
assert!(state.pressed_hotkeys.contains(&id));
let event = make_key_event(Modifiers::CMD_LEFT, Some(Key::J), false);
assert_eq!(state.process_event(&event).len(), 0);
assert!(state.pressed_hotkeys.contains(&id));
}
#[test]
fn side_specific_hotkey_matches_correct_side() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CTRL_RIGHT, Key::Space).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_key_event(Modifiers::CTRL_LEFT, Some(Key::Space), true);
assert_eq!(state.process_event(&event).len(), 0);
let event = make_key_event(Modifiers::CTRL_RIGHT, Some(Key::Space), true);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
assert_eq!(results[0].state, HotkeyState::Pressed);
}
#[test]
fn compound_hotkey_matches_either_side() {
let mut state = ManagerState::new();
let hotkey = Hotkey::new(Modifiers::CMD, Key::K).unwrap();
let id = HotkeyId(0);
state.hotkeys.insert(id, hotkey);
let event = make_key_event(Modifiers::CMD_LEFT, Some(Key::K), true);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
state.pressed_hotkeys.clear();
let event = make_key_event(Modifiers::CMD_RIGHT, Some(Key::K), true);
let results = state.process_event(&event);
assert_eq!(results.len(), 1);
}
}
}