feat(热键工具): 1. 新增热键查询工具 2. 支持查询热键占用情况 3. 提供自动定位占用者和监控模式 4. 添加常见热键程序列表

This commit is contained in:
macro committed 2026-10-07 14:50:57 +08:00
1 parent 55a308a927
commit 2f97391743
2 files changed
+508

No files matched your search

+9
View File
@@ -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"] }
+499
View File
@@ -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<String>,
/// 自动逐个关闭候选程序来精确定位占用者
#[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<ProcessEntry> {
let mut processes = Vec::new();
let mut seen_pids: HashSet<u32> = 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<ProcessEntry>, &mut HashSet<u32>));
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::<PROCESSENTRY32>() 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<ProcessEntry> = Vec::new();
let mut others: Vec<ProcessEntry> = 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<String> = 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<dyn std::error::Error>> {
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(())
}