use anyhow::{Context, Result};
use global_hotkey::{GlobalHotKeyEvent, GlobalHotKeyManager, hotkey::HotKey};
use std::sync::mpsc::Receiver;
pub struct HotkeyGuard {
_manager: GlobalHotKeyManager,
}
pub fn setup(hotkey_str: &str) -> Result<(Receiver<()>, HotkeyGuard)> {
let converted = convert_to_global_hotkey_format(hotkey_str)?;
let hotkey: HotKey = converted
.parse()
.map_err(|e| anyhow::anyhow!("Invalid hotkey '{}': {:?}", hotkey_str, e))?;
let manager = GlobalHotKeyManager::new()
.map_err(|e| anyhow::anyhow!("Failed to create hotkey manager: {:?}", e))?;
manager.register(hotkey.clone()).map_err(|e| {
anyhow::anyhow!(
"Failed to register hotkey '{}': {:?}\n\n\
This may mean the hotkey is already registered by another application.",
hotkey_str,
e
)
})?;
let receiver = GlobalHotKeyEvent::receiver().clone();
let hotkey_id = hotkey.id();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
loop {
if let Ok(event) = receiver.recv() {
if event.id() == hotkey_id {
let _ = tx.send(());
}
}
}
});
Ok((rx, HotkeyGuard { _manager: manager }))
}
fn convert_to_global_hotkey_format(s: &str) -> Result<String> {
let parts: Vec<&str> = s.split('+').map(|p| p.trim()).collect();
if parts.is_empty() {
anyhow::bail!("Empty hotkey string");
}
let mut result = Vec::new();
let mut has_main_key = false;
for part in parts {
let lower = part.to_lowercase();
let converted = match lower.as_str() {
"ctrl" | "control" => "Ctrl".to_string(),
"shift" => "Shift".to_string(),
"alt" => "Alt".to_string(),
"super" | "meta" | "win" | "cmd" => "Super".to_string(),
key if key.len() == 1 && key.chars().next().unwrap().is_ascii_alphabetic() => {
has_main_key = true;
format!("Key{}", key.to_uppercase())
}
key if key.len() == 1 && key.chars().next().unwrap().is_ascii_digit() => {
has_main_key = true;
format!("Digit{}", key)
}
"f1" => {
has_main_key = true;
"F1".to_string()
}
"f2" => {
has_main_key = true;
"F2".to_string()
}
"f3" => {
has_main_key = true;
"F3".to_string()
}
"f4" => {
has_main_key = true;
"F4".to_string()
}
"f5" => {
has_main_key = true;
"F5".to_string()
}
"f6" => {
has_main_key = true;
"F6".to_string()
}
"f7" => {
has_main_key = true;
"F7".to_string()
}
"f8" => {
has_main_key = true;
"F8".to_string()
}
"f9" => {
has_main_key = true;
"F9".to_string()
}
"f10" => {
has_main_key = true;
"F10".to_string()
}
"f11" => {
has_main_key = true;
"F11".to_string()
}
"f12" => {
has_main_key = true;
"F12".to_string()
}
"space" => {
has_main_key = true;
"Space".to_string()
}
"enter" | "return" => {
has_main_key = true;
"Enter".to_string()
}
"escape" | "esc" => {
has_main_key = true;
"Escape".to_string()
}
"tab" => {
has_main_key = true;
"Tab".to_string()
}
"backspace" => {
has_main_key = true;
"Backspace".to_string()
}
"delete" | "del" => {
has_main_key = true;
"Delete".to_string()
}
"insert" | "ins" => {
has_main_key = true;
"Insert".to_string()
}
"home" => {
has_main_key = true;
"Home".to_string()
}
"end" => {
has_main_key = true;
"End".to_string()
}
"pageup" | "pgup" => {
has_main_key = true;
"PageUp".to_string()
}
"pagedown" | "pgdn" => {
has_main_key = true;
"PageDown".to_string()
}
"up" => {
has_main_key = true;
"ArrowUp".to_string()
}
"down" => {
has_main_key = true;
"ArrowDown".to_string()
}
"left" => {
has_main_key = true;
"ArrowLeft".to_string()
}
"right" => {
has_main_key = true;
"ArrowRight".to_string()
}
_ => anyhow::bail!("Unknown key: {}", part),
};
result.push(converted);
}
if !has_main_key {
anyhow::bail!("No main key specified in hotkey");
}
Ok(result.join("+"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hotkey_format_conversion() {
assert_eq!(
convert_to_global_hotkey_format("ctrl+shift+r").unwrap(),
"Ctrl+Shift+KeyR"
);
assert_eq!(
convert_to_global_hotkey_format("alt+5").unwrap(),
"Alt+Digit5"
);
assert_eq!(
convert_to_global_hotkey_format("super+f1").unwrap(),
"Super+F1"
);
assert_eq!(
convert_to_global_hotkey_format("ctrl+space").unwrap(),
"Ctrl+Space"
);
}
#[test]
fn test_invalid_hotkey() {
assert!(convert_to_global_hotkey_format("ctrl+shift+invalidkey").is_err());
assert!(convert_to_global_hotkey_format("").is_err());
assert!(convert_to_global_hotkey_format("ctrl+shift").is_err()); }
}