use async_trait::async_trait;
use dashmap::DashMap;
use parking_lot::{Mutex, RwLock};
use std::cell::Cell;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tauri::AppHandle;
use tauri_plugin_global_shortcut::{
Code, GlobalShortcutExt, Modifiers, Shortcut as TauriShortcut, ShortcutState,
};
use tracing::{debug, info, warn};
use windows::Win32::UI::Input::KeyboardAndMouse::{VK_LCONTROL, VK_RCONTROL};
use zerolaunch_plugin_api::host::HostApiError;
use zerolaunch_plugin_api::services::hotkey::{
CallbackRegistration, Hotkey, HotkeyCallback, HotkeyEvent, HotkeyEventFilter, HotkeyManager,
};
use crate::raw_keyboard::{RawKeyEvent, RawKeyboardListener};
const DOUBLE_CTRL_WINDOW: Duration = Duration::from_millis(400);
const DOUBLE_CTRL_KEYS: [u16; 3] = [0x11, VK_LCONTROL.0, VK_RCONTROL.0];
pub struct WindowsHotkeyManager {
app_handle: Arc<AppHandle>,
event_callback: Arc<RwLock<Option<HotkeyCallback>>>,
double_ctrl_enabled: AtomicBool,
is_listening: Arc<AtomicBool>,
double_ctrl_listener: Mutex<Option<RawKeyboardListener>>,
registered: Mutex<Vec<Hotkey>>,
callbacks: Arc<DashMap<String, CallbackRegistration>>,
}
fn parse_key_code(key: &str) -> Option<Code> {
let mut chars = key.chars();
let normalized = match (chars.next(), chars.next()) {
(Some(c), None) if c.is_ascii_alphabetic() => format!("Key{}", c.to_ascii_uppercase()),
(Some(c), None) if c.is_ascii_digit() => format!("Digit{c}"),
_ => key.to_string(),
};
normalized.parse::<Code>().ok()
}
fn validate_global_hotkey(hotkey: &Hotkey) -> Result<(), HostApiError> {
if !(hotkey.ctrl || hotkey.alt || hotkey.shift || hotkey.meta) {
return Err(HostApiError::ExecutionFailed {
service: "hotkey".to_string(),
reason: "全局快捷键必须包含至少一个修饰键(Ctrl/Alt/Shift/Win)".to_string(),
});
}
convert_hotkey(hotkey).map(|_| ())
}
fn convert_hotkey(hotkey: &Hotkey) -> Result<TauriShortcut, HostApiError> {
let Some(code) = parse_key_code(&hotkey.key) else {
return Err(HostApiError::ExecutionFailed {
service: "hotkey".to_string(),
reason: format!("无效的按键: {}", hotkey.key),
});
};
let mut mods = Modifiers::empty();
if hotkey.ctrl {
mods |= Modifiers::CONTROL;
}
if hotkey.alt {
mods |= Modifiers::ALT;
}
if hotkey.shift {
mods |= Modifiers::SHIFT;
}
if hotkey.meta {
mods |= Modifiers::META;
}
let modifiers = (!mods.is_empty()).then_some(mods);
Ok(TauriShortcut::new(modifiers, code))
}
impl WindowsHotkeyManager {
pub fn new(app_handle: Arc<AppHandle>) -> Self {
let _ = app_handle.plugin(tauri_plugin_global_shortcut::Builder::new().build());
Self {
app_handle,
event_callback: Arc::new(RwLock::new(None)),
double_ctrl_enabled: AtomicBool::new(false),
is_listening: Arc::new(AtomicBool::new(false)),
double_ctrl_listener: Mutex::new(None),
registered: Mutex::new(Vec::new()),
callbacks: Arc::new(DashMap::new()),
}
}
fn convert_to_tauri_shortcut(&self, hotkey: &Hotkey) -> Result<TauriShortcut, HostApiError> {
convert_hotkey(hotkey)
}
fn start_double_ctrl_listener(&self) {
if !self.double_ctrl_enabled.load(Ordering::Relaxed)
|| !self.is_listening.load(Ordering::Relaxed)
{
return;
}
let mut slot = self.double_ctrl_listener.lock();
if slot.is_some() {
return;
}
let callback_ref = self.event_callback.clone();
let last_ctrl_press = Cell::new(Instant::now());
let press_count = Cell::new(0u8);
let last_key_was_release = Cell::new(false);
let started = RawKeyboardListener::start(move |event: RawKeyEvent| {
if !DOUBLE_CTRL_KEYS.contains(&event.vk) {
if event.is_down {
press_count.set(0);
last_key_was_release.set(false);
}
return;
}
if !event.is_down {
last_key_was_release.set(true);
return;
}
let now = Instant::now();
if now.duration_since(last_ctrl_press.get()) < DOUBLE_CTRL_WINDOW {
if press_count.get() == 1 && last_key_was_release.get() {
press_count.set(2);
} else {
press_count.set(1);
}
} else {
press_count.set(1);
}
last_ctrl_press.set(now);
last_key_was_release.set(false);
if press_count.get() == 2 {
press_count.set(0);
if let Some(callback) = callback_ref.read().as_ref() {
callback(HotkeyEvent::DoubleCtrl);
}
}
});
match started {
Ok(listener) => {
info!("双击 Ctrl 监听已启动(原始输入,无输入钩子)");
*slot = Some(listener);
}
Err(e) => warn!("双击 Ctrl 监听启动失败: {e}"),
}
}
fn stop_double_ctrl_listener(&self) {
if let Some(listener) = self.double_ctrl_listener.lock().take() {
listener.stop();
info!("双击 Ctrl 监听已停止");
}
}
}
#[async_trait]
impl HotkeyManager for WindowsHotkeyManager {
async fn validate_hotkey(&self, hotkey: &Hotkey) -> Result<(), HostApiError> {
validate_global_hotkey(hotkey)
}
async fn register_hotkey(&self, hotkey: &Hotkey) -> Result<(), HostApiError> {
let tauri_shortcut = self.convert_to_tauri_shortcut(hotkey)?;
let callback_ref = self.event_callback.clone();
let hotkey_clone = hotkey.clone();
let hotkey_debug = hotkey.clone();
self.app_handle
.global_shortcut()
.on_shortcut(tauri_shortcut, move |_app, _shortcut, event| {
if let ShortcutState::Pressed = event.state() {
debug!("全局快捷键按下: {:?}", hotkey_debug);
if let Some(callback) = callback_ref.read().as_ref() {
debug!("分发快捷键事件: {:?}", hotkey_debug);
callback(HotkeyEvent::GlobalHotkey(hotkey_clone.clone()));
} else {
warn!("快捷键按下但无事件分发器: {:?}", hotkey_debug);
}
}
})
.map_err(|e| HostApiError::ExecutionFailed {
service: "hotkey".to_string(),
reason: format!("注册快捷键失败: {:?}", e),
})?;
self.registered.lock().push(hotkey.clone());
Ok(())
}
async fn unregister_hotkey(&self, hotkey: &Hotkey) -> Result<(), HostApiError> {
let tauri_shortcut = self.convert_to_tauri_shortcut(hotkey)?;
let _ = self.app_handle.global_shortcut().unregister(tauri_shortcut);
self.registered
.lock()
.retain(|registered| registered != hotkey);
Ok(())
}
async fn unregister_all(&self) -> Result<(), HostApiError> {
let _ = self.app_handle.global_shortcut().unregister_all();
self.registered.lock().clear();
Ok(())
}
async fn replace_all_hotkeys(&self, hotkeys: &[Hotkey]) -> Result<(), HostApiError> {
let previous = self.registered.lock().clone();
let _ = self.app_handle.global_shortcut().unregister_all();
self.registered.lock().clear();
for hotkey in hotkeys {
if let Err(e) = self.register_hotkey(hotkey).await {
let _ = self.app_handle.global_shortcut().unregister_all();
self.registered.lock().clear();
for old in &previous {
if let Err(restore_err) = self.register_hotkey(old).await {
warn!("恢复旧快捷键失败: {restore_err}");
}
}
return Err(e);
}
}
Ok(())
}
async fn set_double_ctrl_enabled(&self, enabled: bool) -> Result<(), HostApiError> {
self.double_ctrl_enabled.store(enabled, Ordering::Relaxed);
if enabled {
self.start_double_ctrl_listener();
} else {
self.stop_double_ctrl_listener();
}
Ok(())
}
async fn start_listening(&self) -> Result<(), HostApiError> {
let was_listening = self.is_listening.swap(true, Ordering::Relaxed);
let callbacks = self.callbacks.clone();
let callback_count = callbacks.len();
info!("启动按键监听,已注册 {} 个回调", callback_count);
let dispatcher: HotkeyCallback = Arc::new(move |event| {
debug!("按键事件分发: {:?}, 回调总数: {}", event, callback_count);
let mut matched = 0;
for entry in callbacks.iter() {
let registration = entry.value();
if matches_filter(®istration.filter, &event) {
debug!(" 匹配回调: {}", registration.id);
(registration.callback)(event.clone());
matched += 1;
}
}
if matched == 0 {
debug!(" 无匹配回调");
}
});
*self.event_callback.write() = Some(dispatcher);
self.start_double_ctrl_listener();
if was_listening {
info!("更新按键分发器");
} else {
info!("首次启动按键监听");
}
Ok(())
}
async fn stop_listening(&self) -> Result<(), HostApiError> {
*self.event_callback.write() = None;
self.is_listening.store(false, Ordering::Relaxed);
self.stop_double_ctrl_listener();
Ok(())
}
fn is_listening(&self) -> bool {
self.is_listening.load(Ordering::Relaxed)
}
fn register_callback(&self, id: &str, filter: HotkeyEventFilter, callback: HotkeyCallback) {
self.callbacks.insert(
id.to_string(),
CallbackRegistration {
id: id.to_string(),
filter,
callback,
},
);
}
fn unregister_callback(&self, id: &str) {
self.callbacks.remove(id);
}
}
fn matches_filter(filter: &HotkeyEventFilter, event: &HotkeyEvent) -> bool {
match (filter, event) {
(HotkeyEventFilter::All, _) => true,
(
HotkeyEventFilter::GlobalHotkey(filter_hotkey),
HotkeyEvent::GlobalHotkey(event_hotkey),
) => filter_hotkey == event_hotkey,
(HotkeyEventFilter::DoubleCtrl, HotkeyEvent::DoubleCtrl) => true,
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_key_code_covers_recorded_keys() {
assert_eq!(parse_key_code("A"), Some(Code::KeyA));
assert_eq!(parse_key_code("z"), Some(Code::KeyZ));
assert_eq!(parse_key_code("7"), Some(Code::Digit7));
for named in [
"Space",
"Tab",
"CapsLock",
"Enter",
"ArrowUp",
"ArrowDown",
"ArrowLeft",
"ArrowRight",
"Home",
"End",
"PageUp",
"PageDown",
"Insert",
"Delete",
"F5",
"F12",
] {
assert!(parse_key_code(named).is_some(), "键名 {named} 应可转换");
}
assert_eq!(parse_key_code("Foo"), None);
}
#[test]
fn convert_hotkey_builds_shortcut() {
let shortcut = convert_hotkey(&Hotkey::new("Space").with_alt()).unwrap();
assert_eq!(shortcut.key, Code::Space);
assert_eq!(shortcut.mods, Modifiers::ALT);
let single = convert_hotkey(&Hotkey::new("K")).unwrap();
assert_eq!(single.key, Code::KeyK);
assert_eq!(single.mods, Modifiers::empty());
}
#[test]
fn validate_global_hotkey_rejects_bare_key_and_unknown_key() {
assert!(validate_global_hotkey(&Hotkey::new("Space").with_alt()).is_ok());
assert!(validate_global_hotkey(&Hotkey::new("K")).is_err());
assert!(validate_global_hotkey(&Hotkey::new("Enter")).is_err());
assert!(validate_global_hotkey(&Hotkey::new("Foo").with_ctrl()).is_err());
assert!(validate_global_hotkey(&Hotkey::new("K").with_ctrl()).is_ok());
assert!(validate_global_hotkey(&Hotkey::new("F5").with_alt()).is_ok());
}
}