use super::config::{HotkeyConfig, Modifier};
use crate::error::{AumateError, Result};
use crate::eventhooks::{Event, EventType, Key, grab};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
pub type HotkeyCallback = Arc<dyn Fn() + Send + Sync>;
pub struct ClickHelperHotkeyManager {
is_running: Arc<AtomicBool>,
listener_handle: Option<JoinHandle<()>>,
config: Arc<Mutex<HotkeyConfig>>,
callback: Option<HotkeyCallback>,
}
impl ClickHelperHotkeyManager {
pub fn new() -> Self {
Self {
is_running: Arc::new(AtomicBool::new(false)),
listener_handle: None,
config: Arc::new(Mutex::new(HotkeyConfig::default())),
callback: None,
}
}
pub fn set_config(&mut self, config: HotkeyConfig) {
*self.config.lock().unwrap() = config;
}
pub fn config(&self) -> HotkeyConfig {
self.config.lock().unwrap().clone()
}
pub fn set_callback<F>(&mut self, callback: F)
where
F: Fn() + Send + Sync + 'static,
{
self.callback = Some(Arc::new(callback));
}
pub fn is_running(&self) -> bool {
self.is_running.load(Ordering::Relaxed)
}
pub fn start(&mut self) -> Result<()> {
if self.is_running() {
return Ok(());
}
let callback = self
.callback
.clone()
.ok_or_else(|| AumateError::Other("No callback set".to_string()))?;
let is_running = self.is_running.clone();
let config = self.config.clone();
is_running.store(true, Ordering::Relaxed);
let handle = thread::spawn(move || {
run_grab_loop(is_running.clone(), config, callback);
});
self.listener_handle = Some(handle);
log::info!("Click Helper hotkey listener started (rdev::grab)");
Ok(())
}
pub fn stop(&mut self) {
self.is_running.store(false, Ordering::Relaxed);
self.listener_handle = None;
log::info!("Click Helper hotkey listener stopped");
}
}
impl Default for ClickHelperHotkeyManager {
fn default() -> Self {
Self::new()
}
}
impl Drop for ClickHelperHotkeyManager {
fn drop(&mut self) {
self.stop();
}
}
fn run_grab_loop(
is_running: Arc<AtomicBool>,
config: Arc<Mutex<HotkeyConfig>>,
callback: HotkeyCallback,
) {
let pressed_modifiers = Arc::new(Mutex::new(std::collections::HashSet::<Modifier>::new()));
let main_key_pressed = Arc::new(AtomicBool::new(false));
let pressed_modifiers_clone = pressed_modifiers.clone();
let main_key_pressed_clone = main_key_pressed.clone();
let grab_callback = move |event: Event| -> Option<Event> {
if !is_running.load(Ordering::Relaxed) {
return Some(event);
}
let config = config.lock().unwrap();
match event.event_type {
EventType::KeyPress(key) => {
if let Some(modifier) = key_to_modifier(&key) {
pressed_modifiers_clone.lock().unwrap().insert(modifier);
}
if let Some(target_key) = parse_key_string(&config.key) {
if key == target_key {
let pressed = pressed_modifiers_clone.lock().unwrap();
let all_modifiers_match =
config.modifiers.iter().all(|m| pressed.contains(m));
if all_modifiers_match && !main_key_pressed_clone.load(Ordering::Relaxed) {
main_key_pressed_clone.store(true, Ordering::Relaxed);
log::info!("Click Helper hotkey triggered");
(callback)();
return None;
}
}
}
}
EventType::KeyRelease(key) => {
if let Some(modifier) = key_to_modifier(&key) {
pressed_modifiers_clone.lock().unwrap().remove(&modifier);
}
if let Some(target_key) = parse_key_string(&config.key) {
if key == target_key {
main_key_pressed_clone.store(false, Ordering::Relaxed);
}
}
}
_ => {}
}
Some(event)
};
if let Err(error) = grab(grab_callback) {
log::error!("Click Helper hotkey grab error: {:?}", error);
}
}
fn key_to_modifier(key: &Key) -> Option<Modifier> {
match key {
Key::ControlLeft | Key::ControlRight => Some(Modifier::Ctrl),
Key::Alt | Key::AltGr => Some(Modifier::Alt),
Key::ShiftLeft | Key::ShiftRight => Some(Modifier::Shift),
Key::MetaLeft | Key::MetaRight => Some(Modifier::Meta),
_ => None,
}
}
fn parse_key_string(key_str: &str) -> Option<Key> {
match key_str.to_lowercase().as_str() {
"0" => Some(Key::Num0),
"1" => Some(Key::Num1),
"2" => Some(Key::Num2),
"3" => Some(Key::Num3),
"4" => Some(Key::Num4),
"5" => Some(Key::Num5),
"6" => Some(Key::Num6),
"7" => Some(Key::Num7),
"8" => Some(Key::Num8),
"9" => Some(Key::Num9),
"a" => Some(Key::KeyA),
"b" => Some(Key::KeyB),
"c" => Some(Key::KeyC),
"d" => Some(Key::KeyD),
"e" => Some(Key::KeyE),
"f" => Some(Key::KeyF),
"g" => Some(Key::KeyG),
"h" => Some(Key::KeyH),
"i" => Some(Key::KeyI),
"j" => Some(Key::KeyJ),
"k" => Some(Key::KeyK),
"l" => Some(Key::KeyL),
"m" => Some(Key::KeyM),
"n" => Some(Key::KeyN),
"o" => Some(Key::KeyO),
"p" => Some(Key::KeyP),
"q" => Some(Key::KeyQ),
"r" => Some(Key::KeyR),
"s" => Some(Key::KeyS),
"t" => Some(Key::KeyT),
"u" => Some(Key::KeyU),
"v" => Some(Key::KeyV),
"w" => Some(Key::KeyW),
"x" => Some(Key::KeyX),
"y" => Some(Key::KeyY),
"z" => Some(Key::KeyZ),
"space" => Some(Key::Space),
"enter" | "return" => Some(Key::Return),
"tab" => Some(Key::Tab),
"escape" | "esc" => Some(Key::Escape),
"backspace" => Some(Key::Backspace),
"delete" => Some(Key::Delete),
"f1" => Some(Key::F1),
"f2" => Some(Key::F2),
"f3" => Some(Key::F3),
"f4" => Some(Key::F4),
"f5" => Some(Key::F5),
"f6" => Some(Key::F6),
"f7" => Some(Key::F7),
"f8" => Some(Key::F8),
"f9" => Some(Key::F9),
"f10" => Some(Key::F10),
"f11" => Some(Key::F11),
"f12" => Some(Key::F12),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hotkey_manager_creation() {
let manager = ClickHelperHotkeyManager::new();
assert!(!manager.is_running());
}
#[test]
fn test_parse_key_string() {
assert_eq!(parse_key_string("2"), Some(Key::Num2));
assert_eq!(parse_key_string("space"), Some(Key::Space));
assert_eq!(parse_key_string("F1"), Some(Key::F1));
assert_eq!(parse_key_string("unknown"), None);
}
#[test]
fn test_key_to_modifier() {
assert_eq!(key_to_modifier(&Key::ControlLeft), Some(Modifier::Ctrl));
assert_eq!(key_to_modifier(&Key::Alt), Some(Modifier::Alt));
assert_eq!(key_to_modifier(&Key::ShiftLeft), Some(Modifier::Shift));
assert_eq!(key_to_modifier(&Key::MetaLeft), Some(Modifier::Meta));
assert_eq!(key_to_modifier(&Key::KeyA), None);
}
}