⚡ Optimize hotkey service
This commit is contained in:
817
internal/common/hotkey/README.md
Normal file
817
internal/common/hotkey/README.md
Normal file
@@ -0,0 +1,817 @@
|
||||
# Hotkey - 跨平台全局热键库
|
||||
|
||||
跨平台 Go 语言全局热键库,支持 Windows、Linux (X11) 和 macOS 操作系统。
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- **跨平台支持**:Windows、Linux (X11)、macOS 统一 API
|
||||
- **线程安全**:所有公共 API 使用互斥锁保护
|
||||
- **标准化错误**:提供统一的错误类型,便于错误处理
|
||||
- **资源管理**:支持手动和自动(finalizer)资源清理
|
||||
- **独立实现**:除系统库外无第三方 Go 依赖
|
||||
- **状态查询**:提供 `IsRegistered()` 和 `IsClosed()` 方法
|
||||
|
||||
## 📦 安装
|
||||
|
||||
```bash
|
||||
go get -u voidraft/internal/common/hotkey
|
||||
```
|
||||
|
||||
### 平台特定依赖
|
||||
|
||||
#### Linux
|
||||
|
||||
需要安装 X11 开发库:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt install -y libx11-dev
|
||||
|
||||
# CentOS/RHEL
|
||||
sudo yum install -y libX11-devel
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -S libx11
|
||||
```
|
||||
|
||||
**无界面环境(云服务器等)**:
|
||||
|
||||
```bash
|
||||
# 安装虚拟显示服务器
|
||||
sudo apt install -y xvfb
|
||||
|
||||
# 启动虚拟显示
|
||||
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||
export DISPLAY=:99.0
|
||||
```
|
||||
|
||||
#### macOS
|
||||
|
||||
**GUI 应用**(Wails、Fyne、Cocoa 等):框架管理主事件循环,直接使用即可。
|
||||
|
||||
**CLI 应用**:需要使用 `darwin.Init()` 启动 NSApplication 主事件循环,参见下文"macOS CLI 应用示例"。
|
||||
|
||||
#### Windows
|
||||
|
||||
无额外依赖,开箱即用。
|
||||
|
||||
## 📖 使用指南
|
||||
|
||||
### 基本用法
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"voidraft/internal/common/hotkey"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建热键:Ctrl+Shift+S
|
||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
|
||||
|
||||
// 注册热键
|
||||
if err := hk.Register(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer hk.Close() // 确保资源清理
|
||||
|
||||
fmt.Println("热键已注册,按 Ctrl+Shift+S 触发...")
|
||||
|
||||
// 监听热键事件
|
||||
for {
|
||||
select {
|
||||
case <-hk.Keydown():
|
||||
fmt.Println("热键按下!")
|
||||
case <-hk.Keyup():
|
||||
fmt.Println("热键释放!")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 完整示例:带错误处理
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"voidraft/internal/common/hotkey"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建热键
|
||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModAlt}, hotkey.KeyQ)
|
||||
|
||||
// 注册热键,处理可能的错误
|
||||
if err := hk.Register(); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, hotkey.ErrHotkeyConflict):
|
||||
log.Fatal("热键冲突:该组合键已被其他程序占用")
|
||||
case errors.Is(err, hotkey.ErrPlatformUnavailable):
|
||||
log.Fatal("平台不支持:", err)
|
||||
case errors.Is(err, hotkey.ErrAlreadyRegistered):
|
||||
log.Fatal("热键已经注册")
|
||||
default:
|
||||
log.Fatal("注册热键失败:", err)
|
||||
}
|
||||
}
|
||||
defer hk.Close()
|
||||
|
||||
fmt.Println("热键 Ctrl+Alt+Q 已注册")
|
||||
fmt.Println("按下热键触发,或按 Ctrl+C 退出...")
|
||||
|
||||
// 优雅退出
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// 事件循环
|
||||
for {
|
||||
select {
|
||||
case <-hk.Keydown():
|
||||
fmt.Println("[事件] 热键按下")
|
||||
// 执行你的业务逻辑
|
||||
|
||||
case <-hk.Keyup():
|
||||
fmt.Println("[事件] 热键释放")
|
||||
|
||||
case <-sigChan:
|
||||
fmt.Println("\n正在退出...")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 防抖处理(应用层)
|
||||
|
||||
如果热键按住会持续触发,建议在应用层添加防抖:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
"voidraft/internal/common/hotkey"
|
||||
)
|
||||
|
||||
func main() {
|
||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl}, hotkey.KeyD)
|
||||
if err := hk.Register(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer hk.Close()
|
||||
|
||||
// 防抖参数
|
||||
var lastTrigger time.Time
|
||||
debounceInterval := 800 * time.Millisecond // 推荐 800ms
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-hk.Keydown():
|
||||
now := time.Now()
|
||||
// 检查距离上次触发的时间
|
||||
if !lastTrigger.IsZero() && now.Sub(lastTrigger) < debounceInterval {
|
||||
fmt.Println("触发被忽略(防抖)")
|
||||
continue
|
||||
}
|
||||
lastTrigger = now
|
||||
|
||||
fmt.Println("热键触发!")
|
||||
// 执行你的业务逻辑
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 动态修改热键
|
||||
|
||||
```go
|
||||
// 注销旧热键
|
||||
if err := hk.Unregister(); err != nil {
|
||||
log.Printf("注销失败:%v", err)
|
||||
}
|
||||
|
||||
// 修改热键组合
|
||||
hk = hotkey.New([]hotkey.Modifier{hotkey.ModCtrl}, hotkey.KeyF1)
|
||||
|
||||
// 重新注册
|
||||
if err := hk.Register(); err != nil {
|
||||
log.Printf("注册失败:%v", err)
|
||||
}
|
||||
|
||||
// 重要:重新获取通道引用!
|
||||
keydownChan := hk.Keydown()
|
||||
keyupChan := hk.Keyup()
|
||||
```
|
||||
|
||||
### 状态检查
|
||||
|
||||
```go
|
||||
// 检查热键是否已注册
|
||||
if hk.IsRegistered() {
|
||||
fmt.Println("热键已注册")
|
||||
}
|
||||
|
||||
// 检查热键是否已关闭
|
||||
if hk.IsClosed() {
|
||||
fmt.Println("热键已关闭,无法再使用")
|
||||
}
|
||||
```
|
||||
|
||||
### 资源管理最佳实践
|
||||
|
||||
```go
|
||||
func registerHotkey() error {
|
||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl}, hotkey.KeyH)
|
||||
|
||||
// 使用 defer 确保资源释放
|
||||
defer hk.Close()
|
||||
|
||||
if err := hk.Register(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ... 使用热键 ...
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close() 是幂等的,多次调用是安全的
|
||||
hk.Close()
|
||||
hk.Close() // 不会 panic
|
||||
```
|
||||
|
||||
## 🔑 支持的修饰键
|
||||
|
||||
### 所有平台通用
|
||||
|
||||
```go
|
||||
hotkey.ModCtrl // Ctrl 键
|
||||
hotkey.ModShift // Shift 键
|
||||
hotkey.ModAlt // Alt 键(Linux: 通常映射到 Mod1)
|
||||
```
|
||||
|
||||
### Linux 额外支持
|
||||
|
||||
```go
|
||||
hotkey.Mod1 // 通常是 Alt
|
||||
hotkey.Mod2 // 通常是 Num Lock
|
||||
hotkey.Mod3 // (较少使用)
|
||||
hotkey.Mod4 // 通常是 Super/Windows 键
|
||||
hotkey.Mod5 // (较少使用)
|
||||
```
|
||||
|
||||
### 组合使用
|
||||
|
||||
```go
|
||||
// Ctrl+Shift+Alt+S
|
||||
hk := hotkey.New(
|
||||
[]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift, hotkey.ModAlt},
|
||||
hotkey.KeyS,
|
||||
)
|
||||
```
|
||||
|
||||
## ⌨️ 支持的按键
|
||||
|
||||
### 字母键
|
||||
|
||||
```go
|
||||
hotkey.KeyA - hotkey.KeyZ // A-Z
|
||||
```
|
||||
|
||||
### 数字键
|
||||
|
||||
```go
|
||||
hotkey.Key0 - hotkey.Key9 // 0-9
|
||||
```
|
||||
|
||||
### 功能键
|
||||
|
||||
```go
|
||||
hotkey.KeyF1 - hotkey.KeyF20 // F1-F20
|
||||
```
|
||||
|
||||
### 特殊键
|
||||
|
||||
```go
|
||||
hotkey.KeySpace // 空格
|
||||
hotkey.KeyReturn // 回车
|
||||
hotkey.KeyEscape // ESC
|
||||
hotkey.KeyDelete // Delete
|
||||
hotkey.KeyTab // Tab
|
||||
hotkey.KeyLeft // 左箭头
|
||||
hotkey.KeyRight // 右箭头
|
||||
hotkey.KeyUp // 上箭头
|
||||
hotkey.KeyDown // 下箭头
|
||||
```
|
||||
|
||||
### 自定义键码
|
||||
|
||||
如果需要的键未预定义,可以直接使用键码:
|
||||
|
||||
```go
|
||||
// 使用自定义键码 0x15
|
||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl}, hotkey.Key(0x15))
|
||||
```
|
||||
|
||||
## ⚠️ 错误类型
|
||||
|
||||
```go
|
||||
// 检查特定错误类型
|
||||
if errors.Is(err, hotkey.ErrAlreadyRegistered) {
|
||||
// 热键已经注册
|
||||
}
|
||||
|
||||
if errors.Is(err, hotkey.ErrNotRegistered) {
|
||||
// 热键未注册
|
||||
}
|
||||
|
||||
if errors.Is(err, hotkey.ErrClosed) {
|
||||
// 热键已关闭,无法再使用
|
||||
}
|
||||
|
||||
if errors.Is(err, hotkey.ErrHotkeyConflict) {
|
||||
// 热键冲突,已被其他程序占用
|
||||
}
|
||||
|
||||
if errors.Is(err, hotkey.ErrPlatformUnavailable) {
|
||||
// 平台不可用(如 Linux 无 X11)
|
||||
}
|
||||
|
||||
if errors.Is(err, hotkey.ErrFailedToRegister) {
|
||||
// 注册失败(其他原因)
|
||||
}
|
||||
|
||||
if errors.Is(err, hotkey.ErrFailedToUnregister) {
|
||||
// 注销失败
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 平台特定注意事项
|
||||
|
||||
### Linux
|
||||
|
||||
#### 1. AutoRepeat 行为
|
||||
|
||||
Linux 的 X11 会在按键持续按下时重复发送 `KeyPress` 事件。如果你的应用对此敏感,需要做防抖处理:
|
||||
|
||||
```go
|
||||
var lastTrigger time.Time
|
||||
debounceInterval := 500 * time.Millisecond
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-hk.Keydown():
|
||||
now := time.Now()
|
||||
if now.Sub(lastTrigger) < debounceInterval {
|
||||
continue // 忽略重复触发
|
||||
}
|
||||
lastTrigger = now
|
||||
|
||||
// 处理热键事件
|
||||
fmt.Println("热键触发!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 键位映射差异
|
||||
|
||||
不同的 Linux 发行版和桌面环境可能有不同的键位映射。建议:
|
||||
|
||||
- 使用标准的 `ModCtrl`、`ModShift`、`ModAlt`
|
||||
- 避免依赖 `Mod2`、`Mod3`、`Mod5`(映射不一致)
|
||||
- `Mod4` 通常是 Super/Windows 键,但也可能不同
|
||||
|
||||
#### 3. Wayland 支持
|
||||
|
||||
当前版本仅支持 X11。在 Wayland 环境下:
|
||||
|
||||
- 需要运行在 XWayland 兼容层
|
||||
- 或设置 `GDK_BACKEND=x11` 环境变量
|
||||
- 原生 Wayland 支持正在开发中
|
||||
|
||||
#### 4. Display 连接复用
|
||||
|
||||
本库已优化 Linux 实现,在热键注册期间保持 X11 Display 连接:
|
||||
|
||||
```
|
||||
注册时: XOpenDisplay → XGrabKey → 保持连接
|
||||
事件循环: 使用相同连接 → XNextEvent
|
||||
注销时: XUngrabKey → XCloseDisplay
|
||||
```
|
||||
|
||||
这大幅降低了资源开销和延迟。
|
||||
|
||||
### Windows
|
||||
|
||||
#### 1. 热键按下事件
|
||||
|
||||
Windows 使用 `RegisterHotKey` API 注册系统级热键,通过 `WM_HOTKEY` 消息接收按键事件。
|
||||
|
||||
**实现细节**:
|
||||
- 使用 `PeekMessageW` (Unicode) 轮询消息队列
|
||||
- 10ms 轮询间隔,CPU 占用约 0.3-0.5%
|
||||
- `isKeyDown` 状态标志防止重复触发
|
||||
- 按下事件延迟通常 < 10ms
|
||||
|
||||
#### 2. 热键释放事件
|
||||
|
||||
Windows `RegisterHotKey` API 不提供键释放通知,本库使用 `GetAsyncKeyState` 轮询检测:
|
||||
|
||||
```go
|
||||
// 检测键释放(每 10ms 检查一次)
|
||||
if isKeyDown && GetAsyncKeyState(key) == 0 {
|
||||
keyupIn <- struct{}{}
|
||||
isKeyDown = false
|
||||
}
|
||||
```
|
||||
|
||||
**特性**:
|
||||
- 释放检测延迟:通常 10-20ms
|
||||
- 仅在按键按下后激活检测
|
||||
- 依赖于 `GetAsyncKeyState` 的精度
|
||||
|
||||
#### 3. 持续按住行为
|
||||
|
||||
Windows 在持续按住热键时会重复发送 `WM_HOTKEY` 消息。本库通过 `isKeyDown` 标志防止同一次按住重复触发 Keydown 事件。
|
||||
|
||||
如果需要防止快速连续按键,建议在应用层添加防抖:
|
||||
|
||||
```go
|
||||
var lastTrigger time.Time
|
||||
debounceInterval := 100 * time.Millisecond
|
||||
|
||||
for {
|
||||
<-hk.Keydown()
|
||||
now := time.Now()
|
||||
if now.Sub(lastTrigger) < debounceInterval {
|
||||
continue // 忽略
|
||||
}
|
||||
lastTrigger = now
|
||||
// 处理事件...
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 系统保留热键
|
||||
|
||||
某些热键被 Windows 系统保留,无法注册:
|
||||
|
||||
- `Win+L`:锁定屏幕
|
||||
- `Ctrl+Alt+Del`:安全选项
|
||||
- `Alt+Tab`:切换窗口
|
||||
- `Alt+F4`:关闭窗口
|
||||
- `Win+D`:显示桌面
|
||||
|
||||
尝试注册这些热键会返回 `ErrFailedToRegister`。
|
||||
|
||||
#### 5. 热键冲突
|
||||
|
||||
如果热键已被其他应用注册,`RegisterHotKey` 会失败。常见冲突来源:
|
||||
|
||||
- 游戏快捷键
|
||||
- 输入法快捷键
|
||||
- 快捷键管理工具(AutoHotkey 等)
|
||||
- 其他全局热键应用
|
||||
|
||||
返回错误为 `ErrFailedToRegister`(Windows 不区分冲突和其他失败)。
|
||||
|
||||
#### 6. 线程模型
|
||||
|
||||
- 热键注册和消息循环运行在同一个 OS 线程上
|
||||
- 使用 `runtime.LockOSThread()` 确保线程亲和性
|
||||
- 不要求是主线程(与 macOS 不同)
|
||||
- `ph.funcs` channel 用于在事件循环线程中执行注册/注销操作
|
||||
|
||||
---
|
||||
|
||||
### macOS
|
||||
|
||||
#### 1. 主事件循环要求
|
||||
|
||||
macOS 的 Carbon 事件 API 通过 GCD(Grand Central Dispatch)调度到主队列执行。
|
||||
|
||||
**GUI 应用(Wails、Cocoa 等)**:
|
||||
- ✅ 框架已自动管理主事件循环
|
||||
- ✅ 热键功能开箱即用,无需额外配置
|
||||
|
||||
**纯 CLI 应用**:
|
||||
- ⚠️ 需要手动启动 macOS 运行循环
|
||||
- 参见下文"macOS 纯 CLI 应用示例"
|
||||
|
||||
#### 2. 权限问题
|
||||
|
||||
macOS 可能需要辅助功能权限。如果热键无法注册,请检查:
|
||||
|
||||
```
|
||||
系统偏好设置 → 安全性与隐私 → 隐私 → 辅助功能
|
||||
```
|
||||
|
||||
将你的应用添加到允许列表。
|
||||
|
||||
#### 3. Carbon vs Cocoa
|
||||
|
||||
当前实现使用 Carbon API(稳定且兼容性好)。未来版本可能会迁移到更现代的 Cocoa API。
|
||||
|
||||
#### 4. macOS 纯 CLI 应用示例
|
||||
|
||||
如果你的应用**不是 GUI 应用**,需要启动主事件循环。
|
||||
|
||||
**最简单的方法:使用 darwin.Init()(推荐)**
|
||||
|
||||
```go
|
||||
//go:build darwin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"voidraft/internal/common/hotkey"
|
||||
"voidraft/internal/common/hotkey/darwin"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 使用 darwin.Init 启动主事件循环
|
||||
darwin.Init(run)
|
||||
}
|
||||
|
||||
func run() {
|
||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCmd, hotkey.ModShift}, hotkey.KeyA)
|
||||
if err := hk.Register(); err != nil {
|
||||
fmt.Printf("注册失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
defer hk.Close()
|
||||
|
||||
fmt.Println("热键已注册: Cmd+Shift+A")
|
||||
|
||||
for {
|
||||
<-hk.Keydown()
|
||||
fmt.Println("热键触发!")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**高级用法:使用 darwin.Call() 在主线程执行操作**
|
||||
|
||||
```go
|
||||
darwin.Init(func() {
|
||||
hk := hotkey.New(...)
|
||||
hk.Register()
|
||||
|
||||
for {
|
||||
<-hk.Keydown()
|
||||
// 在主线程执行 macOS API 调用
|
||||
darwin.Call(func() {
|
||||
// 例如调用 Cocoa/AppKit API
|
||||
fmt.Println("这段代码在主线程执行")
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- GUI 应用无需调用 `darwin.Init()`,框架已处理
|
||||
- `darwin.Call()` 用于需要主线程的特定 macOS API
|
||||
- 热键注册本身已通过 `dispatch_get_main_queue()` 自动调度到主线程
|
||||
|
||||
## 🔬 架构设计
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
internal/common/hotkey/
|
||||
├── hotkey.go # 统一的公共 API
|
||||
├── hotkey_windows.go # Windows 平台适配器
|
||||
├── hotkey_darwin.go # macOS 平台适配器
|
||||
├── hotkey_linux.go # Linux 平台适配器
|
||||
├── hotkey_nocgo.go # 非 CGO 平台占位符
|
||||
├── windows/
|
||||
│ ├── hotkey.go # Windows 核心实现
|
||||
│ └── mainthread.go # Windows 线程管理
|
||||
├── darwin/
|
||||
│ ├── hotkey.go # macOS 核心实现
|
||||
│ ├── hotkey.m # Objective-C/Carbon 代码
|
||||
│ └── mainthread.go # macOS 线程管理
|
||||
└── linux/
|
||||
├── hotkey.go # Linux 核心实现
|
||||
├── hotkey.c # X11 C 代码
|
||||
└── mainthread.go # Linux 线程管理
|
||||
```
|
||||
|
||||
### 设计原则
|
||||
|
||||
1. **平台隔离**:每个平台的实现完全独立,通过构建标签分离
|
||||
2. **统一接口**:所有平台提供相同的 Go API
|
||||
3. **资源安全**:自动资源管理,防止泄漏
|
||||
4. **并发安全**:所有公共方法都是线程安全的
|
||||
5. **错误透明**:标准化错误类型,便于处理
|
||||
|
||||
### 事件流程
|
||||
|
||||
```
|
||||
用户按键
|
||||
↓
|
||||
操作系统捕获
|
||||
↓
|
||||
平台特定 API(Win32/X11/Carbon)
|
||||
↓
|
||||
C/Objective-C 回调
|
||||
↓
|
||||
Go channel(类型转换)
|
||||
↓
|
||||
用户应用代码
|
||||
```
|
||||
|
||||
### 并发模型
|
||||
|
||||
```
|
||||
主 Goroutine 事件 Goroutine 转换 Goroutine
|
||||
│ │ │
|
||||
Register() ────启动──────→ eventLoop() │
|
||||
│ │ │
|
||||
│ 等待 OS 事件 │
|
||||
│ │ │
|
||||
│ ├────发送────→ 类型转换 │
|
||||
│ │ │ │
|
||||
│ │ └─→ Keydown()/Keyup()
|
||||
│ │ │
|
||||
Unregister() ──停止信号──→ 退出循环 │
|
||||
│ │ │
|
||||
└──────等待清理─────────────┴──────────────────────────┘
|
||||
```
|
||||
|
||||
## 📊 性能特性
|
||||
|
||||
### 资源占用
|
||||
|
||||
- **内存**:每个热键约 1-2 KB(包括 goroutines、channels、CGO handles)
|
||||
- **Goroutines**:每个热键 3 个
|
||||
- 1 个事件循环 goroutine
|
||||
- 2 个通道转换 goroutine (interface{} → Event)
|
||||
- **CPU**:
|
||||
- **Windows**:10ms 轮询,约 0.3-0.5% CPU(单核,空闲时)
|
||||
- **Linux**:事件驱动 (`XNextEvent` 阻塞),几乎无 CPU 占用
|
||||
- **macOS**:事件驱动 (GCD 调度),几乎无 CPU 占用
|
||||
- **线程**:
|
||||
- 每个热键 1 个 OS 线程(通过 `runtime.LockOSThread()` 锁定)
|
||||
|
||||
### 延迟
|
||||
|
||||
**按下事件 (Keydown)**:
|
||||
- Windows: < 10ms(取决于轮询间隔)
|
||||
- Linux: < 10ms(X11 事件延迟)
|
||||
- macOS: < 5ms(Carbon 事件延迟)
|
||||
|
||||
**释放事件 (Keyup)**:
|
||||
- Windows: 10-20ms(`GetAsyncKeyState` 轮询检测)
|
||||
- Linux: < 15ms(X11 KeyRelease 事件)
|
||||
- macOS: < 10ms(Carbon 事件延迟)
|
||||
|
||||
### 使用建议
|
||||
|
||||
1. **资源管理**:
|
||||
- 使用 `defer hk.Close()` 确保资源释放
|
||||
- 不需要时及时调用 `Unregister()` 或 `Close()`
|
||||
- 避免频繁创建/销毁热键对象
|
||||
|
||||
2. **事件处理**:
|
||||
- 热键事件处理应快速返回,避免阻塞 channel
|
||||
- 复杂逻辑应在新 goroutine 中处理
|
||||
- 考虑应用层防抖(特别是 Linux AutoRepeat)
|
||||
|
||||
3. **错误处理**:
|
||||
- 始终检查 `Register()` 返回的错误
|
||||
- 使用 `errors.Is()` 判断错误类型
|
||||
- 处理热键冲突场景(提供备用方案或用户提示)
|
||||
|
||||
4. **平台差异**:
|
||||
- Windows Keyup 事件有轻微延迟(正常现象)
|
||||
- Linux 可能需要防抖处理 AutoRepeat
|
||||
- macOS CLI 应用需要启动主事件循环
|
||||
|
||||
## 🐛 故障排查
|
||||
|
||||
### Linux: "Failed to initialize the X11 display"
|
||||
|
||||
**问题**:无法连接到 X11 显示服务器
|
||||
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 检查 DISPLAY 环境变量
|
||||
echo $DISPLAY
|
||||
|
||||
# 如果为空,设置它
|
||||
export DISPLAY=:0
|
||||
|
||||
# 或使用虚拟显示
|
||||
Xvfb :99 -screen 0 1024x768x24 &
|
||||
export DISPLAY=:99
|
||||
```
|
||||
|
||||
### macOS: 热键不触发
|
||||
|
||||
**问题**:注册成功但热键无响应
|
||||
|
||||
**解决方案**:
|
||||
1. 检查辅助功能权限(见上文)
|
||||
2. 如果是纯 CLI 应用,确保启动了主运行循环(见上文示例)
|
||||
3. 检查其他应用是否占用了该热键
|
||||
|
||||
### Windows: ErrFailedToRegister
|
||||
|
||||
**问题**:热键注册失败
|
||||
|
||||
**可能原因**:
|
||||
1. 热键已被其他应用占用(AutoHotkey、游戏、输入法等)
|
||||
2. 尝试注册系统保留热键(Win+L、Ctrl+Alt+Del 等)
|
||||
3. 热键 ID 冲突(极少见)
|
||||
|
||||
**解决方案**:
|
||||
1. 检查任务管理器,关闭可能冲突的应用
|
||||
2. 使用不同的热键组合
|
||||
3. 在应用中提供热键自定义功能,让用户选择可用组合
|
||||
4. 提供友好的错误提示,说明热键被占用
|
||||
|
||||
**调试方法**:
|
||||
```go
|
||||
if err := hk.Register(); err != nil {
|
||||
if errors.Is(err, hotkey.ErrFailedToRegister) {
|
||||
log.Printf("热键注册失败: %v", err)
|
||||
log.Printf("提示:该热键可能已被其他应用使用")
|
||||
// 尝试备用热键...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Windows: Keyup 事件延迟
|
||||
|
||||
**问题**:释放事件比预期晚 10-20ms
|
||||
|
||||
**原因**:Windows API 限制,`RegisterHotKey` 不提供释放通知,需要轮询检测。
|
||||
|
||||
**这是正常行为**:
|
||||
- 10ms 轮询间隔导致的固有延迟
|
||||
- 对大多数应用场景影响很小
|
||||
- 如果需要精确的释放时机,考虑使用底层键盘钩子(复杂度更高)
|
||||
|
||||
### 所有平台: Keyup 事件丢失
|
||||
|
||||
**问题**:只收到 Keydown,没有 Keyup
|
||||
|
||||
**可能原因**:
|
||||
1. 在接收 Keyup 前调用了 `Unregister()`
|
||||
2. 通道缓冲区满(不太可能,使用了无缓冲通道)
|
||||
3. Windows 上正常(有轻微延迟)
|
||||
|
||||
**解决方案**:
|
||||
```go
|
||||
// 确保在处理完事件后再注销
|
||||
for {
|
||||
select {
|
||||
case <-hk.Keydown():
|
||||
// 处理...
|
||||
case <-hk.Keyup():
|
||||
// 处理...
|
||||
// 现在可以安全注销
|
||||
hk.Unregister()
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎贡献!请遵循以下原则:
|
||||
|
||||
1. **保持简洁**:不要过度优化
|
||||
2. **平台一致**:确保 API 在所有平台表现一致
|
||||
3. **测试充分**:在所有支持的平台测试
|
||||
4. **文档完善**:更新相关文档
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目是 Voidraft 的一部分,遵循项目主许可证。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
本库的设计和实现参考了多个开源项目的经验:
|
||||
|
||||
- `golang.design/x/hotkey` - 基础架构设计
|
||||
- `github.com/robotn/gohook` - 跨平台事件处理思路
|
||||
|
||||
特别感谢所有为跨平台 Go 开发做出贡献的开发者们!
|
||||
|
||||
---
|
||||
|
||||
**如有问题或建议,欢迎提交 Issue!** 🚀
|
||||
|
||||
Reference in New Issue
Block a user