From 2f9739174347a11a41595331afa5ac3ae88d0993 Mon Sep 17 00:00:00 2001 From: macro Date: Wed, 7 Oct 2026 14:50:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E7=83=AD=E9=94=AE=E5=B7=A5=E5=85=B7):=201?= =?UTF-8?q?.=20=E6=96=B0=E5=A2=9E=E7=83=AD=E9=94=AE=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=202.=20=E6=94=AF=E6=8C=81=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E7=83=AD=E9=94=AE=E5=8D=A0=E7=94=A8=E6=83=85=E5=86=B5=203.=20?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E8=87=AA=E5=8A=A8=E5=AE=9A=E4=BD=8D=E5=8D=A0?= =?UTF-8?q?=E7=94=A8=E8=80=85=E5=92=8C=E7=9B=91=E6=8E=A7=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=204.=20=E6=B7=BB=E5=8A=A0=E5=B8=B8=E8=A7=81=E7=83=AD=E9=94=AE?= =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hotkey/Cargo.toml | 9 + hotkey/src/main.rs | 499 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 508 insertions(+) create mode 100644 hotkey/Cargo.toml create mode 100644 hotkey/src/main.rs diff --git a/hotkey/Cargo.toml b/hotkey/Cargo.toml new file mode 100644 index 0000000..dfde8dc --- /dev/null +++ b/hotkey/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "hotkey" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +winreg = "0.52" +windows = { version = "0.56", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_System_Registry", "Win32_System_SystemServices", "Win32_UI_Input_KeyboardAndMouse", "Win32_System_Diagnostics_ToolHelp", "Win32_System_Threading", "Win32_UI_WindowsAndMessaging"] } diff --git a/hotkey/src/main.rs b/hotkey/src/main.rs new file mode 100644 index 0000000..f8e6fd2 --- /dev/null +++ b/hotkey/src/main.rs @@ -0,0 +1,499 @@ +use clap::Parser; +use std::collections::HashSet; +use std::process::Command; +use windows::Win32::Foundation::{BOOL, HWND, LPARAM, TRUE}; +use windows::Win32::System::Diagnostics::ToolHelp::{CreateToolhelp32Snapshot, Process32First, Process32Next, PROCESSENTRY32, TH32CS_SNAPPROCESS}; +use windows::Win32::UI::Input::KeyboardAndMouse::{MOD_ALT, MOD_CONTROL, MOD_SHIFT, MOD_WIN, RegisterHotKey, UnregisterHotKey}; +use windows::Win32::UI::WindowsAndMessaging::{EnumWindows, GetWindowThreadProcessId, IsWindowVisible}; + +#[derive(Parser, Debug)] +#[command(name = "hotkey")] +#[command(author, version, about, long_about = None)] +#[command( + about = "⌨️ 热键查询工具 - 查看当前用户下的热键配置", + long_about = "⌨️ 热键查询工具\n\n查看当前用户下注册的热键配置,以及查询特定热键被哪个程序占用。\n\n示例:\n hotkey - 列出所有常见热键的占用情况\n hotkey alt+q - 查询 Alt+Q 被哪个程序占用\n hotkey alt+q --find - 自动逐个关闭候选程序,精确定位占用者" +)] +struct Args { + /// 查询特定热键,格式如: alt+q, ctrl+c, win+e + #[arg(value_name = "HOTKEY")] + hotkey: Option, + + /// 自动逐个关闭候选程序来精确定位占用者 + #[arg(short, long)] + find: bool, + + /// 持续监控热键状态,直到热键释放 + #[arg(short, long)] + watch: bool, +} + +// 常见注册全局热键的程序(按可能性排序) +const KNOWN_HOTKEY_APPS: &[&str] = &[ + // 聊天软件 + "Weixin.exe", "WeChatAppEx.exe", "QQ.exe", "TIM.exe", "DingTalk.exe", "AliWorkbench.exe", + // 快捷键工具 + "AutoHotkey.exe", "AutoHotkey64.exe", "AutoHotkeyA32.exe", "AutoHotkeyU32.exe", "AutoHotkeyU64.exe", + "PowerToys.exe", "PowerToys.QuickAccess.exe", "PowerToys.KeyboardManagerEngine.exe", + "Listary.exe", "Wox.exe", "uelauncher.exe", + // 截图/录屏 + "Snipaste.exe", "ShareX.exe", "obs64.exe", "DingTalkDingCapture.exe", "SunloginClient.exe", + // 输入法 + "SogouCloud.exe", "ctfmon.exe", "ChsIME.exe", "TextInputHost.exe", + // 翻译 + "Doubao.exe", "YoudaoDict.exe", "Lingoes.exe", + // 系统工具 + "Nahimic3.exe", "RTSS.exe", "LogiBolt.exe", "GamingServices.exe", + // 远程控制 + "ToDesk.exe", "rustdesk.exe", "AnyDesk.exe", + // 其他 + "GestureSign.exe", "StrokesPlus.xml", "MultiCommander.exe", +]; + +fn parse_hotkey(input: &str) -> Option<(u32, u32)> { + let parts: Vec<&str> = input.split('+').collect(); + if parts.is_empty() { + return None; + } + + let mut modifiers = 0u32; + let mut vk_code = 0u32; + + for part in parts { + let part = part.trim().to_lowercase(); + match part.as_str() { + "alt" => modifiers |= MOD_ALT.0, + "ctrl" | "control" => modifiers |= MOD_CONTROL.0, + "shift" => modifiers |= MOD_SHIFT.0, + "win" | "meta" | "super" => modifiers |= MOD_WIN.0, + key => { + if key.len() == 1 { + let c = key.chars().next().unwrap(); + vk_code = if c.is_ascii_alphabetic() { + c.to_ascii_uppercase() as u32 + } else if c.is_ascii_digit() { + c as u32 + } else { + return None; + }; + } else { + vk_code = match &*key { + "f1" => 0x70, "f2" => 0x71, "f3" => 0x72, "f4" => 0x73, + "f5" => 0x74, "f6" => 0x75, "f7" => 0x76, "f8" => 0x77, + "f9" => 0x78, "f10" => 0x79, "f11" => 0x7A, "f12" => 0x7B, + "space" => 0x20, "enter" => 0x0D, "tab" => 0x09, + "backspace" => 0x08, "delete" => 0x2E, "insert" => 0x2D, + "home" => 0x24, "end" => 0x23, "pageup" => 0x21, "pagedown" => 0x22, + "left" => 0x25, "right" => 0x27, "up" => 0x26, "down" => 0x28, + _ => return None, + }; + } + } + } + } + + if vk_code == 0 { + return None; + } + + Some((modifiers, vk_code)) +} + +fn check_hotkey_occupied(modifiers: u32, vk_code: u32) -> bool { + unsafe { + let result = RegisterHotKey( + HWND(0), + 1, + windows::Win32::UI::Input::KeyboardAndMouse::HOT_KEY_MODIFIERS(modifiers), + vk_code, + ); + if result.is_err() { + true + } else { + let _ = UnregisterHotKey(HWND(0), 1); + false + } + } +} + +fn get_hotkey_string(modifiers: u32, vk_code: u32) -> String { + let mut parts = Vec::new(); + if (modifiers & MOD_ALT.0) != 0 { parts.push(String::from("Alt")); } + if (modifiers & MOD_CONTROL.0) != 0 { parts.push(String::from("Ctrl")); } + if (modifiers & MOD_SHIFT.0) != 0 { parts.push(String::from("Shift")); } + if (modifiers & MOD_WIN.0) != 0 { parts.push(String::from("Win")); } + + let key = if vk_code >= 0x41 && vk_code <= 0x5A { + (vk_code as u8 as char).to_string() + } else if vk_code >= 0x30 && vk_code <= 0x39 { + (vk_code as u8 as char).to_string() + } else if vk_code >= 0x70 && vk_code <= 0x7B { + format!("F{}", vk_code - 0x6F) + } else { + match vk_code { + 0x20 => "Space", 0x0D => "Enter", 0x09 => "Tab", 0x08 => "Backspace", + 0x2E => "Delete", 0x2D => "Insert", 0x24 => "Home", 0x23 => "End", + 0x21 => "PageUp", 0x22 => "PageDown", 0x25 => "Left", 0x27 => "Right", + 0x26 => "Up", 0x28 => "Down", _ => "", + }.to_string() + }; + + parts.push(key); + parts.join("+") +} + +#[derive(Clone)] +struct ProcessEntry { + pid: u32, + name: String, +} + +fn get_visible_window_processes() -> Vec { + let mut processes = Vec::new(); + let mut seen_pids: HashSet = HashSet::new(); + + unsafe { + let data = (&mut processes, &mut seen_pids); + let data_ptr = &data as *const _ as *mut (); + let _ = EnumWindows(Some(enum_windows_callback), LPARAM(data_ptr as isize)); + } + + processes.sort_by(|a: &ProcessEntry, b: &ProcessEntry| a.name.to_lowercase().cmp(&b.name.to_lowercase())); + processes.dedup_by(|a, b| a.name == b.name); + processes +} + +unsafe extern "system" fn enum_windows_callback(hwnd: HWND, lparam: LPARAM) -> BOOL { + unsafe { + if !IsWindowVisible(hwnd).as_bool() { + return TRUE; + } + + let data = &mut *(lparam.0 as *mut (&mut Vec, &mut HashSet)); + let (processes, seen_pids) = data; + + let mut pid = 0u32; + GetWindowThreadProcessId(hwnd, Some(&mut pid)); + + if seen_pids.contains(&pid) { + return TRUE; + } + + // 获取进程名 + let snapshot = match CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) { + Ok(h) => h, + Err(_) => return TRUE, + }; + + let mut pe32 = PROCESSENTRY32 { + dwSize: std::mem::size_of::() as u32, + ..Default::default() + }; + + if Process32First(snapshot, &mut pe32).is_ok() { + loop { + if pe32.th32ProcessID == pid { + let len = pe32.szExeFile.iter().position(|&c| c == 0).unwrap_or(pe32.szExeFile.len()); + let name = String::from_utf8_lossy(std::slice::from_raw_parts( + pe32.szExeFile.as_ptr() as *const u8, + len, + )).to_string(); + if !name.is_empty() { + processes.push(ProcessEntry { pid, name }); + } + break; + } + if Process32Next(snapshot, &mut pe32).is_err() { + break; + } + } + } + + seen_pids.insert(pid); + } + TRUE +} + +fn is_known_hotkey_app(name: &str) -> bool { + KNOWN_HOTKEY_APPS.iter().any(|&app| name.eq_ignore_ascii_case(app)) +} + +fn kill_process(pid: u32) -> bool { + Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/F"]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +fn start_process(name: &str) -> bool { + Command::new("cmd") + .args(["/C", "start", "", name]) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +fn find_hotkey_owner(modifiers: u32, vk_code: u32) { + let hotkey_display = get_hotkey_string(modifiers, vk_code); + + if !check_hotkey_occupied(modifiers, vk_code) { + println!("✅ 热键 {} 未被占用", hotkey_display); + return; + } + + println!("🔍 正在查找占用 {} 的程序...\n", hotkey_display); + println!("策略: 逐个关闭候选程序,每次关闭后检查热键是否释放"); + println!(" 如果释放,则该程序就是占用者,会自动重启它\n"); + + // 构建候选列表:已知热键程序优先 + let all_procs = get_visible_window_processes(); + let mut candidates: Vec = Vec::new(); + let mut others: Vec = Vec::new(); + + for proc in all_procs { + // 跳过自身和关键系统进程 + if proc.name.eq_ignore_ascii_case("hotkey.exe") + || proc.name.eq_ignore_ascii_case("explorer.exe") + || proc.name.eq_ignore_ascii_case("svchost.exe") + || proc.name.eq_ignore_ascii_case("csrss.exe") + || proc.name.eq_ignore_ascii_case("lsass.exe") + || proc.name.eq_ignore_ascii_case("services.exe") + || proc.name.eq_ignore_ascii_case("smss.exe") + || proc.name.eq_ignore_ascii_case("wininit.exe") + || proc.name.eq_ignore_ascii_case("winlogon.exe") + || proc.name.eq_ignore_ascii_case("dwm.exe") + || proc.name.eq_ignore_ascii_case("System") + || proc.name.eq_ignore_ascii_case("Registry") + || proc.name.eq_ignore_ascii_case("Memory Compression") + || proc.name.eq_ignore_ascii_case("Secure System") + { + continue; + } + + if is_known_hotkey_app(&proc.name) { + candidates.push(proc); + } else { + others.push(proc); + } + } + + // 已知热键程序排在前面 + candidates.extend(others); + + if candidates.is_empty() { + println!("❌ 没有找到可测试的候选程序"); + return; + } + + println!("📋 候选程序列表(已知热键程序优先):"); + for (i, proc) in candidates.iter().enumerate() { + let tag = if is_known_hotkey_app(&proc.name) { " [已知热键程序]" } else { "" }; + println!(" {}. {} (PID: {}){}", i + 1, proc.name, proc.pid, tag); + } + println!(); + + let mut closed_apps: Vec<(u32, String)> = Vec::new(); + let mut found = false; + + for (i, proc) in candidates.iter().enumerate() { + let tag = if is_known_hotkey_app(&proc.name) { " [已知热键程序]" } else { "" }; + println!("[{}/{}] 正在关闭 {} (PID: {}){}...", i + 1, candidates.len(), proc.name, proc.pid, tag); + + if !kill_process(proc.pid) { + println!(" ⚠️ 无法关闭,跳过"); + continue; + } + + // 等待进程完全退出 + std::thread::sleep(std::time::Duration::from_millis(500)); + + if !check_hotkey_occupied(modifiers, vk_code) { + println!("\n🎯 找到了!占用 {} 的程序是: {} (PID: {})", hotkey_display, proc.name, proc.pid); + found = true; + break; + } else { + println!(" ❌ 关闭后热键仍被占用,继续..."); + closed_apps.push((proc.pid, proc.name.clone())); + } + } + + // 重启已关闭的程序 + if !closed_apps.is_empty() { + println!("\n♻️ 正在重启之前关闭的程序..."); + for (_, name) in &closed_apps { + if start_process(name) { + println!(" ✅ 已重启 {}", name); + } else { + println!(" ⚠️ 无法重启 {}", name); + } + } + } + + if !found { + println!("\n❌ 未能找到占用者。可能被系统服务或驱动级程序占用。"); + } +} + +fn watch_hotkey(modifiers: u32, vk_code: u32) { + let hotkey_display = get_hotkey_string(modifiers, vk_code); + + if !check_hotkey_occupied(modifiers, vk_code) { + println!("✅ 热键 {} 未被占用", hotkey_display); + return; + } + + println!("👁️ 正在监控热键 {} 的状态...", hotkey_display); + println!(" 关闭占用的程序后,将自动检测到释放\n"); + + let mut last_procs = get_visible_window_processes(); + loop { + std::thread::sleep(std::time::Duration::from_millis(1000)); + + if !check_hotkey_occupied(modifiers, vk_code) { + // 检查哪些进程消失了 + let current_procs = get_visible_window_processes(); + let current_names: HashSet = current_procs.iter().map(|p| p.name.clone()).collect(); + let disappeared: Vec<&ProcessEntry> = last_procs.iter() + .filter(|p| !current_names.contains(&p.name)) + .collect(); + + println!("\n🎯 热键 {} 已释放!", hotkey_display); + if !disappeared.is_empty() { + println!("📋 最近关闭的程序(可能的占用者):"); + for proc in disappeared { + println!(" - {} (PID: {})", proc.name, proc.pid); + } + } + return; + } + + // 更新进程列表 + last_procs = get_visible_window_processes(); + print!("."); + std::io::Write::flush(&mut std::io::stdout()).ok(); + } +} + +fn check_common_hotkeys() -> Vec<(String, bool)> { + let common_hotkeys = [ + (MOD_ALT.0 | MOD_CONTROL.0, 0x44, "Alt+Ctrl+D"), + (MOD_ALT.0, 0x51, "Alt+Q"), + (MOD_ALT.0, 0x46, "Alt+F"), + (MOD_ALT.0, 0x43, "Alt+C"), + (MOD_CONTROL.0, 0x43, "Ctrl+C"), + (MOD_CONTROL.0, 0x56, "Ctrl+V"), + (MOD_CONTROL.0, 0x58, "Ctrl+X"), + (MOD_CONTROL.0, 0x46, "Ctrl+F"), + (MOD_CONTROL.0, 0x53, "Ctrl+S"), + (MOD_CONTROL.0, 0x4E, "Ctrl+N"), + (MOD_CONTROL.0, 0x57, "Ctrl+W"), + (MOD_CONTROL.0, 0x54, "Ctrl+T"), + (MOD_WIN.0, 0x45, "Win+E"), + (MOD_WIN.0, 0x44, "Win+D"), + (MOD_WIN.0, 0x52, "Win+R"), + (MOD_WIN.0, 0x4C, "Win+L"), + (MOD_WIN.0, 0x54, "Win+T"), + (MOD_WIN.0, 0x41, "Win+A"), + (MOD_WIN.0, 0x53, "Win+S"), + (MOD_ALT.0, 0x42, "Alt+B"), + (MOD_ALT.0, 0x54, "Alt+T"), + (MOD_ALT.0, 0x56, "Alt+V"), + (MOD_ALT.0, 0x57, "Alt+W"), + (MOD_CONTROL.0 | MOD_SHIFT.0, 0x43, "Ctrl+Shift+C"), + (MOD_CONTROL.0 | MOD_SHIFT.0, 0x56, "Ctrl+Shift+V"), + (MOD_CONTROL.0, 0x41, "Ctrl+A"), + (MOD_CONTROL.0, 0x4B, "Ctrl+K"), + (MOD_ALT.0, 0x53, "Alt+S"), + (MOD_ALT.0, 0x4E, "Alt+N"), + (MOD_ALT.0, 0x50, "Alt+P"), + ]; + + let mut results: Vec<(String, bool)> = common_hotkeys.iter().map(|&(m, v, name)| { + (name.to_string(), check_hotkey_occupied(m, v)) + }).collect(); + + results.sort_by(|a, b| a.0.cmp(&b.0)); + results +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + + if let Some(hotkey_str) = &args.hotkey { + match parse_hotkey(hotkey_str) { + Some((modifiers, vk_code)) => { + let hotkey_display = get_hotkey_string(modifiers, vk_code); + + if args.find { + find_hotkey_owner(modifiers, vk_code); + } else if args.watch { + watch_hotkey(modifiers, vk_code); + } else { + let occupied = check_hotkey_occupied(modifiers, vk_code); + + if occupied { + println!("❌ 热键 {} 已被占用", hotkey_display); + + let procs = get_visible_window_processes(); + + // 优先显示已知热键程序 + let mut known = Vec::new(); + let mut other = Vec::new(); + for proc in &procs { + if is_known_hotkey_app(&proc.name) { + known.push(proc); + } else { + other.push(proc); + } + } + + if !known.is_empty() { + println!("\n🔴 高度可疑(已知会注册热键的程序):"); + for (i, proc) in known.iter().enumerate() { + println!(" {}. {} (PID: {})", i + 1, proc.name, proc.pid); + } + } + + if !other.is_empty() { + println!("\n🟡 其他运行中的程序:"); + for (i, proc) in other.iter().enumerate() { + println!(" {}. {} (PID: {})", i + 1, proc.name, proc.pid); + } + } + + println!("\n💡 使用以下命令精确定位:"); + println!(" hotkey {} --find 自动逐个关闭程序定位占用者(会自动重启)", hotkey_str); + println!(" hotkey {} --watch 持续监控,手动关闭程序后自动检测", hotkey_str); + } else { + println!("✅ 热键 {} 未被占用,可以使用", hotkey_display); + } + } + } + None => { + eprintln!("❌ 无效的热键格式: {}", hotkey_str); + eprintln!("💡 正确格式示例: alt+q, ctrl+c, win+e, alt+ctrl+delete"); + std::process::exit(1); + } + } + } else { + println!("⌨️ 热键查询工具\n"); + println!("正在检测当前系统中的热键占用情况...\n"); + + let hotkeys = check_common_hotkeys(); + + println!("{:<30} {:<12}", "热键", "状态"); + println!("{}", "-".repeat(45)); + + for (key, occupied) in hotkeys { + let status = if occupied { "🔴 已占用" } else { "🟢 可用" }; + println!("{:<30} {:<12}", key, status); + } + + println!("\n💡 提示:"); + println!(" hotkey <热键> 查询特定热键占用情况"); + println!(" hotkey <热键> --find 自动定位占用者(逐个关闭后检测,会自动重启)"); + println!(" hotkey <热键> --watch 监控模式(手动关闭程序后自动检测释放)"); + } + + Ok(()) +}