use super::config::{HotkeyConfig, HotkeyMode, 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};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HotkeyEvent {
RecordStart,
RecordStop,
}
pub type HotkeyCallback = Arc<dyn Fn(HotkeyEvent) + Send + Sync>;
pub struct HotkeyManager {
is_running: Arc<AtomicBool>,
listener_handle: Option<JoinHandle<()>>,
is_recording: Arc<AtomicBool>,
config: Arc<Mutex<HotkeyConfig>>,
callback: Option<HotkeyCallback>,
}
impl HotkeyManager {
pub fn new() -> Self {
Self {
is_running: Arc::new(AtomicBool::new(false)),
listener_handle: None,
is_recording: Arc::new(AtomicBool::new(false)),
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(HotkeyEvent) + 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 is_recording = self.is_recording.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, is_recording);
});
self.listener_handle = Some(handle);
log::info!("STT hotkey listener started (rdev::grab)");
Ok(())
}
pub fn stop(&mut self) {
self.is_running.store(false, Ordering::Relaxed);
self.is_recording.store(false, Ordering::Relaxed);
self.listener_handle = None;
log::info!("STT hotkey listener stopped");
}
pub fn reset_recording_state(&self) {
self.is_recording.store(false, Ordering::Relaxed);
}
}
impl Default for HotkeyManager {
fn default() -> Self {
Self::new()
}
}
impl Drop for HotkeyManager {
fn drop(&mut self) {
self.stop();
}
}
fn run_grab_loop(
is_running: Arc<AtomicBool>,
config: Arc<Mutex<HotkeyConfig>>,
callback: HotkeyCallback,
is_recording: Arc<AtomicBool>,
) {
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();
log::debug!("STT hotkey grab loop started, waiting for hotkey: {}", config.lock().unwrap().key);
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);
log::debug!("STT: Modifier pressed: {:?}", modifier);
}
if let Some(target_key) = parse_key_string(&config.key) {
if key == target_key {
log::debug!("STT: Target key '{}' pressed", config.key);
let pressed = pressed_modifiers_clone.lock().unwrap();
let all_modifiers_match =
config.modifiers.iter().all(|m| pressed.contains(m));
log::debug!(
"STT: Modifiers check - required: {:?}, pressed: {:?}, match: {}",
config.modifiers,
pressed.iter().collect::<Vec<_>>(),
all_modifiers_match
);
if all_modifiers_match && !main_key_pressed_clone.load(Ordering::Relaxed) {
main_key_pressed_clone.store(true, Ordering::Relaxed);
match config.mode {
HotkeyMode::PushToTalk => {
log::info!(
"STT: Hotkey activated (PushToTalk) - START recording"
);
(callback)(HotkeyEvent::RecordStart);
}
HotkeyMode::Toggle => {
let was_recording =
is_recording.fetch_xor(true, Ordering::Relaxed);
if was_recording {
log::info!(
"STT: Hotkey activated (Toggle) - STOP recording"
);
(callback)(HotkeyEvent::RecordStop);
} else {
log::info!(
"STT: Hotkey activated (Toggle) - START recording"
);
(callback)(HotkeyEvent::RecordStart);
}
}
}
return None;
}
}
}
}
EventType::KeyRelease(key) => {
if let Some(modifier) = key_to_modifier(&key) {
pressed_modifiers_clone.lock().unwrap().remove(&modifier);
log::debug!("STT: Modifier released: {:?}", modifier);
}
if let Some(target_key) = parse_key_string(&config.key) {
if key == target_key && main_key_pressed_clone.load(Ordering::Relaxed) {
main_key_pressed_clone.store(false, Ordering::Relaxed);
log::debug!("STT: Target key '{}' released", config.key);
if config.mode == HotkeyMode::PushToTalk {
log::info!("STT: Hotkey released (PushToTalk) - STOP recording");
(callback)(HotkeyEvent::RecordStop);
}
}
}
}
_ => {}
}
Some(event)
};
if let Err(error) = grab(grab_callback) {
log::error!("STT 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 = HotkeyManager::new();
assert!(!manager.is_running());
}
#[test]
fn test_config_update() {
let mut manager = HotkeyManager::new();
let config = HotkeyConfig {
key: "F1".to_string(),
modifiers: vec![Modifier::Ctrl],
mode: HotkeyMode::Toggle,
};
manager.set_config(config.clone());
assert_eq!(manager.config().key, "F1");
}
}