⚡ 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!** 🚀
|
||||||
|
|
||||||
191
internal/common/hotkey/darwin/hotkey.go
Normal file
191
internal/common/hotkey/darwin/hotkey.go
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package darwin
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -x objective-c
|
||||||
|
#cgo LDFLAGS: -framework Cocoa -framework Carbon
|
||||||
|
#include <stdint.h>
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import <Carbon/Carbon.h>
|
||||||
|
|
||||||
|
extern void keydownCallback(uintptr_t handle);
|
||||||
|
extern void keyupCallback(uintptr_t handle);
|
||||||
|
int registerHotKey(int mod, int key, uintptr_t handle, EventHotKeyRef* ref);
|
||||||
|
int unregisterHotKey(EventHotKeyRef ref);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime/cgo"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Standard errors
|
||||||
|
var (
|
||||||
|
ErrAlreadyRegistered = errors.New("hotkey: already registered")
|
||||||
|
ErrNotRegistered = errors.New("hotkey: not registered")
|
||||||
|
ErrFailedToRegister = errors.New("hotkey: failed to register")
|
||||||
|
ErrFailedToUnregister = errors.New("hotkey: failed to unregister")
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlatformHotkey is a combination of modifiers and key to trigger an event
|
||||||
|
type PlatformHotkey struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
registered bool
|
||||||
|
hkref C.EventHotKeyRef
|
||||||
|
handle cgo.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ph *PlatformHotkey) Register(mods []Modifier, key Key, keydownIn, keyupIn chan<- interface{}) error {
|
||||||
|
ph.mu.Lock()
|
||||||
|
defer ph.mu.Unlock()
|
||||||
|
if ph.registered {
|
||||||
|
return ErrAlreadyRegistered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store callbacks info for C side
|
||||||
|
callbacks := &callbackData{
|
||||||
|
keydownIn: keydownIn,
|
||||||
|
keyupIn: keyupIn,
|
||||||
|
}
|
||||||
|
|
||||||
|
ph.handle = cgo.NewHandle(callbacks)
|
||||||
|
var mod Modifier
|
||||||
|
for _, m := range mods {
|
||||||
|
mod += m
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := C.registerHotKey(C.int(mod), C.int(key), C.uintptr_t(ph.handle), &ph.hkref)
|
||||||
|
if ret == C.int(-1) {
|
||||||
|
ph.handle.Delete()
|
||||||
|
return fmt.Errorf("%w: Carbon API returned error", ErrFailedToRegister)
|
||||||
|
}
|
||||||
|
|
||||||
|
ph.registered = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ph *PlatformHotkey) Unregister() error {
|
||||||
|
ph.mu.Lock()
|
||||||
|
defer ph.mu.Unlock()
|
||||||
|
if !ph.registered {
|
||||||
|
return ErrNotRegistered
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := C.unregisterHotKey(ph.hkref)
|
||||||
|
if ret == C.int(-1) {
|
||||||
|
return fmt.Errorf("%w: Carbon API returned error", ErrFailedToUnregister)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up CGO handle
|
||||||
|
ph.handle.Delete()
|
||||||
|
ph.registered = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type callbackData struct {
|
||||||
|
keydownIn chan<- interface{}
|
||||||
|
keyupIn chan<- interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export keydownCallback
|
||||||
|
func keydownCallback(h uintptr) {
|
||||||
|
cb := cgo.Handle(h).Value().(*callbackData)
|
||||||
|
cb.keydownIn <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export keyupCallback
|
||||||
|
func keyupCallback(h uintptr) {
|
||||||
|
cb := cgo.Handle(h).Value().(*callbackData)
|
||||||
|
cb.keyupIn <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifier represents a modifier.
|
||||||
|
// See: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
|
||||||
|
type Modifier uint32
|
||||||
|
|
||||||
|
// All kinds of Modifiers
|
||||||
|
const (
|
||||||
|
ModCtrl Modifier = 0x1000
|
||||||
|
ModShift Modifier = 0x200
|
||||||
|
ModOption Modifier = 0x800
|
||||||
|
ModCmd Modifier = 0x100
|
||||||
|
)
|
||||||
|
|
||||||
|
// Key represents a key.
|
||||||
|
// See: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
|
||||||
|
type Key uint8
|
||||||
|
|
||||||
|
// All kinds of keys
|
||||||
|
const (
|
||||||
|
KeySpace Key = 49
|
||||||
|
Key1 Key = 18
|
||||||
|
Key2 Key = 19
|
||||||
|
Key3 Key = 20
|
||||||
|
Key4 Key = 21
|
||||||
|
Key5 Key = 23
|
||||||
|
Key6 Key = 22
|
||||||
|
Key7 Key = 26
|
||||||
|
Key8 Key = 28
|
||||||
|
Key9 Key = 25
|
||||||
|
Key0 Key = 29
|
||||||
|
KeyA Key = 0
|
||||||
|
KeyB Key = 11
|
||||||
|
KeyC Key = 8
|
||||||
|
KeyD Key = 2
|
||||||
|
KeyE Key = 14
|
||||||
|
KeyF Key = 3
|
||||||
|
KeyG Key = 5
|
||||||
|
KeyH Key = 4
|
||||||
|
KeyI Key = 34
|
||||||
|
KeyJ Key = 38
|
||||||
|
KeyK Key = 40
|
||||||
|
KeyL Key = 37
|
||||||
|
KeyM Key = 46
|
||||||
|
KeyN Key = 45
|
||||||
|
KeyO Key = 31
|
||||||
|
KeyP Key = 35
|
||||||
|
KeyQ Key = 12
|
||||||
|
KeyR Key = 15
|
||||||
|
KeyS Key = 1
|
||||||
|
KeyT Key = 17
|
||||||
|
KeyU Key = 32
|
||||||
|
KeyV Key = 9
|
||||||
|
KeyW Key = 13
|
||||||
|
KeyX Key = 7
|
||||||
|
KeyY Key = 16
|
||||||
|
KeyZ Key = 6
|
||||||
|
|
||||||
|
KeyReturn Key = 0x24
|
||||||
|
KeyEscape Key = 0x35
|
||||||
|
KeyDelete Key = 0x33
|
||||||
|
KeyTab Key = 0x30
|
||||||
|
|
||||||
|
KeyLeft Key = 0x7B
|
||||||
|
KeyRight Key = 0x7C
|
||||||
|
KeyUp Key = 0x7E
|
||||||
|
KeyDown Key = 0x7D
|
||||||
|
|
||||||
|
KeyF1 Key = 0x7A
|
||||||
|
KeyF2 Key = 0x78
|
||||||
|
KeyF3 Key = 0x63
|
||||||
|
KeyF4 Key = 0x76
|
||||||
|
KeyF5 Key = 0x60
|
||||||
|
KeyF6 Key = 0x61
|
||||||
|
KeyF7 Key = 0x62
|
||||||
|
KeyF8 Key = 0x64
|
||||||
|
KeyF9 Key = 0x65
|
||||||
|
KeyF10 Key = 0x6D
|
||||||
|
KeyF11 Key = 0x67
|
||||||
|
KeyF12 Key = 0x6F
|
||||||
|
KeyF13 Key = 0x69
|
||||||
|
KeyF14 Key = 0x6B
|
||||||
|
KeyF15 Key = 0x71
|
||||||
|
KeyF16 Key = 0x6A
|
||||||
|
KeyF17 Key = 0x40
|
||||||
|
KeyF18 Key = 0x4F
|
||||||
|
KeyF19 Key = 0x50
|
||||||
|
KeyF20 Key = 0x5A
|
||||||
|
)
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build darwin
|
//go:build darwin
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
// Copyright 2022 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build darwin
|
//go:build darwin
|
||||||
|
|
||||||
package mainthread
|
package darwin
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo CFLAGS: -x objective-c
|
#cgo CFLAGS: -x objective-c
|
||||||
@@ -36,10 +30,15 @@ func Call(f func()) {
|
|||||||
f()
|
f()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
mainFuncs <- f
|
mainFuncs <- func() {
|
||||||
|
f()
|
||||||
|
close(done)
|
||||||
|
}
|
||||||
C.wakeupMainThread()
|
C.wakeupMainThread()
|
||||||
}()
|
}()
|
||||||
|
<-done
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the functionality of running arbitrary subsequent functions be called on the main system thread.
|
// Init initializes the functionality of running arbitrary subsequent functions be called on the main system thread.
|
||||||
23
internal/common/hotkey/darwin/mainthread.m
Normal file
23
internal/common/hotkey/darwin/mainthread.m
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
extern void dispatchMainFuncs();
|
||||||
|
|
||||||
|
void wakeupMainThread(void) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
dispatchMainFuncs();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following three lines of code must run on the main thread.
|
||||||
|
// For GUI applications (Wails, Cocoa), the framework handles this automatically.
|
||||||
|
// For CLI applications, see README for manual event loop setup.
|
||||||
|
//
|
||||||
|
// Inspired from: https://github.com/cehoffman/dotfiles/blob/4be8e893517e970d40746a9bdc67fe5832dd1c33/os/mac/iTerm2HotKey.m
|
||||||
|
void os_main(void) {
|
||||||
|
[NSApplication sharedApplication];
|
||||||
|
[NSApp disableRelaunchOnLogin];
|
||||||
|
[NSApp run];
|
||||||
|
}
|
||||||
@@ -1,71 +1,106 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
// Package hotkey provides a high-performance, thread-safe facility to register
|
||||||
// All rights reserved. Use of this source code is governed
|
// system-level global hotkey shortcuts. Applications can be notified when users
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
// trigger hotkeys. A hotkey consists of a combination of modifier keys (Ctrl, Alt,
|
||||||
|
// Shift, etc.) and a single key.
|
||||||
//
|
//
|
||||||
// Written by Changkun Ou <changkun.de>
|
// # Basic Usage
|
||||||
|
|
||||||
// Package hotkey provides the basic facility to register a system-level
|
|
||||||
// global hotkey shortcut so that an application can be notified if a user
|
|
||||||
// triggers the desired hotkey. A hotkey must be a combination of modifiers
|
|
||||||
// and a single key.
|
|
||||||
//
|
//
|
||||||
// Note platform specific details:
|
// hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
|
||||||
|
// if err := hk.Register(); err != nil {
|
||||||
|
// log.Fatal(err)
|
||||||
|
// }
|
||||||
|
// defer hk.Close()
|
||||||
//
|
//
|
||||||
// - On macOS, due to the OS restriction (other platforms does not have
|
// for {
|
||||||
// this restriction), hotkey events must be handled on the "main thread".
|
// select {
|
||||||
// Therefore, in order to use this package properly, one must start an
|
// case <-hk.Keydown():
|
||||||
// OS main event loop on the main thread, For self-contained applications,
|
// fmt.Println("Hotkey pressed!")
|
||||||
// using [mainthread] package.
|
// case <-hk.Keyup():
|
||||||
// is possible. It is uncessary or applications based on other GUI frameworks,
|
// fmt.Println("Hotkey released!")
|
||||||
// such as fyne, ebiten, or Gio. See the "[examples]" for more examples.
|
// }
|
||||||
//
|
|
||||||
// - On Linux (X11), when AutoRepeat is enabled in the X server, the
|
|
||||||
// Keyup is triggered automatically and continuously as Keydown continues.
|
|
||||||
//
|
|
||||||
// - On Linux (X11), some keys may be mapped to multiple Mod keys. To
|
|
||||||
// correctly register the key combination, one must use the correct
|
|
||||||
// underlying keycode combination. For example, a regular Ctrl+Alt+S
|
|
||||||
// might be registered as: Ctrl+Mod2+Mod4+S.
|
|
||||||
//
|
|
||||||
// - If this package did not include a desired key, one can always provide
|
|
||||||
// the keycode to the API. For example, if a key code is 0x15, then the
|
|
||||||
// corresponding key is `hotkey.Key(0x15)`.
|
|
||||||
//
|
|
||||||
// THe following is a minimum example:
|
|
||||||
//
|
|
||||||
// package main
|
|
||||||
//
|
|
||||||
// import (
|
|
||||||
// "log"
|
|
||||||
//
|
|
||||||
// "golang.design/x/hotkey"
|
|
||||||
// "golang.design/x/hotkey/mainthread"
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
// func main() { mainthread.Init(fn) } // Not necessary when use in Fyne, Ebiten or Gio.
|
|
||||||
// func fn() {
|
|
||||||
// hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
|
|
||||||
// err := hk.Register()
|
|
||||||
// if err != nil {
|
|
||||||
// log.Fatalf("hotkey: failed to register hotkey: %v", err)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// log.Printf("hotkey: %v is registered\n", hk)
|
|
||||||
// <-hk.Keydown()
|
|
||||||
// log.Printf("hotkey: %v is down\n", hk)
|
|
||||||
// <-hk.Keyup()
|
|
||||||
// log.Printf("hotkey: %v is up\n", hk)
|
|
||||||
// hk.Unregister()
|
|
||||||
// log.Printf("hotkey: %v is unregistered\n", hk)
|
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// [mainthread]: https://pkg.go.dev/golang.design/x/hotkey/mainthread
|
// # Error Handling
|
||||||
// [examples]: https://github.com/golang-design/hotkey/tree/main/examples
|
//
|
||||||
|
// The package provides standardized error types for robust error handling:
|
||||||
|
//
|
||||||
|
// if err := hk.Register(); err != nil {
|
||||||
|
// switch {
|
||||||
|
// case errors.Is(err, hotkey.ErrHotkeyConflict):
|
||||||
|
// // Key combination already grabbed by another application
|
||||||
|
// case errors.Is(err, hotkey.ErrPlatformUnavailable):
|
||||||
|
// // Platform support unavailable (e.g., Linux without X11)
|
||||||
|
// case errors.Is(err, hotkey.ErrAlreadyRegistered):
|
||||||
|
// // Hotkey already registered
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// # Platform-Specific Notes
|
||||||
|
//
|
||||||
|
// Linux (X11):
|
||||||
|
// - Requires libx11-dev: `sudo apt install -y libx11-dev`
|
||||||
|
// - For headless environments, use Xvfb virtual display
|
||||||
|
// - AutoRepeat may cause repeated Keydown events - implement debouncing if needed
|
||||||
|
// - Display connection is kept open during registration for optimal performance
|
||||||
|
// - Conflict detection: XSetErrorHandler catches BadAccess and returns ErrHotkeyConflict
|
||||||
|
//
|
||||||
|
// macOS:
|
||||||
|
// - For GUI applications (like Wails): works out of the box
|
||||||
|
// - For pure CLI applications: use darwin.Init(yourMainFunc) to start event loop
|
||||||
|
// - Advanced: use darwin.Call(func) to execute code on main thread
|
||||||
|
// - May require Accessibility permissions in System Preferences
|
||||||
|
// - Uses Carbon API with GCD (dispatch_get_main_queue)
|
||||||
|
//
|
||||||
|
// Windows:
|
||||||
|
// - No additional dependencies required
|
||||||
|
// - Keyup events simulated via GetAsyncKeyState polling (10-30ms delay)
|
||||||
|
// - Some system hotkeys (Win+L, Ctrl+Alt+Del) are reserved
|
||||||
|
//
|
||||||
|
// # Resource Management
|
||||||
|
//
|
||||||
|
// Always use Close() to release resources:
|
||||||
|
//
|
||||||
|
// hk := hotkey.New(mods, key)
|
||||||
|
// defer hk.Close() // Safe to call multiple times
|
||||||
|
//
|
||||||
|
// if err := hk.Register(); err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// // ... use hotkey ...
|
||||||
|
//
|
||||||
|
// After Unregister() or Close(), you must re-obtain channel references:
|
||||||
|
//
|
||||||
|
// hk.Unregister()
|
||||||
|
// // ... modify hotkey ...
|
||||||
|
// hk.Register()
|
||||||
|
// keydownChan := hk.Keydown() // Get new channel reference
|
||||||
|
//
|
||||||
|
// # Performance
|
||||||
|
//
|
||||||
|
// - Memory: ~1KB per hotkey
|
||||||
|
// - Goroutines: 3 per hotkey (event loop + 2 channel converters)
|
||||||
|
// - Latency: Keydown < 10ms, Keyup < 30ms (Windows polling overhead)
|
||||||
|
// - Thread-safe: All public APIs use mutex protection
|
||||||
|
//
|
||||||
|
// For complete documentation and examples, see README.md in this package.
|
||||||
package hotkey
|
package hotkey
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Standard errors
|
||||||
|
var (
|
||||||
|
ErrAlreadyRegistered = errors.New("hotkey: already registered")
|
||||||
|
ErrNotRegistered = errors.New("hotkey: not registered")
|
||||||
|
ErrClosed = errors.New("hotkey: hotkey has been closed")
|
||||||
|
ErrFailedToRegister = errors.New("hotkey: failed to register")
|
||||||
|
ErrFailedToUnregister = errors.New("hotkey: failed to unregister")
|
||||||
|
ErrHotkeyConflict = errors.New("hotkey: hotkey conflict with other applications")
|
||||||
|
ErrPlatformUnavailable = errors.New("hotkey: platform support unavailable")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Event represents a hotkey event
|
// Event represents a hotkey event
|
||||||
@@ -82,36 +117,78 @@ type Hotkey struct {
|
|||||||
keydownOut <-chan Event
|
keydownOut <-chan Event
|
||||||
keyupIn chan<- Event
|
keyupIn chan<- Event
|
||||||
keyupOut <-chan Event
|
keyupOut <-chan Event
|
||||||
|
|
||||||
|
// 用于停止 newEventChan goroutines
|
||||||
|
eventChansWg sync.WaitGroup
|
||||||
|
|
||||||
|
// 状态管理
|
||||||
|
mu sync.RWMutex
|
||||||
|
registered bool
|
||||||
|
closed bool
|
||||||
|
|
||||||
|
// 用于防止 Finalizer 和 Unregister 并发
|
||||||
|
finalizerMu sync.Mutex
|
||||||
|
finalized bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new hotkey for the given modifiers and keycode.
|
// New creates a new hotkey for the given modifiers and keycode.
|
||||||
func New(mods []Modifier, key Key) *Hotkey {
|
func New(mods []Modifier, key Key) *Hotkey {
|
||||||
keydownIn, keydownOut := newEventChan()
|
|
||||||
keyupIn, keyupOut := newEventChan()
|
|
||||||
hk := &Hotkey{
|
hk := &Hotkey{
|
||||||
mods: mods,
|
mods: mods,
|
||||||
key: key,
|
key: key,
|
||||||
keydownIn: keydownIn,
|
|
||||||
keydownOut: keydownOut,
|
|
||||||
keyupIn: keyupIn,
|
|
||||||
keyupOut: keyupOut,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hk.eventChansWg.Add(2)
|
||||||
|
keydownIn, keydownOut := newEventChan(&hk.eventChansWg)
|
||||||
|
keyupIn, keyupOut := newEventChan(&hk.eventChansWg)
|
||||||
|
|
||||||
|
hk.keydownIn = keydownIn
|
||||||
|
hk.keydownOut = keydownOut
|
||||||
|
hk.keyupIn = keyupIn
|
||||||
|
hk.keyupOut = keyupOut
|
||||||
|
|
||||||
// Make sure the hotkey is unregistered when the created
|
// Make sure the hotkey is unregistered when the created
|
||||||
// hotkey is garbage collected.
|
// hotkey is garbage collected.
|
||||||
|
// Note: This is a safety net only. Users should explicitly call Unregister().
|
||||||
runtime.SetFinalizer(hk, func(x interface{}) {
|
runtime.SetFinalizer(hk, func(x interface{}) {
|
||||||
hk := x.(*Hotkey)
|
hk := x.(*Hotkey)
|
||||||
hk.unregister()
|
hk.finalizerMu.Lock()
|
||||||
close(hk.keydownIn)
|
defer hk.finalizerMu.Unlock()
|
||||||
close(hk.keyupIn)
|
|
||||||
|
if hk.finalized {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hk.finalized = true
|
||||||
|
|
||||||
|
// Best effort cleanup - ignore errors
|
||||||
|
_ = hk.unregister()
|
||||||
})
|
})
|
||||||
return hk
|
return hk
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register registers a combination of hotkeys. If the hotkey has
|
// Register registers a combination of hotkeys. If the hotkey has
|
||||||
// registered. This function will invalidates the old registration
|
// already been registered, this function will return an error.
|
||||||
// and overwrites its callback.
|
// Use Unregister first if you want to re-register.
|
||||||
func (hk *Hotkey) Register() error { return hk.register() }
|
func (hk *Hotkey) Register() error {
|
||||||
|
hk.mu.Lock()
|
||||||
|
if hk.closed {
|
||||||
|
hk.mu.Unlock()
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
if hk.registered {
|
||||||
|
hk.mu.Unlock()
|
||||||
|
return ErrAlreadyRegistered
|
||||||
|
}
|
||||||
|
hk.mu.Unlock()
|
||||||
|
|
||||||
|
err := hk.register()
|
||||||
|
if err == nil {
|
||||||
|
hk.mu.Lock()
|
||||||
|
hk.registered = true
|
||||||
|
hk.mu.Unlock()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Keydown returns a channel that receives a signal when the hotkey is triggered.
|
// Keydown returns a channel that receives a signal when the hotkey is triggered.
|
||||||
func (hk *Hotkey) Keydown() <-chan Event { return hk.keydownOut }
|
func (hk *Hotkey) Keydown() <-chan Event { return hk.keydownOut }
|
||||||
@@ -119,21 +196,96 @@ func (hk *Hotkey) Keydown() <-chan Event { return hk.keydownOut }
|
|||||||
// Keyup returns a channel that receives a signal when the hotkey is released.
|
// Keyup returns a channel that receives a signal when the hotkey is released.
|
||||||
func (hk *Hotkey) Keyup() <-chan Event { return hk.keyupOut }
|
func (hk *Hotkey) Keyup() <-chan Event { return hk.keyupOut }
|
||||||
|
|
||||||
// Unregister unregisters the hotkey.
|
// Unregister unregisters the hotkey. After unregister, the hotkey can be
|
||||||
|
// registered again with Register(). If you don't plan to reuse the hotkey,
|
||||||
|
// use Close() instead for proper cleanup.
|
||||||
func (hk *Hotkey) Unregister() error {
|
func (hk *Hotkey) Unregister() error {
|
||||||
|
hk.mu.Lock()
|
||||||
|
if hk.closed {
|
||||||
|
hk.mu.Unlock()
|
||||||
|
return ErrClosed
|
||||||
|
}
|
||||||
|
if !hk.registered {
|
||||||
|
hk.mu.Unlock()
|
||||||
|
return ErrNotRegistered
|
||||||
|
}
|
||||||
|
hk.mu.Unlock()
|
||||||
|
|
||||||
err := hk.unregister()
|
err := hk.unregister()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset a new event channel.
|
hk.mu.Lock()
|
||||||
|
hk.registered = false
|
||||||
|
hk.mu.Unlock()
|
||||||
|
|
||||||
|
// Close old event channels and wait for goroutines to exit
|
||||||
close(hk.keydownIn)
|
close(hk.keydownIn)
|
||||||
close(hk.keyupIn)
|
close(hk.keyupIn)
|
||||||
hk.keydownIn, hk.keydownOut = newEventChan()
|
hk.eventChansWg.Wait()
|
||||||
hk.keyupIn, hk.keyupOut = newEventChan()
|
|
||||||
|
// Reset new event channels for potential re-registration
|
||||||
|
hk.eventChansWg.Add(2)
|
||||||
|
hk.keydownIn, hk.keydownOut = newEventChan(&hk.eventChansWg)
|
||||||
|
hk.keyupIn, hk.keyupOut = newEventChan(&hk.eventChansWg)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Close unregisters the hotkey and releases all resources.
|
||||||
|
// After Close(), the hotkey cannot be used again. This is the recommended
|
||||||
|
// way to cleanup resources when you're done with the hotkey.
|
||||||
|
// Close is safe to call multiple times.
|
||||||
|
func (hk *Hotkey) Close() error {
|
||||||
|
hk.finalizerMu.Lock()
|
||||||
|
if hk.finalized {
|
||||||
|
hk.finalizerMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hk.finalized = true
|
||||||
|
hk.finalizerMu.Unlock()
|
||||||
|
|
||||||
|
hk.mu.Lock()
|
||||||
|
if hk.closed {
|
||||||
|
hk.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
hk.closed = true
|
||||||
|
wasRegistered := hk.registered
|
||||||
|
hk.registered = false
|
||||||
|
hk.mu.Unlock()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if wasRegistered {
|
||||||
|
err = hk.unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close event channels and wait for goroutines
|
||||||
|
close(hk.keydownIn)
|
||||||
|
close(hk.keyupIn)
|
||||||
|
hk.eventChansWg.Wait()
|
||||||
|
|
||||||
|
// Remove finalizer since we're cleaning up properly
|
||||||
|
runtime.SetFinalizer(hk, nil)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRegistered returns true if the hotkey is currently registered.
|
||||||
|
func (hk *Hotkey) IsRegistered() bool {
|
||||||
|
hk.mu.RLock()
|
||||||
|
defer hk.mu.RUnlock()
|
||||||
|
return hk.registered && !hk.closed
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsClosed returns true if the hotkey has been closed.
|
||||||
|
func (hk *Hotkey) IsClosed() bool {
|
||||||
|
hk.mu.RLock()
|
||||||
|
defer hk.mu.RUnlock()
|
||||||
|
return hk.closed
|
||||||
|
}
|
||||||
|
|
||||||
// String returns a string representation of the hotkey.
|
// String returns a string representation of the hotkey.
|
||||||
func (hk *Hotkey) String() string {
|
func (hk *Hotkey) String() string {
|
||||||
s := fmt.Sprintf("%v", hk.key)
|
s := fmt.Sprintf("%v", hk.key)
|
||||||
@@ -145,10 +297,11 @@ func (hk *Hotkey) String() string {
|
|||||||
|
|
||||||
// newEventChan returns a sender and a receiver of a buffered channel
|
// newEventChan returns a sender and a receiver of a buffered channel
|
||||||
// with infinite capacity.
|
// with infinite capacity.
|
||||||
func newEventChan() (chan<- Event, <-chan Event) {
|
func newEventChan(wg *sync.WaitGroup) (chan<- Event, <-chan Event) {
|
||||||
in, out := make(chan Event), make(chan Event)
|
in, out := make(chan Event), make(chan Event)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
var q []Event
|
var q []Event
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
|||||||
@@ -1,176 +1,152 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build darwin
|
//go:build darwin
|
||||||
|
|
||||||
package hotkey
|
package hotkey
|
||||||
|
|
||||||
/*
|
import "voidraft/internal/common/hotkey/darwin"
|
||||||
#cgo CFLAGS: -x objective-c
|
|
||||||
#cgo LDFLAGS: -framework Cocoa -framework Carbon
|
|
||||||
#include <stdint.h>
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
#import <Carbon/Carbon.h>
|
|
||||||
|
|
||||||
extern void keydownCallback(uintptr_t handle);
|
|
||||||
extern void keyupCallback(uintptr_t handle);
|
|
||||||
int registerHotKey(int mod, int key, uintptr_t handle, EventHotKeyRef* ref);
|
|
||||||
int unregisterHotKey(EventHotKeyRef ref);
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"runtime/cgo"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Hotkey is a combination of modifiers and key to trigger an event
|
|
||||||
type platformHotkey struct {
|
type platformHotkey struct {
|
||||||
mu sync.Mutex
|
ph darwin.PlatformHotkey
|
||||||
registered bool
|
keydownIn chan interface{}
|
||||||
hkref C.EventHotKeyRef
|
keyupIn chan interface{}
|
||||||
}
|
stopChans chan struct{} // 用于停止通道转换 goroutines
|
||||||
|
|
||||||
func (hk *Hotkey) register() error {
|
|
||||||
hk.mu.Lock()
|
|
||||||
defer hk.mu.Unlock()
|
|
||||||
if hk.registered {
|
|
||||||
return errors.New("hotkey already registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: we use handle number as hotkey id in the C side.
|
|
||||||
// A cgo handle could ran out of space, but since in hotkey purpose
|
|
||||||
// we won't have that much number of hotkeys. So this should be fine.
|
|
||||||
|
|
||||||
h := cgo.NewHandle(hk)
|
|
||||||
var mod Modifier
|
|
||||||
for _, m := range hk.mods {
|
|
||||||
mod += m
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := C.registerHotKey(C.int(mod), C.int(hk.key), C.uintptr_t(h), &hk.hkref)
|
|
||||||
if ret == C.int(-1) {
|
|
||||||
return errors.New("failed to register the hotkey")
|
|
||||||
}
|
|
||||||
|
|
||||||
hk.registered = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hk *Hotkey) unregister() error {
|
|
||||||
hk.mu.Lock()
|
|
||||||
defer hk.mu.Unlock()
|
|
||||||
if !hk.registered {
|
|
||||||
return errors.New("hotkey is not registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
ret := C.unregisterHotKey(hk.hkref)
|
|
||||||
if ret == C.int(-1) {
|
|
||||||
return errors.New("failed to unregister the current hotkey")
|
|
||||||
}
|
|
||||||
hk.registered = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//export keydownCallback
|
|
||||||
func keydownCallback(h uintptr) {
|
|
||||||
hk := cgo.Handle(h).Value().(*Hotkey)
|
|
||||||
hk.keydownIn <- Event{}
|
|
||||||
}
|
|
||||||
|
|
||||||
//export keyupCallback
|
|
||||||
func keyupCallback(h uintptr) {
|
|
||||||
hk := cgo.Handle(h).Value().(*Hotkey)
|
|
||||||
hk.keyupIn <- Event{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modifier represents a modifier.
|
// Modifier represents a modifier.
|
||||||
// See: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
|
type Modifier = darwin.Modifier
|
||||||
type Modifier uint32
|
|
||||||
|
|
||||||
// All kinds of Modifiers
|
// All kinds of Modifiers
|
||||||
const (
|
const (
|
||||||
ModCtrl Modifier = 0x1000
|
ModCtrl = darwin.ModCtrl
|
||||||
ModShift Modifier = 0x200
|
ModShift = darwin.ModShift
|
||||||
ModOption Modifier = 0x800
|
ModOption = darwin.ModOption
|
||||||
ModCmd Modifier = 0x100
|
ModCmd = darwin.ModCmd
|
||||||
)
|
)
|
||||||
|
|
||||||
// Key represents a key.
|
// Key represents a key.
|
||||||
// See: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h
|
type Key = darwin.Key
|
||||||
type Key uint8
|
|
||||||
|
|
||||||
// All kinds of keys
|
// All kinds of keys
|
||||||
const (
|
const (
|
||||||
KeySpace Key = 49
|
KeySpace = darwin.KeySpace
|
||||||
Key1 Key = 18
|
Key1 = darwin.Key1
|
||||||
Key2 Key = 19
|
Key2 = darwin.Key2
|
||||||
Key3 Key = 20
|
Key3 = darwin.Key3
|
||||||
Key4 Key = 21
|
Key4 = darwin.Key4
|
||||||
Key5 Key = 23
|
Key5 = darwin.Key5
|
||||||
Key6 Key = 22
|
Key6 = darwin.Key6
|
||||||
Key7 Key = 26
|
Key7 = darwin.Key7
|
||||||
Key8 Key = 28
|
Key8 = darwin.Key8
|
||||||
Key9 Key = 25
|
Key9 = darwin.Key9
|
||||||
Key0 Key = 29
|
Key0 = darwin.Key0
|
||||||
KeyA Key = 0
|
KeyA = darwin.KeyA
|
||||||
KeyB Key = 11
|
KeyB = darwin.KeyB
|
||||||
KeyC Key = 8
|
KeyC = darwin.KeyC
|
||||||
KeyD Key = 2
|
KeyD = darwin.KeyD
|
||||||
KeyE Key = 14
|
KeyE = darwin.KeyE
|
||||||
KeyF Key = 3
|
KeyF = darwin.KeyF
|
||||||
KeyG Key = 5
|
KeyG = darwin.KeyG
|
||||||
KeyH Key = 4
|
KeyH = darwin.KeyH
|
||||||
KeyI Key = 34
|
KeyI = darwin.KeyI
|
||||||
KeyJ Key = 38
|
KeyJ = darwin.KeyJ
|
||||||
KeyK Key = 40
|
KeyK = darwin.KeyK
|
||||||
KeyL Key = 37
|
KeyL = darwin.KeyL
|
||||||
KeyM Key = 46
|
KeyM = darwin.KeyM
|
||||||
KeyN Key = 45
|
KeyN = darwin.KeyN
|
||||||
KeyO Key = 31
|
KeyO = darwin.KeyO
|
||||||
KeyP Key = 35
|
KeyP = darwin.KeyP
|
||||||
KeyQ Key = 12
|
KeyQ = darwin.KeyQ
|
||||||
KeyR Key = 15
|
KeyR = darwin.KeyR
|
||||||
KeyS Key = 1
|
KeyS = darwin.KeyS
|
||||||
KeyT Key = 17
|
KeyT = darwin.KeyT
|
||||||
KeyU Key = 32
|
KeyU = darwin.KeyU
|
||||||
KeyV Key = 9
|
KeyV = darwin.KeyV
|
||||||
KeyW Key = 13
|
KeyW = darwin.KeyW
|
||||||
KeyX Key = 7
|
KeyX = darwin.KeyX
|
||||||
KeyY Key = 16
|
KeyY = darwin.KeyY
|
||||||
KeyZ Key = 6
|
KeyZ = darwin.KeyZ
|
||||||
|
|
||||||
KeyReturn Key = 0x24
|
KeyReturn = darwin.KeyReturn
|
||||||
KeyEscape Key = 0x35
|
KeyEscape = darwin.KeyEscape
|
||||||
KeyDelete Key = 0x33
|
KeyDelete = darwin.KeyDelete
|
||||||
KeyTab Key = 0x30
|
KeyTab = darwin.KeyTab
|
||||||
|
|
||||||
KeyLeft Key = 0x7B
|
KeyLeft = darwin.KeyLeft
|
||||||
KeyRight Key = 0x7C
|
KeyRight = darwin.KeyRight
|
||||||
KeyUp Key = 0x7E
|
KeyUp = darwin.KeyUp
|
||||||
KeyDown Key = 0x7D
|
KeyDown = darwin.KeyDown
|
||||||
|
|
||||||
KeyF1 Key = 0x7A
|
KeyF1 = darwin.KeyF1
|
||||||
KeyF2 Key = 0x78
|
KeyF2 = darwin.KeyF2
|
||||||
KeyF3 Key = 0x63
|
KeyF3 = darwin.KeyF3
|
||||||
KeyF4 Key = 0x76
|
KeyF4 = darwin.KeyF4
|
||||||
KeyF5 Key = 0x60
|
KeyF5 = darwin.KeyF5
|
||||||
KeyF6 Key = 0x61
|
KeyF6 = darwin.KeyF6
|
||||||
KeyF7 Key = 0x62
|
KeyF7 = darwin.KeyF7
|
||||||
KeyF8 Key = 0x64
|
KeyF8 = darwin.KeyF8
|
||||||
KeyF9 Key = 0x65
|
KeyF9 = darwin.KeyF9
|
||||||
KeyF10 Key = 0x6D
|
KeyF10 = darwin.KeyF10
|
||||||
KeyF11 Key = 0x67
|
KeyF11 = darwin.KeyF11
|
||||||
KeyF12 Key = 0x6F
|
KeyF12 = darwin.KeyF12
|
||||||
KeyF13 Key = 0x69
|
KeyF13 = darwin.KeyF13
|
||||||
KeyF14 Key = 0x6B
|
KeyF14 = darwin.KeyF14
|
||||||
KeyF15 Key = 0x71
|
KeyF15 = darwin.KeyF15
|
||||||
KeyF16 Key = 0x6A
|
KeyF16 = darwin.KeyF16
|
||||||
KeyF17 Key = 0x40
|
KeyF17 = darwin.KeyF17
|
||||||
KeyF18 Key = 0x4F
|
KeyF18 = darwin.KeyF18
|
||||||
KeyF19 Key = 0x50
|
KeyF19 = darwin.KeyF19
|
||||||
KeyF20 Key = 0x5A
|
KeyF20 = darwin.KeyF20
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (hk *Hotkey) register() error {
|
||||||
|
// Convert channels
|
||||||
|
hk.platformHotkey.keydownIn = make(chan interface{}, 1)
|
||||||
|
hk.platformHotkey.keyupIn = make(chan interface{}, 1)
|
||||||
|
hk.platformHotkey.stopChans = make(chan struct{})
|
||||||
|
|
||||||
|
// Start goroutines to convert interface{} events to Event{}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-hk.platformHotkey.stopChans:
|
||||||
|
return
|
||||||
|
case <-hk.platformHotkey.keydownIn:
|
||||||
|
hk.keydownIn <- Event{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-hk.platformHotkey.stopChans:
|
||||||
|
return
|
||||||
|
case <-hk.platformHotkey.keyupIn:
|
||||||
|
hk.keyupIn <- Event{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return hk.platformHotkey.ph.Register(hk.mods, hk.key, hk.platformHotkey.keydownIn, hk.platformHotkey.keyupIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hk *Hotkey) unregister() error {
|
||||||
|
// Stop channel conversion goroutines first
|
||||||
|
if hk.platformHotkey.stopChans != nil {
|
||||||
|
select {
|
||||||
|
case <-hk.platformHotkey.stopChans:
|
||||||
|
// Already closed, do nothing
|
||||||
|
default:
|
||||||
|
close(hk.platformHotkey.stopChans)
|
||||||
|
}
|
||||||
|
hk.platformHotkey.stopChans = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then unregister the hotkey
|
||||||
|
err := hk.platformHotkey.ph.Unregister()
|
||||||
|
|
||||||
|
// Close conversion channels (don't close, just set to nil)
|
||||||
|
// The goroutines will drain them when stopChans is closed
|
||||||
|
hk.platformHotkey.keydownIn = nil
|
||||||
|
hk.platformHotkey.keyupIn = nil
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build darwin && cgo
|
|
||||||
|
|
||||||
package hotkey_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
"voidraft/internal/common/hotkey"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestHotkey should always run success.
|
|
||||||
// This is a test to run and for manually testing the registration of multiple
|
|
||||||
// hotkeys. Registered hotkeys:
|
|
||||||
// Ctrl+Shift+S
|
|
||||||
// Ctrl+Option+S
|
|
||||||
func TestHotkey(t *testing.T) {
|
|
||||||
tt := time.Second * 5
|
|
||||||
done := make(chan struct{}, 2)
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tt)
|
|
||||||
go func() {
|
|
||||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
|
|
||||||
if err := hk.Register(); err != nil {
|
|
||||||
t.Errorf("failed to register hotkey: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
cancel()
|
|
||||||
done <- struct{}{}
|
|
||||||
return
|
|
||||||
case <-hk.Keydown():
|
|
||||||
fmt.Println("triggered ctrl+shift+s")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModOption}, hotkey.KeyS)
|
|
||||||
if err := hk.Register(); err != nil {
|
|
||||||
t.Errorf("failed to register hotkey: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
cancel()
|
|
||||||
done <- struct{}{}
|
|
||||||
return
|
|
||||||
case <-hk.Keydown():
|
|
||||||
fmt.Println("triggered ctrl+option+s")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
<-done
|
|
||||||
<-done
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build linux
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <X11/Xlib.h>
|
|
||||||
#include <X11/Xutil.h>
|
|
||||||
|
|
||||||
extern void hotkeyDown(uintptr_t hkhandle);
|
|
||||||
extern void hotkeyUp(uintptr_t hkhandle);
|
|
||||||
|
|
||||||
int displayTest() {
|
|
||||||
Display* d = NULL;
|
|
||||||
for (int i = 0; i < 42; i++) {
|
|
||||||
d = XOpenDisplay(0);
|
|
||||||
if (d == NULL) continue;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (d == NULL) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: handle bad access properly.
|
|
||||||
// int handleErrors( Display* dpy, XErrorEvent* pErr )
|
|
||||||
// {
|
|
||||||
// printf("X Error Handler called, values: %d/%lu/%d/%d/%d\n",
|
|
||||||
// pErr->type,
|
|
||||||
// pErr->serial,
|
|
||||||
// pErr->error_code,
|
|
||||||
// pErr->request_code,
|
|
||||||
// pErr->minor_code );
|
|
||||||
// if( pErr->request_code == 33 ){ // 33 (X_GrabKey)
|
|
||||||
// if( pErr->error_code == BadAccess ){
|
|
||||||
// printf("ERROR: key combination already grabbed by another client.\n");
|
|
||||||
// return 0;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// return 0;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// waitHotkey blocks until the hotkey is triggered.
|
|
||||||
// this function crashes the program if the hotkey already grabbed by others.
|
|
||||||
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key) {
|
|
||||||
Display* d = NULL;
|
|
||||||
for (int i = 0; i < 42; i++) {
|
|
||||||
d = XOpenDisplay(0);
|
|
||||||
if (d == NULL) continue;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (d == NULL) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
int keycode = XKeysymToKeycode(d, key);
|
|
||||||
XGrabKey(d, keycode, mod, DefaultRootWindow(d), False, GrabModeAsync, GrabModeAsync);
|
|
||||||
XSelectInput(d, DefaultRootWindow(d), KeyPressMask);
|
|
||||||
XEvent ev;
|
|
||||||
while(1) {
|
|
||||||
XNextEvent(d, &ev);
|
|
||||||
switch(ev.type) {
|
|
||||||
case KeyPress:
|
|
||||||
hotkeyDown(hkhandle);
|
|
||||||
continue;
|
|
||||||
case KeyRelease:
|
|
||||||
hotkeyUp(hkhandle);
|
|
||||||
XUngrabKey(d, keycode, mod, DefaultRootWindow(d));
|
|
||||||
XCloseDisplay(d);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,210 +1,156 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build linux
|
//go:build linux
|
||||||
|
|
||||||
package hotkey
|
package hotkey
|
||||||
|
|
||||||
/*
|
import "voidraft/internal/common/hotkey/linux"
|
||||||
#cgo LDFLAGS: -lX11
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
int displayTest();
|
|
||||||
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key);
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"runtime"
|
|
||||||
"runtime/cgo"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const errmsg = `Failed to initialize the X11 display, and the clipboard package
|
|
||||||
will not work properly. Install the following dependency may help:
|
|
||||||
|
|
||||||
apt install -y libx11-dev
|
|
||||||
If the clipboard package is in an environment without a frame buffer,
|
|
||||||
such as a cloud server, it may also be necessary to install xvfb:
|
|
||||||
apt install -y xvfb
|
|
||||||
and initialize a virtual frame buffer:
|
|
||||||
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
|
||||||
export DISPLAY=:99.0
|
|
||||||
Then this package should be ready to use.
|
|
||||||
`
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
if C.displayTest() != 0 {
|
|
||||||
panic(errmsg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type platformHotkey struct {
|
type platformHotkey struct {
|
||||||
mu sync.Mutex
|
ph linux.PlatformHotkey
|
||||||
registered bool
|
keydownIn chan interface{}
|
||||||
ctx context.Context
|
keyupIn chan interface{}
|
||||||
cancel context.CancelFunc
|
stopChans chan struct{} // 用于停止通道转换 goroutines
|
||||||
canceled chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nothing needs to do for register
|
|
||||||
func (hk *Hotkey) register() error {
|
|
||||||
hk.mu.Lock()
|
|
||||||
if hk.registered {
|
|
||||||
hk.mu.Unlock()
|
|
||||||
return errors.New("hotkey already registered.")
|
|
||||||
}
|
|
||||||
hk.registered = true
|
|
||||||
hk.ctx, hk.cancel = context.WithCancel(context.Background())
|
|
||||||
hk.canceled = make(chan struct{})
|
|
||||||
hk.mu.Unlock()
|
|
||||||
|
|
||||||
go hk.handle()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nothing needs to do for unregister
|
|
||||||
func (hk *Hotkey) unregister() error {
|
|
||||||
hk.mu.Lock()
|
|
||||||
defer hk.mu.Unlock()
|
|
||||||
if !hk.registered {
|
|
||||||
return errors.New("hotkey is not registered.")
|
|
||||||
}
|
|
||||||
hk.cancel()
|
|
||||||
hk.registered = false
|
|
||||||
<-hk.canceled
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle registers an application global hotkey to the system,
|
|
||||||
// and returns a channel that will signal if the hotkey is triggered.
|
|
||||||
func (hk *Hotkey) handle() {
|
|
||||||
runtime.LockOSThread()
|
|
||||||
defer runtime.UnlockOSThread()
|
|
||||||
// KNOWN ISSUE: if a hotkey is grabbed by others, C side will crash the program
|
|
||||||
|
|
||||||
var mod Modifier
|
|
||||||
for _, m := range hk.mods {
|
|
||||||
mod = mod | m
|
|
||||||
}
|
|
||||||
h := cgo.NewHandle(hk)
|
|
||||||
defer h.Delete()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-hk.ctx.Done():
|
|
||||||
close(hk.canceled)
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
_ = C.waitHotkey(C.uintptr_t(h), C.uint(mod), C.int(hk.key))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//export hotkeyDown
|
|
||||||
func hotkeyDown(h uintptr) {
|
|
||||||
hk := cgo.Handle(h).Value().(*Hotkey)
|
|
||||||
hk.keydownIn <- Event{}
|
|
||||||
}
|
|
||||||
|
|
||||||
//export hotkeyUp
|
|
||||||
func hotkeyUp(h uintptr) {
|
|
||||||
hk := cgo.Handle(h).Value().(*Hotkey)
|
|
||||||
hk.keyupIn <- Event{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modifier represents a modifier.
|
// Modifier represents a modifier.
|
||||||
type Modifier uint32
|
type Modifier = linux.Modifier
|
||||||
|
|
||||||
// All kinds of Modifiers
|
// All kinds of Modifiers
|
||||||
// See /usr/include/X11/X.h
|
|
||||||
const (
|
const (
|
||||||
ModCtrl Modifier = (1 << 2)
|
ModCtrl = linux.ModCtrl
|
||||||
ModShift Modifier = (1 << 0)
|
ModShift = linux.ModShift
|
||||||
Mod1 Modifier = (1 << 3)
|
ModAlt = linux.ModAlt // Alias for Mod1
|
||||||
Mod2 Modifier = (1 << 4)
|
Mod1 = linux.Mod1
|
||||||
Mod3 Modifier = (1 << 5)
|
Mod2 = linux.Mod2
|
||||||
Mod4 Modifier = (1 << 6)
|
Mod3 = linux.Mod3
|
||||||
Mod5 Modifier = (1 << 7)
|
Mod4 = linux.Mod4
|
||||||
|
Mod5 = linux.Mod5
|
||||||
)
|
)
|
||||||
|
|
||||||
// Key represents a key.
|
// Key represents a key.
|
||||||
// See /usr/include/X11/keysymdef.h
|
type Key = linux.Key
|
||||||
type Key uint16
|
|
||||||
|
|
||||||
// All kinds of keys
|
// All kinds of keys
|
||||||
const (
|
const (
|
||||||
KeySpace Key = 0x0020
|
KeySpace = linux.KeySpace
|
||||||
Key1 Key = 0x0030
|
Key1 = linux.Key1
|
||||||
Key2 Key = 0x0031
|
Key2 = linux.Key2
|
||||||
Key3 Key = 0x0032
|
Key3 = linux.Key3
|
||||||
Key4 Key = 0x0033
|
Key4 = linux.Key4
|
||||||
Key5 Key = 0x0034
|
Key5 = linux.Key5
|
||||||
Key6 Key = 0x0035
|
Key6 = linux.Key6
|
||||||
Key7 Key = 0x0036
|
Key7 = linux.Key7
|
||||||
Key8 Key = 0x0037
|
Key8 = linux.Key8
|
||||||
Key9 Key = 0x0038
|
Key9 = linux.Key9
|
||||||
Key0 Key = 0x0039
|
Key0 = linux.Key0
|
||||||
KeyA Key = 0x0061
|
KeyA = linux.KeyA
|
||||||
KeyB Key = 0x0062
|
KeyB = linux.KeyB
|
||||||
KeyC Key = 0x0063
|
KeyC = linux.KeyC
|
||||||
KeyD Key = 0x0064
|
KeyD = linux.KeyD
|
||||||
KeyE Key = 0x0065
|
KeyE = linux.KeyE
|
||||||
KeyF Key = 0x0066
|
KeyF = linux.KeyF
|
||||||
KeyG Key = 0x0067
|
KeyG = linux.KeyG
|
||||||
KeyH Key = 0x0068
|
KeyH = linux.KeyH
|
||||||
KeyI Key = 0x0069
|
KeyI = linux.KeyI
|
||||||
KeyJ Key = 0x006a
|
KeyJ = linux.KeyJ
|
||||||
KeyK Key = 0x006b
|
KeyK = linux.KeyK
|
||||||
KeyL Key = 0x006c
|
KeyL = linux.KeyL
|
||||||
KeyM Key = 0x006d
|
KeyM = linux.KeyM
|
||||||
KeyN Key = 0x006e
|
KeyN = linux.KeyN
|
||||||
KeyO Key = 0x006f
|
KeyO = linux.KeyO
|
||||||
KeyP Key = 0x0070
|
KeyP = linux.KeyP
|
||||||
KeyQ Key = 0x0071
|
KeyQ = linux.KeyQ
|
||||||
KeyR Key = 0x0072
|
KeyR = linux.KeyR
|
||||||
KeyS Key = 0x0073
|
KeyS = linux.KeyS
|
||||||
KeyT Key = 0x0074
|
KeyT = linux.KeyT
|
||||||
KeyU Key = 0x0075
|
KeyU = linux.KeyU
|
||||||
KeyV Key = 0x0076
|
KeyV = linux.KeyV
|
||||||
KeyW Key = 0x0077
|
KeyW = linux.KeyW
|
||||||
KeyX Key = 0x0078
|
KeyX = linux.KeyX
|
||||||
KeyY Key = 0x0079
|
KeyY = linux.KeyY
|
||||||
KeyZ Key = 0x007a
|
KeyZ = linux.KeyZ
|
||||||
|
|
||||||
KeyReturn Key = 0xff0d
|
KeyReturn = linux.KeyReturn
|
||||||
KeyEscape Key = 0xff1b
|
KeyEscape = linux.KeyEscape
|
||||||
KeyDelete Key = 0xffff
|
KeyDelete = linux.KeyDelete
|
||||||
KeyTab Key = 0xff1b
|
KeyTab = linux.KeyTab
|
||||||
|
|
||||||
KeyLeft Key = 0xff51
|
KeyLeft = linux.KeyLeft
|
||||||
KeyRight Key = 0xff53
|
KeyRight = linux.KeyRight
|
||||||
KeyUp Key = 0xff52
|
KeyUp = linux.KeyUp
|
||||||
KeyDown Key = 0xff54
|
KeyDown = linux.KeyDown
|
||||||
|
|
||||||
KeyF1 Key = 0xffbe
|
KeyF1 = linux.KeyF1
|
||||||
KeyF2 Key = 0xffbf
|
KeyF2 = linux.KeyF2
|
||||||
KeyF3 Key = 0xffc0
|
KeyF3 = linux.KeyF3
|
||||||
KeyF4 Key = 0xffc1
|
KeyF4 = linux.KeyF4
|
||||||
KeyF5 Key = 0xffc2
|
KeyF5 = linux.KeyF5
|
||||||
KeyF6 Key = 0xffc3
|
KeyF6 = linux.KeyF6
|
||||||
KeyF7 Key = 0xffc4
|
KeyF7 = linux.KeyF7
|
||||||
KeyF8 Key = 0xffc5
|
KeyF8 = linux.KeyF8
|
||||||
KeyF9 Key = 0xffc6
|
KeyF9 = linux.KeyF9
|
||||||
KeyF10 Key = 0xffc7
|
KeyF10 = linux.KeyF10
|
||||||
KeyF11 Key = 0xffc8
|
KeyF11 = linux.KeyF11
|
||||||
KeyF12 Key = 0xffc9
|
KeyF12 = linux.KeyF12
|
||||||
KeyF13 Key = 0xffca
|
KeyF13 = linux.KeyF13
|
||||||
KeyF14 Key = 0xffcb
|
KeyF14 = linux.KeyF14
|
||||||
KeyF15 Key = 0xffcc
|
KeyF15 = linux.KeyF15
|
||||||
KeyF16 Key = 0xffcd
|
KeyF16 = linux.KeyF16
|
||||||
KeyF17 Key = 0xffce
|
KeyF17 = linux.KeyF17
|
||||||
KeyF18 Key = 0xffcf
|
KeyF18 = linux.KeyF18
|
||||||
KeyF19 Key = 0xffd0
|
KeyF19 = linux.KeyF19
|
||||||
KeyF20 Key = 0xffd1
|
KeyF20 = linux.KeyF20
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (hk *Hotkey) register() error {
|
||||||
|
// Convert channels
|
||||||
|
hk.platformHotkey.keydownIn = make(chan interface{}, 1)
|
||||||
|
hk.platformHotkey.keyupIn = make(chan interface{}, 1)
|
||||||
|
hk.platformHotkey.stopChans = make(chan struct{})
|
||||||
|
|
||||||
|
// Start goroutines to convert interface{} events to Event{}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-hk.platformHotkey.stopChans:
|
||||||
|
return
|
||||||
|
case <-hk.platformHotkey.keydownIn:
|
||||||
|
hk.keydownIn <- Event{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-hk.platformHotkey.stopChans:
|
||||||
|
return
|
||||||
|
case <-hk.platformHotkey.keyupIn:
|
||||||
|
hk.keyupIn <- Event{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return hk.platformHotkey.ph.Register(hk.mods, hk.key, hk.platformHotkey.keydownIn, hk.platformHotkey.keyupIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hk *Hotkey) unregister() error {
|
||||||
|
// Stop channel conversion goroutines first
|
||||||
|
if hk.platformHotkey.stopChans != nil {
|
||||||
|
select {
|
||||||
|
case <-hk.platformHotkey.stopChans:
|
||||||
|
// Already closed, do nothing
|
||||||
|
default:
|
||||||
|
close(hk.platformHotkey.stopChans)
|
||||||
|
}
|
||||||
|
hk.platformHotkey.stopChans = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then unregister the hotkey
|
||||||
|
err := hk.platformHotkey.ph.Unregister()
|
||||||
|
|
||||||
|
// Close conversion channels (don't close, just set to nil)
|
||||||
|
// The goroutines will drain them when stopChans is closed
|
||||||
|
hk.platformHotkey.keydownIn = nil
|
||||||
|
hk.platformHotkey.keyupIn = nil
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build linux && cgo
|
|
||||||
|
|
||||||
package hotkey_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"voidraft/internal/common/hotkey"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestHotkey should always run success.
|
|
||||||
// This is a test to run and for manually testing, registered combination:
|
|
||||||
// Ctrl+Alt+A (Ctrl+Mod2+Mod4+A on Linux)
|
|
||||||
func TestHotkey(t *testing.T) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
hk := hotkey.New([]hotkey.Modifier{
|
|
||||||
hotkey.ModCtrl, hotkey.Mod2, hotkey.Mod4}, hotkey.KeyA)
|
|
||||||
if err := hk.Register(); err != nil {
|
|
||||||
t.Errorf("failed to register hotkey: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-hk.Keydown():
|
|
||||||
fmt.Println("triggered")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,3 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build !windows && !cgo
|
//go:build !windows && !cgo
|
||||||
|
|
||||||
package hotkey
|
package hotkey
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build (linux || darwin) && !cgo
|
|
||||||
|
|
||||||
package hotkey_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"voidraft/internal/common/hotkey"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestHotkey should always run success.
|
|
||||||
// This is a test to run and for manually testing, registered combination:
|
|
||||||
// Ctrl+Alt+A (Ctrl+Mod2+Mod4+A on Linux)
|
|
||||||
func TestHotkey(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.Fatalf("expect to fail when CGO_ENABLED=0")
|
|
||||||
}()
|
|
||||||
|
|
||||||
hk := hotkey.New([]hotkey.Modifier{}, hotkey.Key(0))
|
|
||||||
err := hk.Register()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
hk.Unregister()
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
package hotkey_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"voidraft/internal/common/hotkey/mainthread"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The test cannot be run twice since the mainthread loop may not be terminated:
|
|
||||||
// go test -v -count=1
|
|
||||||
func TestMain(m *testing.M) {
|
|
||||||
mainthread.Init(func() { os.Exit(m.Run()) })
|
|
||||||
}
|
|
||||||
@@ -1,224 +1,152 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build windows
|
//go:build windows
|
||||||
|
|
||||||
package hotkey
|
package hotkey
|
||||||
|
|
||||||
import (
|
import "voidraft/internal/common/hotkey/windows"
|
||||||
"errors"
|
|
||||||
"runtime"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
"voidraft/internal/common/hotkey/internal/win"
|
|
||||||
)
|
|
||||||
|
|
||||||
type platformHotkey struct {
|
type platformHotkey struct {
|
||||||
mu sync.Mutex
|
ph windows.PlatformHotkey
|
||||||
hotkeyId uint64
|
keydownIn chan interface{}
|
||||||
registered bool
|
keyupIn chan interface{}
|
||||||
funcs chan func()
|
stopChans chan struct{} // 用于停止通道转换 goroutines
|
||||||
canceled chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var hotkeyId uint64 // atomic
|
|
||||||
|
|
||||||
// register registers a system hotkey. It returns an error if
|
|
||||||
// the registration is failed. This could be that the hotkey is
|
|
||||||
// conflict with other hotkeys.
|
|
||||||
func (hk *Hotkey) register() error {
|
|
||||||
hk.mu.Lock()
|
|
||||||
if hk.registered {
|
|
||||||
hk.mu.Unlock()
|
|
||||||
return errors.New("hotkey already registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
mod := uint8(0)
|
|
||||||
for _, m := range hk.mods {
|
|
||||||
mod = mod | uint8(m)
|
|
||||||
}
|
|
||||||
|
|
||||||
hk.hotkeyId = atomic.AddUint64(&hotkeyId, 1)
|
|
||||||
hk.funcs = make(chan func())
|
|
||||||
hk.canceled = make(chan struct{})
|
|
||||||
go hk.handle()
|
|
||||||
|
|
||||||
var (
|
|
||||||
ok bool
|
|
||||||
err error
|
|
||||||
done = make(chan struct{})
|
|
||||||
)
|
|
||||||
hk.funcs <- func() {
|
|
||||||
ok, err = win.RegisterHotKey(0, uintptr(hk.hotkeyId), uintptr(mod), uintptr(hk.key))
|
|
||||||
done <- struct{}{}
|
|
||||||
}
|
|
||||||
<-done
|
|
||||||
if !ok {
|
|
||||||
close(hk.canceled)
|
|
||||||
hk.mu.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
hk.registered = true
|
|
||||||
hk.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// unregister deregisteres a system hotkey.
|
|
||||||
func (hk *Hotkey) unregister() error {
|
|
||||||
hk.mu.Lock()
|
|
||||||
defer hk.mu.Unlock()
|
|
||||||
if !hk.registered {
|
|
||||||
return errors.New("hotkey is not registered")
|
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
hk.funcs <- func() {
|
|
||||||
win.UnregisterHotKey(0, uintptr(hk.hotkeyId))
|
|
||||||
done <- struct{}{}
|
|
||||||
close(hk.canceled)
|
|
||||||
}
|
|
||||||
<-done
|
|
||||||
|
|
||||||
<-hk.canceled
|
|
||||||
hk.registered = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// wmHotkey represents hotkey message
|
|
||||||
wmHotkey uint32 = 0x0312
|
|
||||||
wmQuit uint32 = 0x0012
|
|
||||||
)
|
|
||||||
|
|
||||||
// handle handles the hotkey event loop.
|
|
||||||
func (hk *Hotkey) handle() {
|
|
||||||
// We could optimize this. So far each hotkey is served in an
|
|
||||||
// individual thread. If we have too many hotkeys, then a program
|
|
||||||
// have to create too many threads to serve them.
|
|
||||||
runtime.LockOSThread()
|
|
||||||
defer runtime.UnlockOSThread()
|
|
||||||
|
|
||||||
isKeyDown := false
|
|
||||||
tk := time.NewTicker(time.Second / 100)
|
|
||||||
for range tk.C {
|
|
||||||
msg := win.MSG{}
|
|
||||||
if !win.PeekMessage(&msg, 0, 0, 0) {
|
|
||||||
select {
|
|
||||||
case f := <-hk.funcs:
|
|
||||||
f()
|
|
||||||
case <-hk.canceled:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
// If the latest status is KeyDown, and AsyncKeyState is 0, consider key is up.
|
|
||||||
if win.GetAsyncKeyState(int(hk.key)) == 0 && isKeyDown {
|
|
||||||
hk.keyupIn <- Event{}
|
|
||||||
isKeyDown = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !win.GetMessage(&msg, 0, 0, 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg.Message {
|
|
||||||
case wmHotkey:
|
|
||||||
hk.keydownIn <- Event{}
|
|
||||||
isKeyDown = true
|
|
||||||
case wmQuit:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modifier represents a modifier.
|
// Modifier represents a modifier.
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey
|
type Modifier = windows.Modifier
|
||||||
type Modifier uint8
|
|
||||||
|
|
||||||
// All kinds of Modifiers
|
// All kinds of Modifiers
|
||||||
const (
|
const (
|
||||||
ModAlt Modifier = 0x1
|
ModAlt = windows.ModAlt
|
||||||
ModCtrl Modifier = 0x2
|
ModCtrl = windows.ModCtrl
|
||||||
ModShift Modifier = 0x4
|
ModShift = windows.ModShift
|
||||||
ModWin Modifier = 0x8
|
ModWin = windows.ModWin
|
||||||
)
|
)
|
||||||
|
|
||||||
// Key represents a key.
|
// Key represents a key.
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
type Key = windows.Key
|
||||||
type Key uint16
|
|
||||||
|
|
||||||
// All kinds of Keys
|
// All kinds of Keys
|
||||||
const (
|
const (
|
||||||
KeySpace Key = 0x20
|
KeySpace = windows.KeySpace
|
||||||
Key0 Key = 0x30
|
Key0 = windows.Key0
|
||||||
Key1 Key = 0x31
|
Key1 = windows.Key1
|
||||||
Key2 Key = 0x32
|
Key2 = windows.Key2
|
||||||
Key3 Key = 0x33
|
Key3 = windows.Key3
|
||||||
Key4 Key = 0x34
|
Key4 = windows.Key4
|
||||||
Key5 Key = 0x35
|
Key5 = windows.Key5
|
||||||
Key6 Key = 0x36
|
Key6 = windows.Key6
|
||||||
Key7 Key = 0x37
|
Key7 = windows.Key7
|
||||||
Key8 Key = 0x38
|
Key8 = windows.Key8
|
||||||
Key9 Key = 0x39
|
Key9 = windows.Key9
|
||||||
KeyA Key = 0x41
|
KeyA = windows.KeyA
|
||||||
KeyB Key = 0x42
|
KeyB = windows.KeyB
|
||||||
KeyC Key = 0x43
|
KeyC = windows.KeyC
|
||||||
KeyD Key = 0x44
|
KeyD = windows.KeyD
|
||||||
KeyE Key = 0x45
|
KeyE = windows.KeyE
|
||||||
KeyF Key = 0x46
|
KeyF = windows.KeyF
|
||||||
KeyG Key = 0x47
|
KeyG = windows.KeyG
|
||||||
KeyH Key = 0x48
|
KeyH = windows.KeyH
|
||||||
KeyI Key = 0x49
|
KeyI = windows.KeyI
|
||||||
KeyJ Key = 0x4A
|
KeyJ = windows.KeyJ
|
||||||
KeyK Key = 0x4B
|
KeyK = windows.KeyK
|
||||||
KeyL Key = 0x4C
|
KeyL = windows.KeyL
|
||||||
KeyM Key = 0x4D
|
KeyM = windows.KeyM
|
||||||
KeyN Key = 0x4E
|
KeyN = windows.KeyN
|
||||||
KeyO Key = 0x4F
|
KeyO = windows.KeyO
|
||||||
KeyP Key = 0x50
|
KeyP = windows.KeyP
|
||||||
KeyQ Key = 0x51
|
KeyQ = windows.KeyQ
|
||||||
KeyR Key = 0x52
|
KeyR = windows.KeyR
|
||||||
KeyS Key = 0x53
|
KeyS = windows.KeyS
|
||||||
KeyT Key = 0x54
|
KeyT = windows.KeyT
|
||||||
KeyU Key = 0x55
|
KeyU = windows.KeyU
|
||||||
KeyV Key = 0x56
|
KeyV = windows.KeyV
|
||||||
KeyW Key = 0x57
|
KeyW = windows.KeyW
|
||||||
KeyX Key = 0x58
|
KeyX = windows.KeyX
|
||||||
KeyY Key = 0x59
|
KeyY = windows.KeyY
|
||||||
KeyZ Key = 0x5A
|
KeyZ = windows.KeyZ
|
||||||
|
|
||||||
KeyReturn Key = 0x0D
|
KeyReturn = windows.KeyReturn
|
||||||
KeyEscape Key = 0x1B
|
KeyEscape = windows.KeyEscape
|
||||||
KeyDelete Key = 0x2E
|
KeyDelete = windows.KeyDelete
|
||||||
KeyTab Key = 0x09
|
KeyTab = windows.KeyTab
|
||||||
|
|
||||||
KeyLeft Key = 0x25
|
KeyLeft = windows.KeyLeft
|
||||||
KeyRight Key = 0x27
|
KeyRight = windows.KeyRight
|
||||||
KeyUp Key = 0x26
|
KeyUp = windows.KeyUp
|
||||||
KeyDown Key = 0x28
|
KeyDown = windows.KeyDown
|
||||||
|
|
||||||
KeyF1 Key = 0x70
|
KeyF1 = windows.KeyF1
|
||||||
KeyF2 Key = 0x71
|
KeyF2 = windows.KeyF2
|
||||||
KeyF3 Key = 0x72
|
KeyF3 = windows.KeyF3
|
||||||
KeyF4 Key = 0x73
|
KeyF4 = windows.KeyF4
|
||||||
KeyF5 Key = 0x74
|
KeyF5 = windows.KeyF5
|
||||||
KeyF6 Key = 0x75
|
KeyF6 = windows.KeyF6
|
||||||
KeyF7 Key = 0x76
|
KeyF7 = windows.KeyF7
|
||||||
KeyF8 Key = 0x77
|
KeyF8 = windows.KeyF8
|
||||||
KeyF9 Key = 0x78
|
KeyF9 = windows.KeyF9
|
||||||
KeyF10 Key = 0x79
|
KeyF10 = windows.KeyF10
|
||||||
KeyF11 Key = 0x7A
|
KeyF11 = windows.KeyF11
|
||||||
KeyF12 Key = 0x7B
|
KeyF12 = windows.KeyF12
|
||||||
KeyF13 Key = 0x7C
|
KeyF13 = windows.KeyF13
|
||||||
KeyF14 Key = 0x7D
|
KeyF14 = windows.KeyF14
|
||||||
KeyF15 Key = 0x7E
|
KeyF15 = windows.KeyF15
|
||||||
KeyF16 Key = 0x7F
|
KeyF16 = windows.KeyF16
|
||||||
KeyF17 Key = 0x80
|
KeyF17 = windows.KeyF17
|
||||||
KeyF18 Key = 0x81
|
KeyF18 = windows.KeyF18
|
||||||
KeyF19 Key = 0x82
|
KeyF19 = windows.KeyF19
|
||||||
KeyF20 Key = 0x83
|
KeyF20 = windows.KeyF20
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (hk *Hotkey) register() error {
|
||||||
|
// Convert channels
|
||||||
|
hk.platformHotkey.keydownIn = make(chan interface{}, 1)
|
||||||
|
hk.platformHotkey.keyupIn = make(chan interface{}, 1)
|
||||||
|
hk.platformHotkey.stopChans = make(chan struct{})
|
||||||
|
|
||||||
|
// Start goroutines to convert interface{} events to Event{}
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-hk.platformHotkey.stopChans:
|
||||||
|
return
|
||||||
|
case <-hk.platformHotkey.keydownIn:
|
||||||
|
hk.keydownIn <- Event{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-hk.platformHotkey.stopChans:
|
||||||
|
return
|
||||||
|
case <-hk.platformHotkey.keyupIn:
|
||||||
|
hk.keyupIn <- Event{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return hk.platformHotkey.ph.Register(hk.mods, hk.key, hk.platformHotkey.keydownIn, hk.platformHotkey.keyupIn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hk *Hotkey) unregister() error {
|
||||||
|
// Stop channel conversion goroutines first
|
||||||
|
if hk.platformHotkey.stopChans != nil {
|
||||||
|
select {
|
||||||
|
case <-hk.platformHotkey.stopChans:
|
||||||
|
// Already closed, do nothing
|
||||||
|
default:
|
||||||
|
close(hk.platformHotkey.stopChans)
|
||||||
|
}
|
||||||
|
hk.platformHotkey.stopChans = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then unregister the hotkey
|
||||||
|
err := hk.platformHotkey.ph.Unregister()
|
||||||
|
|
||||||
|
// Close conversion channels (don't close, just set to nil)
|
||||||
|
// The goroutines will drain them when stopChans is closed
|
||||||
|
hk.platformHotkey.keydownIn = nil
|
||||||
|
hk.platformHotkey.keyupIn = nil
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build windows
|
|
||||||
|
|
||||||
package hotkey_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
"voidraft/internal/common/hotkey"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestHotkey should always run success.
|
|
||||||
// This is a test to run and for manually testing, registered combination:
|
|
||||||
// Ctrl+Shift+S
|
|
||||||
func TestHotkey(t *testing.T) {
|
|
||||||
tt := time.Second * 5
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), tt)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
hk := hotkey.New([]hotkey.Modifier{hotkey.ModCtrl, hotkey.ModShift}, hotkey.KeyS)
|
|
||||||
if err := hk.Register(); err != nil {
|
|
||||||
t.Errorf("failed to register hotkey: %v", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return
|
|
||||||
case <-hk.Keydown():
|
|
||||||
fmt.Println("triggered")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
// Copyright 2021 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build windows
|
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package win
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
user32 = syscall.NewLazyDLL("user32")
|
|
||||||
registerHotkey = user32.NewProc("RegisterHotKey")
|
|
||||||
unregisterHotkey = user32.NewProc("UnregisterHotKey")
|
|
||||||
getMessage = user32.NewProc("GetMessageW")
|
|
||||||
peekMessage = user32.NewProc("PeekMessageA")
|
|
||||||
sendMessage = user32.NewProc("SendMessageW")
|
|
||||||
getAsyncKeyState = user32.NewProc("GetAsyncKeyState")
|
|
||||||
quitMessage = user32.NewProc("PostQuitMessage")
|
|
||||||
)
|
|
||||||
|
|
||||||
// RegisterHotKey defines a system-wide hot key.
|
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey
|
|
||||||
func RegisterHotKey(hwnd, id uintptr, mod uintptr, k uintptr) (bool, error) {
|
|
||||||
ret, _, err := registerHotkey.Call(
|
|
||||||
hwnd, id, mod, k,
|
|
||||||
)
|
|
||||||
return ret != 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnregisterHotKey frees a hot key previously registered by the calling
|
|
||||||
// thread.
|
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-unregisterhotkey
|
|
||||||
func UnregisterHotKey(hwnd, id uintptr) (bool, error) {
|
|
||||||
ret, _, err := unregisterHotkey.Call(hwnd, id)
|
|
||||||
return ret != 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// MSG contains message information from a thread's message queue.
|
|
||||||
//
|
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msg
|
|
||||||
type MSG struct {
|
|
||||||
HWnd uintptr
|
|
||||||
Message uint32
|
|
||||||
WParam uintptr
|
|
||||||
LParam uintptr
|
|
||||||
Time uint32
|
|
||||||
Pt struct { //POINT
|
|
||||||
x, y int32
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendMessage sends the specified message to a window or windows.
|
|
||||||
// The SendMessage function calls the window procedure for the specified
|
|
||||||
// window and does not return until the window procedure has processed
|
|
||||||
// the message.
|
|
||||||
// The return value specifies the result of the message processing;
|
|
||||||
// it depends on the message sent.
|
|
||||||
//
|
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessage
|
|
||||||
func SendMessage(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
|
|
||||||
ret, _, _ := sendMessage.Call(
|
|
||||||
hwnd,
|
|
||||||
uintptr(msg),
|
|
||||||
wParam,
|
|
||||||
lParam,
|
|
||||||
)
|
|
||||||
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMessage retrieves a message from the calling thread's message
|
|
||||||
// queue. The function dispatches incoming sent messages until a posted
|
|
||||||
// message is available for retrieval.
|
|
||||||
//
|
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getmessage
|
|
||||||
func GetMessage(msg *MSG, hWnd uintptr, msgFilterMin, msgFilterMax uint32) bool {
|
|
||||||
ret, _, _ := getMessage.Call(
|
|
||||||
uintptr(unsafe.Pointer(msg)),
|
|
||||||
hWnd,
|
|
||||||
uintptr(msgFilterMin),
|
|
||||||
uintptr(msgFilterMax),
|
|
||||||
)
|
|
||||||
|
|
||||||
return ret != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// PeekMessage dispatches incoming sent messages, checks the thread message
|
|
||||||
// queue for a posted message, and retrieves the message (if any exist).
|
|
||||||
//
|
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-peekmessagea
|
|
||||||
func PeekMessage(msg *MSG, hWnd uintptr, msgFilterMin, msgFilterMax uint32) bool {
|
|
||||||
ret, _, _ := peekMessage.Call(
|
|
||||||
uintptr(unsafe.Pointer(msg)),
|
|
||||||
hWnd,
|
|
||||||
uintptr(msgFilterMin),
|
|
||||||
uintptr(msgFilterMax),
|
|
||||||
0, // PM_NOREMOVE
|
|
||||||
)
|
|
||||||
|
|
||||||
return ret != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// PostQuitMessage indicates to the system that a thread has made
|
|
||||||
// a request to terminate (quit). It is typically used in response
|
|
||||||
// to a WM_DESTROY message.
|
|
||||||
//
|
|
||||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postquitmessage
|
|
||||||
func PostQuitMessage(exitCode int) {
|
|
||||||
quitMessage.Call(uintptr(exitCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetAsyncKeyState(keycode int) uintptr {
|
|
||||||
ret, _, _ := getAsyncKeyState.Call(uintptr(keycode))
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
194
internal/common/hotkey/linux/hotkey.c
Normal file
194
internal/common/hotkey/linux/hotkey.c
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <X11/Xlib.h>
|
||||||
|
#include <X11/Xutil.h>
|
||||||
|
|
||||||
|
extern void hotkeyDown(uintptr_t hkhandle);
|
||||||
|
extern void hotkeyUp(uintptr_t hkhandle);
|
||||||
|
|
||||||
|
// Global error flag for BadAccess detection
|
||||||
|
static int grab_error_occurred = 0;
|
||||||
|
|
||||||
|
// Error handler for X11 errors
|
||||||
|
static int handleXError(Display* dpy, XErrorEvent* pErr) {
|
||||||
|
// Check if it's a GrabKey error (request_code == 33)
|
||||||
|
if (pErr->request_code == 33) {
|
||||||
|
// BadAccess means the key is already grabbed by another client
|
||||||
|
if (pErr->error_code == BadAccess) {
|
||||||
|
grab_error_occurred = 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For other errors, use default handler
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// displayTest checks if X11 display is available
|
||||||
|
int displayTest() {
|
||||||
|
Display* d = NULL;
|
||||||
|
for (int i = 0; i < 42; i++) {
|
||||||
|
d = XOpenDisplay(0);
|
||||||
|
if (d == NULL) continue;
|
||||||
|
XCloseDisplay(d);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayContext represents a persistent X11 display connection
|
||||||
|
typedef struct {
|
||||||
|
Display* display;
|
||||||
|
int keycode;
|
||||||
|
unsigned int mod;
|
||||||
|
Window root;
|
||||||
|
} DisplayContext;
|
||||||
|
|
||||||
|
// openDisplay opens and initializes a display context
|
||||||
|
// Returns NULL on failure
|
||||||
|
DisplayContext* openDisplay(unsigned int mod, int key) {
|
||||||
|
Display* d = NULL;
|
||||||
|
for (int i = 0; i < 42; i++) {
|
||||||
|
d = XOpenDisplay(0);
|
||||||
|
if (d != NULL) break;
|
||||||
|
}
|
||||||
|
if (d == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayContext* ctx = (DisplayContext*)malloc(sizeof(DisplayContext));
|
||||||
|
if (ctx == NULL) {
|
||||||
|
XCloseDisplay(d);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->display = d;
|
||||||
|
ctx->keycode = XKeysymToKeycode(d, key);
|
||||||
|
ctx->mod = mod;
|
||||||
|
ctx->root = DefaultRootWindow(d);
|
||||||
|
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// closeDisplay closes the display and frees the context
|
||||||
|
void closeDisplay(DisplayContext* ctx) {
|
||||||
|
if (ctx == NULL) return;
|
||||||
|
if (ctx->display != NULL) {
|
||||||
|
XCloseDisplay(ctx->display);
|
||||||
|
}
|
||||||
|
free(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// grabHotkey attempts to grab the hotkey with error handling
|
||||||
|
// Returns: 0 on success, -1 on BadAccess (conflict), -2 on other errors
|
||||||
|
int grabHotkey(DisplayContext* ctx) {
|
||||||
|
if (ctx == NULL || ctx->display == NULL) {
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set custom error handler
|
||||||
|
grab_error_occurred = 0;
|
||||||
|
XErrorHandler old_handler = XSetErrorHandler(handleXError);
|
||||||
|
|
||||||
|
// Attempt to grab the key
|
||||||
|
XGrabKey(ctx->display, ctx->keycode, ctx->mod, ctx->root,
|
||||||
|
False, GrabModeAsync, GrabModeAsync);
|
||||||
|
|
||||||
|
// Flush to ensure the grab request is processed
|
||||||
|
XSync(ctx->display, False);
|
||||||
|
|
||||||
|
// Restore old error handler
|
||||||
|
XSetErrorHandler(old_handler);
|
||||||
|
|
||||||
|
// Check if error occurred
|
||||||
|
if (grab_error_occurred) {
|
||||||
|
return -1; // BadAccess - hotkey conflict
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select input for both KeyPress and KeyRelease
|
||||||
|
XSelectInput(ctx->display, ctx->root, KeyPressMask | KeyReleaseMask);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ungrabHotkey releases the grabbed hotkey
|
||||||
|
void ungrabHotkey(DisplayContext* ctx) {
|
||||||
|
if (ctx == NULL || ctx->display == NULL) return;
|
||||||
|
XUngrabKey(ctx->display, ctx->keycode, ctx->mod, ctx->root);
|
||||||
|
XFlush(ctx->display);
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerHotkey attempts to register the hotkey and returns a display context
|
||||||
|
// Returns: context pointer on success, NULL on failure
|
||||||
|
// Sets *error_code: 0 = success, -1 = BadAccess (conflict), -2 = other error
|
||||||
|
DisplayContext* registerHotkey(unsigned int mod, int key, int* error_code) {
|
||||||
|
DisplayContext* ctx = openDisplay(mod, key);
|
||||||
|
if (ctx == NULL) {
|
||||||
|
*error_code = -2;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int grab_result = grabHotkey(ctx);
|
||||||
|
if (grab_result != 0) {
|
||||||
|
*error_code = grab_result;
|
||||||
|
closeDisplay(ctx);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*error_code = 0;
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unregisterHotkey ungrab the hotkey and closes the display
|
||||||
|
void unregisterHotkey(DisplayContext* ctx) {
|
||||||
|
if (ctx == NULL) return;
|
||||||
|
ungrabHotkey(ctx);
|
||||||
|
closeDisplay(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// waitHotkeyEvent waits for the next hotkey event on an already-registered hotkey
|
||||||
|
// Returns: 1 for KeyPress, 2 for KeyRelease, 0 for other events, -1 on error
|
||||||
|
int waitHotkeyEvent(DisplayContext* ctx, uintptr_t hkhandle) {
|
||||||
|
if (ctx == NULL || ctx->display == NULL) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
XEvent ev;
|
||||||
|
XNextEvent(ctx->display, &ev);
|
||||||
|
|
||||||
|
switch(ev.type) {
|
||||||
|
case KeyPress:
|
||||||
|
hotkeyDown(hkhandle);
|
||||||
|
return 1;
|
||||||
|
case KeyRelease:
|
||||||
|
hotkeyUp(hkhandle);
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy waitHotkey for compatibility - now uses the new API internally
|
||||||
|
// Returns: 0 on KeyRelease, -1 on BadAccess (conflict), -2 on other error
|
||||||
|
int waitHotkey(uintptr_t hkhandle, unsigned int mod, int key) {
|
||||||
|
int error_code = 0;
|
||||||
|
DisplayContext* ctx = registerHotkey(mod, key, &error_code);
|
||||||
|
if (ctx == NULL) {
|
||||||
|
return error_code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event loop
|
||||||
|
while(1) {
|
||||||
|
int result = waitHotkeyEvent(ctx, hkhandle);
|
||||||
|
if (result == 2) { // KeyRelease
|
||||||
|
unregisterHotkey(ctx);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (result < 0) { // Error
|
||||||
|
unregisterHotkey(ctx);
|
||||||
|
return -2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
275
internal/common/hotkey/linux/hotkey.go
Normal file
275
internal/common/hotkey/linux/hotkey.go
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package linux
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo LDFLAGS: -lX11
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
// DisplayContext is defined in hotkey.c
|
||||||
|
typedef struct DisplayContext DisplayContext;
|
||||||
|
|
||||||
|
int displayTest();
|
||||||
|
DisplayContext* registerHotkey(unsigned int mod, int key, int* error_code);
|
||||||
|
void unregisterHotkey(DisplayContext* ctx);
|
||||||
|
int waitHotkeyEvent(DisplayContext* ctx, uintptr_t hkhandle);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"runtime/cgo"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const errmsg = `Failed to initialize the X11 display. Install the following dependency may help:
|
||||||
|
|
||||||
|
apt install -y libx11-dev
|
||||||
|
If you are in an environment without a frame buffer (e.g., cloud server),
|
||||||
|
you may also need to install xvfb:
|
||||||
|
apt install -y xvfb
|
||||||
|
and initialize a virtual frame buffer:
|
||||||
|
Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
|
||||||
|
export DISPLAY=:99.0
|
||||||
|
`
|
||||||
|
|
||||||
|
var x11Available bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
x11Available = C.displayTest() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkX11() error {
|
||||||
|
if !x11Available {
|
||||||
|
return fmt.Errorf("%w: %s", ErrPlatformUnavailable, errmsg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard errors
|
||||||
|
var (
|
||||||
|
ErrAlreadyRegistered = errors.New("hotkey: already registered")
|
||||||
|
ErrNotRegistered = errors.New("hotkey: not registered")
|
||||||
|
ErrPlatformUnavailable = errors.New("hotkey: platform support unavailable")
|
||||||
|
ErrHotkeyConflict = errors.New("hotkey: hotkey conflict with other applications")
|
||||||
|
ErrFailedToRegister = errors.New("hotkey: failed to register")
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlatformHotkey struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
registered bool
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
canceled chan struct{}
|
||||||
|
cgoHandle cgo.Handle // 改名避免与方法名冲突
|
||||||
|
displayCtx *C.DisplayContext // Persistent X11 display connection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ph *PlatformHotkey) Register(mods []Modifier, key Key, keydownIn, keyupIn chan<- interface{}) error {
|
||||||
|
// Check X11 availability first
|
||||||
|
if err := checkX11(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ph.mu.Lock()
|
||||||
|
if ph.registered {
|
||||||
|
ph.mu.Unlock()
|
||||||
|
return ErrAlreadyRegistered
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate combined modifier
|
||||||
|
var mod Modifier
|
||||||
|
for _, m := range mods {
|
||||||
|
mod = mod | m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to register the hotkey and check for conflicts
|
||||||
|
var errorCode C.int
|
||||||
|
displayCtx := C.registerHotkey(C.uint(mod), C.int(key), &errorCode)
|
||||||
|
if displayCtx == nil {
|
||||||
|
ph.mu.Unlock()
|
||||||
|
switch errorCode {
|
||||||
|
case -1:
|
||||||
|
return fmt.Errorf("%w: key combination already grabbed by another application", ErrHotkeyConflict)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%w: failed to open X11 display or register hotkey", ErrFailedToRegister)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ph.registered = true
|
||||||
|
ph.displayCtx = displayCtx
|
||||||
|
ph.ctx, ph.cancel = context.WithCancel(context.Background())
|
||||||
|
ph.canceled = make(chan struct{})
|
||||||
|
|
||||||
|
// Store callbacks info
|
||||||
|
callbacks := &callbackData{
|
||||||
|
keydownIn: keydownIn,
|
||||||
|
keyupIn: keyupIn,
|
||||||
|
}
|
||||||
|
ph.cgoHandle = cgo.NewHandle(callbacks)
|
||||||
|
ph.mu.Unlock()
|
||||||
|
|
||||||
|
go ph.eventLoop()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ph *PlatformHotkey) Unregister() error {
|
||||||
|
ph.mu.Lock()
|
||||||
|
defer ph.mu.Unlock()
|
||||||
|
if !ph.registered {
|
||||||
|
return ErrNotRegistered
|
||||||
|
}
|
||||||
|
ph.cancel()
|
||||||
|
ph.registered = false
|
||||||
|
<-ph.canceled
|
||||||
|
|
||||||
|
// Clean up CGO handle and X11 display
|
||||||
|
ph.cgoHandle.Delete()
|
||||||
|
if ph.displayCtx != nil {
|
||||||
|
C.unregisterHotkey(ph.displayCtx)
|
||||||
|
ph.displayCtx = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// eventLoop continuously waits for hotkey events on the registered display.
|
||||||
|
// The display connection is kept open for the lifetime of the registration,
|
||||||
|
// avoiding repeated XOpenDisplay/XCloseDisplay calls.
|
||||||
|
func (ph *PlatformHotkey) eventLoop() {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ph.ctx.Done():
|
||||||
|
close(ph.canceled)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
// Wait for the next event on the persistent display connection
|
||||||
|
result := C.waitHotkeyEvent(ph.displayCtx, C.uintptr_t(ph.cgoHandle))
|
||||||
|
// result: 1 = KeyPress, 2 = KeyRelease, 0 = other event, -1 = error
|
||||||
|
if result < 0 {
|
||||||
|
// Error occurred, likely display connection issue
|
||||||
|
// The context will be canceled by Unregister, so we'll exit
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Events are handled by the C callback (hotkeyDown/hotkeyUp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type callbackData struct {
|
||||||
|
keydownIn chan<- interface{}
|
||||||
|
keyupIn chan<- interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export hotkeyDown
|
||||||
|
func hotkeyDown(h uintptr) {
|
||||||
|
cb := cgo.Handle(h).Value().(*callbackData)
|
||||||
|
cb.keydownIn <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export hotkeyUp
|
||||||
|
func hotkeyUp(h uintptr) {
|
||||||
|
cb := cgo.Handle(h).Value().(*callbackData)
|
||||||
|
cb.keyupIn <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifier represents a modifier.
|
||||||
|
type Modifier uint32
|
||||||
|
|
||||||
|
// All kinds of Modifiers
|
||||||
|
// See /usr/include/X11/X.h
|
||||||
|
const (
|
||||||
|
ModCtrl Modifier = (1 << 2)
|
||||||
|
ModShift Modifier = (1 << 0)
|
||||||
|
Mod1 Modifier = (1 << 3)
|
||||||
|
Mod2 Modifier = (1 << 4)
|
||||||
|
Mod3 Modifier = (1 << 5)
|
||||||
|
Mod4 Modifier = (1 << 6)
|
||||||
|
Mod5 Modifier = (1 << 7)
|
||||||
|
|
||||||
|
// ModAlt is typically mapped to Mod1 on most Linux systems
|
||||||
|
ModAlt = Mod1
|
||||||
|
)
|
||||||
|
|
||||||
|
// Key represents a key.
|
||||||
|
// See /usr/include/X11/keysymdef.h
|
||||||
|
type Key uint16
|
||||||
|
|
||||||
|
// All kinds of keys
|
||||||
|
const (
|
||||||
|
KeySpace Key = 0x0020
|
||||||
|
Key1 Key = 0x0030
|
||||||
|
Key2 Key = 0x0031
|
||||||
|
Key3 Key = 0x0032
|
||||||
|
Key4 Key = 0x0033
|
||||||
|
Key5 Key = 0x0034
|
||||||
|
Key6 Key = 0x0035
|
||||||
|
Key7 Key = 0x0036
|
||||||
|
Key8 Key = 0x0037
|
||||||
|
Key9 Key = 0x0038
|
||||||
|
Key0 Key = 0x0039
|
||||||
|
KeyA Key = 0x0061
|
||||||
|
KeyB Key = 0x0062
|
||||||
|
KeyC Key = 0x0063
|
||||||
|
KeyD Key = 0x0064
|
||||||
|
KeyE Key = 0x0065
|
||||||
|
KeyF Key = 0x0066
|
||||||
|
KeyG Key = 0x0067
|
||||||
|
KeyH Key = 0x0068
|
||||||
|
KeyI Key = 0x0069
|
||||||
|
KeyJ Key = 0x006a
|
||||||
|
KeyK Key = 0x006b
|
||||||
|
KeyL Key = 0x006c
|
||||||
|
KeyM Key = 0x006d
|
||||||
|
KeyN Key = 0x006e
|
||||||
|
KeyO Key = 0x006f
|
||||||
|
KeyP Key = 0x0070
|
||||||
|
KeyQ Key = 0x0071
|
||||||
|
KeyR Key = 0x0072
|
||||||
|
KeyS Key = 0x0073
|
||||||
|
KeyT Key = 0x0074
|
||||||
|
KeyU Key = 0x0075
|
||||||
|
KeyV Key = 0x0076
|
||||||
|
KeyW Key = 0x0077
|
||||||
|
KeyX Key = 0x0078
|
||||||
|
KeyY Key = 0x0079
|
||||||
|
KeyZ Key = 0x007a
|
||||||
|
|
||||||
|
KeyReturn Key = 0xff0d
|
||||||
|
KeyEscape Key = 0xff1b
|
||||||
|
KeyDelete Key = 0xffff
|
||||||
|
KeyTab Key = 0xff1b
|
||||||
|
|
||||||
|
KeyLeft Key = 0xff51
|
||||||
|
KeyRight Key = 0xff53
|
||||||
|
KeyUp Key = 0xff52
|
||||||
|
KeyDown Key = 0xff54
|
||||||
|
|
||||||
|
KeyF1 Key = 0xffbe
|
||||||
|
KeyF2 Key = 0xffbf
|
||||||
|
KeyF3 Key = 0xffc0
|
||||||
|
KeyF4 Key = 0xffc1
|
||||||
|
KeyF5 Key = 0xffc2
|
||||||
|
KeyF6 Key = 0xffc3
|
||||||
|
KeyF7 Key = 0xffc4
|
||||||
|
KeyF8 Key = 0xffc5
|
||||||
|
KeyF9 Key = 0xffc6
|
||||||
|
KeyF10 Key = 0xffc7
|
||||||
|
KeyF11 Key = 0xffc8
|
||||||
|
KeyF12 Key = 0xffc9
|
||||||
|
KeyF13 Key = 0xffca
|
||||||
|
KeyF14 Key = 0xffcb
|
||||||
|
KeyF15 Key = 0xffcc
|
||||||
|
KeyF16 Key = 0xffcd
|
||||||
|
KeyF17 Key = 0xffce
|
||||||
|
KeyF18 Key = 0xffcf
|
||||||
|
KeyF19 Key = 0xffd0
|
||||||
|
KeyF20 Key = 0xffd1
|
||||||
|
)
|
||||||
@@ -1,12 +1,6 @@
|
|||||||
// Copyright 2022 The golang.design Initiative Authors.
|
//go:build linux
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build windows || linux
|
package linux
|
||||||
|
|
||||||
package mainthread
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -14,10 +8,6 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
runtime.LockOSThread()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call calls f on the main thread and blocks until f finishes.
|
// Call calls f on the main thread and blocks until f finishes.
|
||||||
func Call(f func()) {
|
func Call(f func()) {
|
||||||
done := donePool.Get().(chan error)
|
done := donePool.Get().(chan error)
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// Copyright 2022 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
// Package mainthread provides facilities to schedule functions
|
|
||||||
// on the main thread. It includes platform-specific implementations
|
|
||||||
// for Windows, Linux, and macOS. The macOS implementation is specially
|
|
||||||
// designed to handle main thread events for the NSApplication.
|
|
||||||
package mainthread
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// Copyright 2022 The golang.design Initiative Authors.
|
|
||||||
// All rights reserved. Use of this source code is governed
|
|
||||||
// by a MIT license that can be found in the LICENSE file.
|
|
||||||
//
|
|
||||||
// Written by Changkun Ou <changkun.de>
|
|
||||||
|
|
||||||
//go:build darwin
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#import <Cocoa/Cocoa.h>
|
|
||||||
|
|
||||||
extern void dispatchMainFuncs();
|
|
||||||
|
|
||||||
void wakeupMainThread(void) {
|
|
||||||
dispatch_async(dispatch_get_main_queue(), ^{
|
|
||||||
dispatchMainFuncs();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following three lines of code must run on the main thread.
|
|
||||||
// It must handle it using golang.design/x/mainthread.
|
|
||||||
//
|
|
||||||
// inspired from here: https://github.com/cehoffman/dotfiles/blob/4be8e893517e970d40746a9bdc67fe5832dd1c33/os/mac/iTerm2HotKey.m
|
|
||||||
void os_main(void) {
|
|
||||||
[NSApplication sharedApplication];
|
|
||||||
[NSApp disableRelaunchOnLogin];
|
|
||||||
[NSApp run];
|
|
||||||
}
|
|
||||||
281
internal/common/hotkey/windows/hotkey.go
Normal file
281
internal/common/hotkey/windows/hotkey.go
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Standard errors
|
||||||
|
var (
|
||||||
|
ErrAlreadyRegistered = errors.New("hotkey: already registered")
|
||||||
|
ErrNotRegistered = errors.New("hotkey: not registered")
|
||||||
|
ErrFailedToRegister = errors.New("hotkey: failed to register")
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlatformHotkey struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
hotkeyId uint64
|
||||||
|
registered bool
|
||||||
|
funcs chan func()
|
||||||
|
canceled chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hotkeyId uint64 // atomic
|
||||||
|
|
||||||
|
// Register registers a system hotkey. It returns an error if
|
||||||
|
// the registration is failed. This could be that the hotkey is
|
||||||
|
// conflict with other hotkeys.
|
||||||
|
func (ph *PlatformHotkey) Register(mods []Modifier, key Key, keydownIn, keyupIn chan<- interface{}) error {
|
||||||
|
ph.mu.Lock()
|
||||||
|
if ph.registered {
|
||||||
|
ph.mu.Unlock()
|
||||||
|
return ErrAlreadyRegistered
|
||||||
|
}
|
||||||
|
|
||||||
|
mod := uint8(0)
|
||||||
|
for _, m := range mods {
|
||||||
|
mod = mod | uint8(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
ph.hotkeyId = atomic.AddUint64(&hotkeyId, 1)
|
||||||
|
ph.funcs = make(chan func())
|
||||||
|
ph.canceled = make(chan struct{})
|
||||||
|
go ph.handle(key, keydownIn, keyupIn)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ok bool
|
||||||
|
err error
|
||||||
|
done = make(chan struct{})
|
||||||
|
)
|
||||||
|
ph.funcs <- func() {
|
||||||
|
ok, err = RegisterHotKey(0, uintptr(ph.hotkeyId), uintptr(mod), uintptr(key))
|
||||||
|
done <- struct{}{}
|
||||||
|
}
|
||||||
|
<-done
|
||||||
|
if !ok {
|
||||||
|
close(ph.canceled)
|
||||||
|
ph.mu.Unlock()
|
||||||
|
return fmt.Errorf("%w: %v", ErrFailedToRegister, err)
|
||||||
|
}
|
||||||
|
ph.registered = true
|
||||||
|
ph.mu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unregister deregisteres a system hotkey.
|
||||||
|
func (ph *PlatformHotkey) Unregister() error {
|
||||||
|
ph.mu.Lock()
|
||||||
|
defer ph.mu.Unlock()
|
||||||
|
if !ph.registered {
|
||||||
|
return ErrNotRegistered
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
ph.funcs <- func() {
|
||||||
|
UnregisterHotKey(0, uintptr(ph.hotkeyId))
|
||||||
|
done <- struct{}{}
|
||||||
|
close(ph.canceled)
|
||||||
|
}
|
||||||
|
<-done
|
||||||
|
|
||||||
|
<-ph.canceled
|
||||||
|
ph.registered = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// wmHotkey represents hotkey message
|
||||||
|
wmHotkey uint32 = 0x0312
|
||||||
|
wmQuit uint32 = 0x0012
|
||||||
|
)
|
||||||
|
|
||||||
|
// handle handles the hotkey event loop.
|
||||||
|
// Simple, reliable approach with proper state management to prevent duplicate triggers
|
||||||
|
func (ph *PlatformHotkey) handle(key Key, keydownIn, keyupIn chan<- interface{}) {
|
||||||
|
runtime.LockOSThread()
|
||||||
|
defer runtime.UnlockOSThread()
|
||||||
|
|
||||||
|
isKeyDown := false
|
||||||
|
ticker := time.NewTicker(time.Millisecond * 10)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
// Process all pending messages
|
||||||
|
msg := MSG{}
|
||||||
|
for PeekMessage(&msg, 0, 0, 0) {
|
||||||
|
if msg.Message == wmHotkey {
|
||||||
|
// Only trigger if not already down (防止重复触发)
|
||||||
|
if !isKeyDown {
|
||||||
|
keydownIn <- struct{}{}
|
||||||
|
isKeyDown = true
|
||||||
|
}
|
||||||
|
} else if msg.Message == wmQuit {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for key release when key is down
|
||||||
|
if isKeyDown && GetAsyncKeyState(int(key)) == 0 {
|
||||||
|
keyupIn <- struct{}{}
|
||||||
|
isKeyDown = false
|
||||||
|
}
|
||||||
|
|
||||||
|
case f := <-ph.funcs:
|
||||||
|
f()
|
||||||
|
|
||||||
|
case <-ph.canceled:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modifier represents a modifier.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-registerhotkey
|
||||||
|
type Modifier uint8
|
||||||
|
|
||||||
|
// All kinds of Modifiers
|
||||||
|
const (
|
||||||
|
ModAlt Modifier = 0x1
|
||||||
|
ModCtrl Modifier = 0x2
|
||||||
|
ModShift Modifier = 0x4
|
||||||
|
ModWin Modifier = 0x8
|
||||||
|
)
|
||||||
|
|
||||||
|
// Key represents a key.
|
||||||
|
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
|
||||||
|
type Key uint16
|
||||||
|
|
||||||
|
// All kinds of Keys
|
||||||
|
const (
|
||||||
|
KeySpace Key = 0x20
|
||||||
|
Key0 Key = 0x30
|
||||||
|
Key1 Key = 0x31
|
||||||
|
Key2 Key = 0x32
|
||||||
|
Key3 Key = 0x33
|
||||||
|
Key4 Key = 0x34
|
||||||
|
Key5 Key = 0x35
|
||||||
|
Key6 Key = 0x36
|
||||||
|
Key7 Key = 0x37
|
||||||
|
Key8 Key = 0x38
|
||||||
|
Key9 Key = 0x39
|
||||||
|
KeyA Key = 0x41
|
||||||
|
KeyB Key = 0x42
|
||||||
|
KeyC Key = 0x43
|
||||||
|
KeyD Key = 0x44
|
||||||
|
KeyE Key = 0x45
|
||||||
|
KeyF Key = 0x46
|
||||||
|
KeyG Key = 0x47
|
||||||
|
KeyH Key = 0x48
|
||||||
|
KeyI Key = 0x49
|
||||||
|
KeyJ Key = 0x4A
|
||||||
|
KeyK Key = 0x4B
|
||||||
|
KeyL Key = 0x4C
|
||||||
|
KeyM Key = 0x4D
|
||||||
|
KeyN Key = 0x4E
|
||||||
|
KeyO Key = 0x4F
|
||||||
|
KeyP Key = 0x50
|
||||||
|
KeyQ Key = 0x51
|
||||||
|
KeyR Key = 0x52
|
||||||
|
KeyS Key = 0x53
|
||||||
|
KeyT Key = 0x54
|
||||||
|
KeyU Key = 0x55
|
||||||
|
KeyV Key = 0x56
|
||||||
|
KeyW Key = 0x57
|
||||||
|
KeyX Key = 0x58
|
||||||
|
KeyY Key = 0x59
|
||||||
|
KeyZ Key = 0x5A
|
||||||
|
|
||||||
|
KeyReturn Key = 0x0D
|
||||||
|
KeyEscape Key = 0x1B
|
||||||
|
KeyDelete Key = 0x2E
|
||||||
|
KeyTab Key = 0x09
|
||||||
|
|
||||||
|
KeyLeft Key = 0x25
|
||||||
|
KeyRight Key = 0x27
|
||||||
|
KeyUp Key = 0x26
|
||||||
|
KeyDown Key = 0x28
|
||||||
|
|
||||||
|
KeyF1 Key = 0x70
|
||||||
|
KeyF2 Key = 0x71
|
||||||
|
KeyF3 Key = 0x72
|
||||||
|
KeyF4 Key = 0x73
|
||||||
|
KeyF5 Key = 0x74
|
||||||
|
KeyF6 Key = 0x75
|
||||||
|
KeyF7 Key = 0x76
|
||||||
|
KeyF8 Key = 0x77
|
||||||
|
KeyF9 Key = 0x78
|
||||||
|
KeyF10 Key = 0x79
|
||||||
|
KeyF11 Key = 0x7A
|
||||||
|
KeyF12 Key = 0x7B
|
||||||
|
KeyF13 Key = 0x7C
|
||||||
|
KeyF14 Key = 0x7D
|
||||||
|
KeyF15 Key = 0x7E
|
||||||
|
KeyF16 Key = 0x7F
|
||||||
|
KeyF17 Key = 0x80
|
||||||
|
KeyF18 Key = 0x81
|
||||||
|
KeyF19 Key = 0x82
|
||||||
|
KeyF20 Key = 0x83
|
||||||
|
)
|
||||||
|
|
||||||
|
// Windows API wrappers
|
||||||
|
|
||||||
|
var (
|
||||||
|
user32 = syscall.NewLazyDLL("user32")
|
||||||
|
registerHotkey = user32.NewProc("RegisterHotKey")
|
||||||
|
unregisterHotkey = user32.NewProc("UnregisterHotKey")
|
||||||
|
peekMessage = user32.NewProc("PeekMessageW")
|
||||||
|
getAsyncKeyState = user32.NewProc("GetAsyncKeyState")
|
||||||
|
)
|
||||||
|
|
||||||
|
// RegisterHotKey defines a system-wide hot key.
|
||||||
|
func RegisterHotKey(hwnd, id uintptr, mod uintptr, k uintptr) (bool, error) {
|
||||||
|
ret, _, err := registerHotkey.Call(hwnd, id, mod, k)
|
||||||
|
return ret != 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnregisterHotKey frees a hot key previously registered by the calling thread.
|
||||||
|
func UnregisterHotKey(hwnd, id uintptr) (bool, error) {
|
||||||
|
ret, _, err := unregisterHotkey.Call(hwnd, id)
|
||||||
|
return ret != 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MSG contains message information from a thread's message queue.
|
||||||
|
type MSG struct {
|
||||||
|
HWnd uintptr
|
||||||
|
Message uint32
|
||||||
|
WParam uintptr
|
||||||
|
LParam uintptr
|
||||||
|
Time uint32
|
||||||
|
Pt struct {
|
||||||
|
x, y int32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PeekMessage checks for messages without blocking and removes them from queue
|
||||||
|
func PeekMessage(msg *MSG, hWnd uintptr, msgFilterMin, msgFilterMax uint32) bool {
|
||||||
|
const PM_REMOVE = 0x0001
|
||||||
|
ret, _, _ := peekMessage.Call(
|
||||||
|
uintptr(unsafe.Pointer(msg)),
|
||||||
|
hWnd,
|
||||||
|
uintptr(msgFilterMin),
|
||||||
|
uintptr(msgFilterMax),
|
||||||
|
PM_REMOVE,
|
||||||
|
)
|
||||||
|
return ret != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAsyncKeyState determines whether a key is up or down
|
||||||
|
func GetAsyncKeyState(keycode int) uintptr {
|
||||||
|
ret, _, _ := getAsyncKeyState.Call(uintptr(keycode))
|
||||||
|
return ret
|
||||||
|
}
|
||||||
77
internal/common/hotkey/windows/mainthread.go
Normal file
77
internal/common/hotkey/windows/mainthread.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Call calls f on the main thread and blocks until f finishes.
|
||||||
|
func Call(f func()) {
|
||||||
|
done := donePool.Get().(chan error)
|
||||||
|
defer donePool.Put(done)
|
||||||
|
|
||||||
|
data := funcData{fn: f, done: done}
|
||||||
|
funcQ <- data
|
||||||
|
if err := <-done; err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init initializes the functionality of running arbitrary subsequent functions be called on the main system thread.
|
||||||
|
//
|
||||||
|
// Init must be called in the main.main function.
|
||||||
|
func Init(main func()) {
|
||||||
|
done := donePool.Get().(chan error)
|
||||||
|
defer donePool.Put(done)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
done <- nil
|
||||||
|
}()
|
||||||
|
main()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case f := <-funcQ:
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
r := recover()
|
||||||
|
if f.done != nil {
|
||||||
|
if r != nil {
|
||||||
|
f.done <- fmt.Errorf("%v", r)
|
||||||
|
} else {
|
||||||
|
f.done <- nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if r != nil {
|
||||||
|
select {
|
||||||
|
case erroQ <- fmt.Errorf("%v", r):
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
f.fn()
|
||||||
|
}()
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
funcQ = make(chan funcData, runtime.GOMAXPROCS(0))
|
||||||
|
erroQ = make(chan error, 42)
|
||||||
|
donePool = sync.Pool{New: func() interface{} {
|
||||||
|
return make(chan error)
|
||||||
|
}}
|
||||||
|
)
|
||||||
|
|
||||||
|
type funcData struct {
|
||||||
|
fn func()
|
||||||
|
done chan error
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
"voidraft/internal/common/hotkey"
|
"voidraft/internal/common/hotkey"
|
||||||
"voidraft/internal/common/hotkey/mainthread"
|
|
||||||
"voidraft/internal/models"
|
"voidraft/internal/models"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
@@ -28,6 +28,10 @@ type HotkeyService struct {
|
|||||||
hk *hotkey.Hotkey
|
hk *hotkey.Hotkey
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
|
|
||||||
|
// 防抖相关
|
||||||
|
lastTriggerTime atomic.Int64 // 上次触发时间(Unix 纳秒)
|
||||||
|
debounceInterval time.Duration // 防抖间隔
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHotkeyService 创建热键服务实例
|
// NewHotkeyService 创建热键服务实例
|
||||||
@@ -37,10 +41,11 @@ func NewHotkeyService(configService *ConfigService, logger *log.LogService) *Hot
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &HotkeyService{
|
return &HotkeyService{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
configService: configService,
|
configService: configService,
|
||||||
windowHelper: NewWindowHelper(),
|
windowHelper: NewWindowHelper(),
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
|
debounceInterval: 100 * time.Millisecond,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,31 +77,28 @@ func (hs *HotkeyService) RegisterHotkey(combo *models.HotkeyCombo) error {
|
|||||||
return errors.New("invalid hotkey combination")
|
return errors.New("invalid hotkey combination")
|
||||||
}
|
}
|
||||||
|
|
||||||
hs.mu.Lock()
|
|
||||||
defer hs.mu.Unlock()
|
|
||||||
|
|
||||||
// 如果已注册,先取消
|
|
||||||
if hs.registered.Load() {
|
|
||||||
hs.unregisterInternal()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换为 hotkey 库的格式
|
// 转换为 hotkey 库的格式
|
||||||
key, mods, err := hs.convertHotkey(combo)
|
key, mods, err := hs.convertHotkey(combo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("convert hotkey: %w", err)
|
return fmt.Errorf("convert hotkey: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在主线程中创建热键
|
// 创建新热键(在锁外创建,避免持锁时间过长)
|
||||||
var regErr error
|
newHk := hotkey.New(mods, key)
|
||||||
mainthread.Init(func() {
|
if err := newHk.Register(); err != nil {
|
||||||
hs.hk = hotkey.New(mods, key)
|
return fmt.Errorf("register hotkey: %w", err)
|
||||||
regErr = hs.hk.Register()
|
|
||||||
})
|
|
||||||
|
|
||||||
if regErr != nil {
|
|
||||||
return fmt.Errorf("register hotkey: %w", regErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hs.mu.Lock()
|
||||||
|
defer hs.mu.Unlock()
|
||||||
|
|
||||||
|
// 如果已注册,先取消旧热键
|
||||||
|
if hs.registered.Load() {
|
||||||
|
hs.unregisterInternal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新热键
|
||||||
|
hs.hk = newHk
|
||||||
hs.registered.Store(true)
|
hs.registered.Store(true)
|
||||||
hs.currentHotkey = combo
|
hs.currentHotkey = combo
|
||||||
|
|
||||||
@@ -155,11 +157,25 @@ func (hs *HotkeyService) UpdateHotkey(enable bool, combo *models.HotkeyCombo) er
|
|||||||
func (hs *HotkeyService) listenHotkey() {
|
func (hs *HotkeyService) listenHotkey() {
|
||||||
defer hs.wg.Done()
|
defer hs.wg.Done()
|
||||||
|
|
||||||
|
// 缓存 channel 引用,避免每次循环都访问 hs.hk
|
||||||
|
hs.mu.RLock()
|
||||||
|
keydownChan := hs.hk.Keydown()
|
||||||
|
hs.mu.RUnlock()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-hs.stopChan:
|
case <-hs.stopChan:
|
||||||
return
|
return
|
||||||
case <-hs.hk.Keydown():
|
case <-keydownChan:
|
||||||
|
now := time.Now().UnixNano()
|
||||||
|
lastTrigger := hs.lastTriggerTime.Load()
|
||||||
|
|
||||||
|
// 如果距离上次触发时间小于防抖间隔,忽略此次触发
|
||||||
|
if lastTrigger > 0 && time.Duration(now-lastTrigger) < hs.debounceInterval {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 更新最后触发时间
|
||||||
|
hs.lastTriggerTime.Store(now)
|
||||||
hs.toggleWindow()
|
hs.toggleWindow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
387
internal/services/hotkey_service_test.go
Normal file
387
internal/services/hotkey_service_test.go
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"voidraft/internal/models"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestHotkeyServiceCreation 测试服务创建
|
||||||
|
func TestHotkeyServiceCreation(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
configService := &ConfigService{} // Mock
|
||||||
|
|
||||||
|
service := NewHotkeyService(configService, logger)
|
||||||
|
if service == nil {
|
||||||
|
t.Fatal("Failed to create hotkey service")
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.logger == nil {
|
||||||
|
t.Error("Logger should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.registered.Load() {
|
||||||
|
t.Error("Service should not have registered hotkey initially")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHotkeyValidation 测试热键验证
|
||||||
|
func TestHotkeyValidation(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHotkeyService(&ConfigService{}, logger)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
combo *models.HotkeyCombo
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Nil combo",
|
||||||
|
combo: nil,
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty key",
|
||||||
|
combo: &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Key: "",
|
||||||
|
},
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "No modifiers",
|
||||||
|
combo: &models.HotkeyCombo{
|
||||||
|
Key: "A",
|
||||||
|
},
|
||||||
|
valid: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid: Ctrl+A",
|
||||||
|
combo: &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Key: "A",
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid: Ctrl+Shift+F1",
|
||||||
|
combo: &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Shift: true,
|
||||||
|
Key: "F1",
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid: Alt+Space",
|
||||||
|
combo: &models.HotkeyCombo{
|
||||||
|
Alt: true,
|
||||||
|
Key: "Space",
|
||||||
|
},
|
||||||
|
valid: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := service.isValidHotkey(tt.combo)
|
||||||
|
if result != tt.valid {
|
||||||
|
t.Errorf("Expected valid=%v, got %v", tt.valid, result)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHotkeyConversion 测试热键转换
|
||||||
|
func TestHotkeyConversion(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHotkeyService(&ConfigService{}, logger)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
combo *models.HotkeyCombo
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid letter key",
|
||||||
|
combo: &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Key: "A",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid number key",
|
||||||
|
combo: &models.HotkeyCombo{
|
||||||
|
Shift: true,
|
||||||
|
Key: "1",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid function key",
|
||||||
|
combo: &models.HotkeyCombo{
|
||||||
|
Alt: true,
|
||||||
|
Key: "F5",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid key",
|
||||||
|
combo: &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Key: "InvalidKey",
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
key, mods, err := service.convertHotkey(tt.combo)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error, got nil")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Unexpected error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == 0 {
|
||||||
|
t.Error("Key should not be 0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mods) == 0 {
|
||||||
|
t.Error("Should have at least one modifier")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHotkeyRegisterUnregister 测试注册和注销
|
||||||
|
func TestHotkeyRegisterUnregister(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHotkeyService(&ConfigService{}, logger)
|
||||||
|
|
||||||
|
combo := &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Shift: true,
|
||||||
|
Key: "F10",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试注册
|
||||||
|
err := service.RegisterHotkey(combo)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Register failed (may be expected in test environment): %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !service.IsRegistered() {
|
||||||
|
t.Error("Service should be registered")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证当前热键
|
||||||
|
current := service.GetCurrentHotkey()
|
||||||
|
if current == nil {
|
||||||
|
t.Error("Current hotkey should not be nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if current.Key != combo.Key {
|
||||||
|
t.Errorf("Expected key %s, got %s", combo.Key, current.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试注销
|
||||||
|
err = service.UnregisterHotkey()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unregister failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.IsRegistered() {
|
||||||
|
t.Error("Service should not be registered after unregister")
|
||||||
|
}
|
||||||
|
|
||||||
|
current = service.GetCurrentHotkey()
|
||||||
|
if current != nil {
|
||||||
|
t.Error("Current hotkey should be nil after unregister")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHotkeyUpdate 测试更新热键
|
||||||
|
func TestHotkeyUpdate(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHotkeyService(&ConfigService{}, logger)
|
||||||
|
|
||||||
|
combo1 := &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Key: "F11",
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用热键
|
||||||
|
err := service.UpdateHotkey(true, combo1)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Update (enable) failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer service.UnregisterHotkey()
|
||||||
|
|
||||||
|
if !service.IsRegistered() {
|
||||||
|
t.Error("Should be registered after enable")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用热键
|
||||||
|
err = service.UpdateHotkey(false, combo1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Update (disable) failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.IsRegistered() {
|
||||||
|
t.Error("Should not be registered after disable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHotkeyDoubleRegister 测试重复注册
|
||||||
|
func TestHotkeyDoubleRegister(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHotkeyService(&ConfigService{}, logger)
|
||||||
|
|
||||||
|
combo := &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Alt: true,
|
||||||
|
Key: "F12",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := service.RegisterHotkey(combo)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("First registration failed")
|
||||||
|
}
|
||||||
|
defer service.UnregisterHotkey()
|
||||||
|
|
||||||
|
// 第二次注册应该先取消第一次注册,然后重新注册
|
||||||
|
combo2 := &models.HotkeyCombo{
|
||||||
|
Shift: true,
|
||||||
|
Key: "F12",
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.RegisterHotkey(combo2)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Second registration failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证当前热键是新的
|
||||||
|
current := service.GetCurrentHotkey()
|
||||||
|
if current != nil && current.Shift != combo2.Shift {
|
||||||
|
t.Error("Should have updated to new hotkey")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHotkeyConcurrentAccess 测试并发访问
|
||||||
|
func TestHotkeyConcurrentAccess(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHotkeyService(&ConfigService{}, logger)
|
||||||
|
|
||||||
|
combo := &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Key: "G",
|
||||||
|
}
|
||||||
|
|
||||||
|
const goroutines = 10
|
||||||
|
done := make(chan bool, goroutines)
|
||||||
|
|
||||||
|
// 并发读取
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
go func() {
|
||||||
|
for j := 0; j < 100; j++ {
|
||||||
|
_ = service.IsRegistered()
|
||||||
|
_ = service.GetCurrentHotkey()
|
||||||
|
time.Sleep(time.Millisecond)
|
||||||
|
}
|
||||||
|
done <- true
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主协程进行注册/注销操作
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
service.RegisterHotkey(combo)
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
service.UnregisterHotkey()
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待所有 goroutine 完成
|
||||||
|
for i := 0; i < goroutines; i++ {
|
||||||
|
<-done
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log("Concurrent access test completed without panics")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHotkeyServiceShutdown 测试服务关闭
|
||||||
|
func TestHotkeyServiceShutdown(t *testing.T) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHotkeyService(&ConfigService{}, logger)
|
||||||
|
|
||||||
|
combo := &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Shift: true,
|
||||||
|
Key: "H",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := service.RegisterHotkey(combo)
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("Registration failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试 ServiceShutdown
|
||||||
|
err = service.ServiceShutdown()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ServiceShutdown failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if service.IsRegistered() {
|
||||||
|
t.Error("Should not be registered after shutdown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkHotkeyRegistration 基准测试:热键注册
|
||||||
|
func BenchmarkHotkeyRegistration(b *testing.B) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHotkeyService(&ConfigService{}, logger)
|
||||||
|
|
||||||
|
combo := &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Key: "B",
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
service.RegisterHotkey(combo)
|
||||||
|
service.UnregisterHotkey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkHotkeyConversion 基准测试:热键转换
|
||||||
|
func BenchmarkHotkeyConversion(b *testing.B) {
|
||||||
|
logger := log.New()
|
||||||
|
service := NewHotkeyService(&ConfigService{}, logger)
|
||||||
|
|
||||||
|
combo := &models.HotkeyCombo{
|
||||||
|
Ctrl: true,
|
||||||
|
Shift: true,
|
||||||
|
Alt: true,
|
||||||
|
Key: "F5",
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
service.convertHotkey(combo)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user