Files
voidraft/internal/common/hotkey/windows/hotkey.go
2025-11-06 22:42:44 +08:00

282 lines
5.9 KiB
Go

//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
}