use aethermap_common::tracing::warn;
use std::collections::HashSet;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::config::HotkeyBinding;
use crate::layer_manager::LayerManager;
use crate::config::ConfigManager;
pub mod key_codes {
pub const KEY_LEFTCTRL: u16 = 29;
pub const KEY_RIGHTCTRL: u16 = 97;
pub const KEY_LEFTALT: u16 = 56;
pub const KEY_RIGHTALT: u16 = 100;
pub const KEY_LEFTSHIFT: u16 = 42;
pub const KEY_RIGHTSHIFT: u16 = 54;
pub const KEY_LEFTMETA: u16 = 125;
pub const KEY_RIGHTMETA: u16 = 126;
pub const MODIFIER_KEYS: &[u16] = &[
KEY_LEFTCTRL, KEY_RIGHTCTRL,
KEY_LEFTALT, KEY_RIGHTALT,
KEY_LEFTSHIFT, KEY_RIGHTSHIFT,
KEY_LEFTMETA, KEY_RIGHTMETA,
];
pub const KEY_1: u16 = 2;
pub const KEY_2: u16 = 3;
pub const KEY_3: u16 = 4;
pub const KEY_4: u16 = 5;
pub const KEY_5: u16 = 6;
pub const KEY_6: u16 = 7;
pub const KEY_7: u16 = 8;
pub const KEY_8: u16 = 9;
pub const KEY_9: u16 = 10;
}
pub fn is_modifier_key(key_code: u16) -> bool {
key_codes::MODIFIER_KEYS.contains(&key_code)
}
pub fn normalize_modifier_name(name: &str) -> Option<&'static str> {
match name.to_lowercase().as_str() {
"ctrl" | "control" | "ctl" => Some("ctrl"),
"alt" | "altgr" | "alt_gr" => Some("alt"),
"shift" | "shft" => Some("shift"),
"super" | "win" | "windows" | "meta" | "mod" => Some("super"),
_ => None,
}
}
pub fn key_code_to_modifier_name(key_code: u16) -> Option<&'static str> {
match key_code {
key_codes::KEY_LEFTCTRL | key_codes::KEY_RIGHTCTRL => Some("ctrl"),
key_codes::KEY_LEFTALT | key_codes::KEY_RIGHTALT => Some("alt"),
key_codes::KEY_LEFTSHIFT | key_codes::KEY_RIGHTSHIFT => Some("shift"),
key_codes::KEY_LEFTMETA | key_codes::KEY_RIGHTMETA => Some("super"),
_ => None,
}
}
pub struct GlobalHotkeyManager {
bindings: Vec<HotkeyBinding>,
active_modifiers: HashSet<u16>,
layer_manager: Arc<RwLock<LayerManager>>,
config_manager: Arc<ConfigManager>,
}
impl GlobalHotkeyManager {
pub fn new(
layer_manager: Arc<RwLock<LayerManager>>,
config_manager: Arc<ConfigManager>,
) -> Self {
Self {
bindings: Vec::new(),
active_modifiers: HashSet::new(),
layer_manager,
config_manager,
}
}
pub async fn load_bindings(&mut self) -> Result<(), Box<dyn std::error::Error>> {
let all_bindings = self.config_manager.get_all_hotkey_bindings().await;
self.bindings = if all_bindings.is_empty() {
aethermap_common::tracing::info!("No hotkey bindings found, using defaults");
crate::config::default_hotkey_bindings()
} else {
all_bindings.into_iter().map(|mut binding| {
binding.modifiers = binding.modifiers.iter()
.filter_map(|m| normalize_modifier_name(m))
.map(|s| s.to_string())
.collect();
binding
}).collect()
};
aethermap_common::tracing::info!(
"Loaded {} hotkey bindings",
self.bindings.len()
);
Ok(())
}
fn get_active_modifier_names(&self) -> HashSet<String> {
self.active_modifiers
.iter()
.filter_map(|&code| key_code_to_modifier_name(code))
.map(|s| s.to_string())
.collect()
}
pub async fn check_key_event(&mut self, key_code: u16, pressed: bool) -> bool {
if is_modifier_key(key_code) {
if pressed {
self.active_modifiers.insert(key_code);
} else {
self.active_modifiers.remove(&key_code);
}
return false;
}
if !pressed {
return false;
}
let active_modifiers = self.get_active_modifier_names();
for binding in &self.bindings {
if self.binding_matches(binding, key_code, &active_modifiers) {
aethermap_common::tracing::info!(
"Hotkey matched: modifiers={:?}, key={}, profile={}",
binding.modifiers,
binding.key,
binding.profile_name
);
let _ = self.trigger_action(binding).await;
return true;
}
}
false
}
fn binding_matches(
&self,
binding: &HotkeyBinding,
key_code: u16,
active_modifiers: &HashSet<String>,
) -> bool {
let binding_modifiers: HashSet<String> = binding
.normalize_modifiers()
.into_iter()
.collect();
if binding_modifiers != *active_modifiers {
return false;
}
let binding_key_code = self.parse_binding_key(&binding.key);
binding_key_code == Some(key_code)
}
fn parse_binding_key(&self, key: &str) -> Option<u16> {
let key_lower = key.to_lowercase();
if key_lower.len() == 1 {
if let Some(digit) = key_lower.chars().next() {
if digit.is_ascii_digit() {
let num = digit.to_digit(10)?;
return match num {
1 => Some(key_codes::KEY_1),
2 => Some(key_codes::KEY_2),
3 => Some(key_codes::KEY_3),
4 => Some(key_codes::KEY_4),
5 => Some(key_codes::KEY_5),
6 => Some(key_codes::KEY_6),
7 => Some(key_codes::KEY_7),
8 => Some(key_codes::KEY_8),
9 => Some(key_codes::KEY_9),
_ => None,
};
}
}
}
#[cfg(feature = "key_parser")]
{
if let Ok(parsed) = crate::key_parser::parse_key(&key_lower) {
return Some(parsed);
}
}
if let Ok(code) = key_lower.parse::<u16>() {
return Some(code);
}
warn!("Failed to parse hotkey key: {}", key);
None
}
async fn trigger_action(&self, binding: &HotkeyBinding) -> Result<(), String> {
let device_ids = if let Some(ref device_id) = binding.device_id {
vec![device_id.clone()]
} else {
let layer_manager = self.layer_manager.read().await;
layer_manager.get_device_ids().await
};
for device_id in device_ids {
if let Some(_profile) = self.config_manager
.get_device_profile(&device_id, &binding.profile_name).await
{
let layer_manager = self.layer_manager.read().await;
if let Some(layer_id) = binding.layer_id {
layer_manager.activate_layer(&device_id, layer_id).await;
aethermap_common::tracing::info!(
"Hotkey activated: device={}, profile={}, layer={}",
&device_id,
&binding.profile_name,
layer_id
);
} else {
aethermap_common::tracing::info!(
"Hotkey activated: device={}, profile={}",
&device_id,
&binding.profile_name
);
}
return Ok(());
}
warn!(
"Hotkey referenced non-existent profile '{}' for device {}",
&binding.profile_name, &device_id
);
}
Err(format!("Profile '{}' not found for any device", binding.profile_name))
}
pub fn binding_count(&self) -> usize {
self.bindings.len()
}
pub fn clear_modifiers(&mut self) {
self.active_modifiers.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_modifier_key() {
assert!(is_modifier_key(key_codes::KEY_LEFTCTRL));
assert!(is_modifier_key(key_codes::KEY_RIGHTCTRL));
assert!(is_modifier_key(key_codes::KEY_LEFTALT));
assert!(is_modifier_key(key_codes::KEY_LEFTSHIFT));
assert!(is_modifier_key(key_codes::KEY_LEFTMETA));
assert!(!is_modifier_key(key_codes::KEY_1));
assert!(!is_modifier_key(1000)); }
#[test]
fn test_normalize_modifier_name() {
assert_eq!(normalize_modifier_name("ctrl"), Some("ctrl"));
assert_eq!(normalize_modifier_name("CTRL"), Some("ctrl"));
assert_eq!(normalize_modifier_name("control"), Some("ctrl"));
assert_eq!(normalize_modifier_name("Alt"), Some("alt"));
assert_eq!(normalize_modifier_name("SHIFT"), Some("shift"));
assert_eq!(normalize_modifier_name("Win"), Some("super"));
assert_eq!(normalize_modifier_name("invalid"), None);
}
#[test]
fn test_key_code_to_modifier_name() {
assert_eq!(key_code_to_modifier_name(key_codes::KEY_LEFTCTRL), Some("ctrl"));
assert_eq!(key_code_to_modifier_name(key_codes::KEY_RIGHTCTRL), Some("ctrl"));
assert_eq!(key_code_to_modifier_name(key_codes::KEY_LEFTALT), Some("alt"));
assert_eq!(key_code_to_modifier_name(key_codes::KEY_LEFTSHIFT), Some("shift"));
assert_eq!(key_code_to_modifier_name(key_codes::KEY_LEFTMETA), Some("super"));
assert_eq!(key_code_to_modifier_name(key_codes::KEY_1), None);
}
#[test]
fn test_hotkey_binding_new() {
let binding = HotkeyBinding::new(
vec!["ctrl".to_string(), "alt".to_string()],
"1".to_string(),
"gaming".to_string(),
);
assert_eq!(binding.modifiers, vec!["ctrl", "alt"]);
assert_eq!(binding.key, "1");
assert_eq!(binding.profile_name, "gaming");
assert!(binding.device_id.is_none());
assert!(binding.layer_id.is_none());
}
#[test]
fn test_hotkey_binding_with_device() {
let binding = HotkeyBinding::with_device(
vec!["ctrl".to_string()],
"2".to_string(),
"work".to_string(),
"1532:0220".to_string(),
);
assert_eq!(binding.modifiers, vec!["ctrl"]);
assert_eq!(binding.key, "2");
assert_eq!(binding.profile_name, "work");
assert_eq!(binding.device_id, Some("1532:0220".to_string()));
assert!(binding.layer_id.is_none());
}
#[test]
fn test_hotkey_binding_with_layer() {
let binding = HotkeyBinding::with_layer(
vec!["ctrl".to_string(), "shift".to_string()],
"3".to_string(),
"gaming".to_string(),
2,
);
assert_eq!(binding.modifiers, vec!["ctrl", "shift"]);
assert_eq!(binding.key, "3");
assert_eq!(binding.profile_name, "gaming");
assert!(binding.device_id.is_none());
assert_eq!(binding.layer_id, Some(2));
}
#[test]
fn test_hotkey_binding_normalize_modifiers() {
let binding = HotkeyBinding::new(
vec!["CTRL".to_string(), "AlT".to_string()],
"1".to_string(),
"gaming".to_string(),
);
let normalized = binding.normalize_modifiers();
assert_eq!(normalized, vec!["ctrl", "alt"]);
}
#[test]
fn test_default_hotkey_bindings() {
let bindings = crate::config::default_hotkey_bindings();
assert_eq!(bindings.len(), 9);
assert_eq!(bindings[0].modifiers, vec!["ctrl", "alt", "shift"]);
assert_eq!(bindings[0].key, "1");
assert_eq!(bindings[0].profile_name, "profile1");
assert_eq!(bindings[8].key, "9");
assert_eq!(bindings[8].profile_name, "profile9");
}
#[test]
fn test_modifier_tracking() {
let mut modifiers = HashSet::new();
modifiers.insert(key_codes::KEY_LEFTCTRL);
modifiers.insert(key_codes::KEY_LEFTSHIFT);
assert!(modifiers.contains(&key_codes::KEY_LEFTCTRL));
assert!(modifiers.contains(&key_codes::KEY_LEFTSHIFT));
assert!(!modifiers.contains(&key_codes::KEY_LEFTALT));
modifiers.remove(&key_codes::KEY_LEFTCTRL);
assert!(!modifiers.contains(&key_codes::KEY_LEFTCTRL));
}
#[test]
fn test_key_codes_constants() {
assert_eq!(key_codes::KEY_LEFTCTRL, 29);
assert_eq!(key_codes::KEY_RIGHTCTRL, 97);
assert_eq!(key_codes::KEY_LEFTALT, 56);
assert_eq!(key_codes::KEY_RIGHTALT, 100);
assert_eq!(key_codes::KEY_LEFTSHIFT, 42);
assert_eq!(key_codes::KEY_RIGHTSHIFT, 54);
assert_eq!(key_codes::KEY_LEFTMETA, 125);
assert_eq!(key_codes::KEY_RIGHTMETA, 126);
assert_eq!(key_codes::KEY_1, 2);
assert_eq!(key_codes::KEY_2, 3);
assert_eq!(key_codes::KEY_3, 4);
assert_eq!(key_codes::KEY_9, 10);
}
#[test]
fn test_modifier_keys_array() {
assert!(key_codes::MODIFIER_KEYS.contains(&key_codes::KEY_LEFTCTRL));
assert!(key_codes::MODIFIER_KEYS.contains(&key_codes::KEY_RIGHTCTRL));
assert!(key_codes::MODIFIER_KEYS.contains(&key_codes::KEY_LEFTALT));
assert!(key_codes::MODIFIER_KEYS.contains(&key_codes::KEY_RIGHTALT));
assert!(key_codes::MODIFIER_KEYS.contains(&key_codes::KEY_LEFTSHIFT));
assert!(key_codes::MODIFIER_KEYS.contains(&key_codes::KEY_RIGHTSHIFT));
assert!(key_codes::MODIFIER_KEYS.contains(&key_codes::KEY_LEFTMETA));
assert!(key_codes::MODIFIER_KEYS.contains(&key_codes::KEY_RIGHTMETA));
assert!(!key_codes::MODIFIER_KEYS.contains(&key_codes::KEY_1));
}
}