//go:build windows package win32 import ( "fmt" "runtime" "sync" "syscall" "unsafe" "golang.org/x/sys/windows" ) // HotkeyManager owns a dedicated OS thread that runs a GetMessageW // pump. RegisterHotKey can only be called from the thread that will // receive the messages, so all register/unregister/dispatch operations // are serialized onto that goroutine via the cmd channel. type HotkeyManager struct { cmd chan hkCmd events chan HotkeyEvent mu sync.Mutex nextID int32 stopped bool } type hkCmd struct { kind hkCmdKind id int32 hotkey Hotkey reply chan error } type hkCmdKind int const ( hkCmdRegister hkCmdKind = iota hkCmdUnregister hkCmdStop ) // NewHotkeyManager starts the dedicated thread and returns a manager // ready to accept Register calls. Events() yields presses; the channel // is closed on Stop(). func NewHotkeyManager() *HotkeyManager { m := &HotkeyManager{ cmd: make(chan hkCmd), events: make(chan HotkeyEvent, 16), nextID: 1, } started := make(chan struct{}) go m.run(started) <-started return m } // Events returns the read-only event channel. func (m *HotkeyManager) Events() <-chan HotkeyEvent { return m.events } // Register adds a global hotkey and returns its assigned id. Re-registering // a combination that is already taken by another application returns an // error; the caller should surface it to the user. func (m *HotkeyManager) Register(h Hotkey) (int32, error) { m.mu.Lock() if m.stopped { m.mu.Unlock() return 0, fmt.Errorf("win32: hotkey manager stopped") } id := m.nextID m.nextID++ m.mu.Unlock() reply := make(chan error, 1) m.cmd <- hkCmd{kind: hkCmdRegister, id: id, hotkey: h, reply: reply} if err := <-reply; err != nil { return 0, err } return id, nil } // Unregister removes a previously registered hotkey. func (m *HotkeyManager) Unregister(id int32) error { reply := make(chan error, 1) m.cmd <- hkCmd{kind: hkCmdUnregister, id: id, reply: reply} return <-reply } // Stop tears down the message pump and closes Events(). Subsequent // Register calls fail. func (m *HotkeyManager) Stop() { m.mu.Lock() if m.stopped { m.mu.Unlock() return } m.stopped = true m.mu.Unlock() reply := make(chan error, 1) m.cmd <- hkCmd{kind: hkCmdStop, reply: reply} <-reply } // run is the dedicated-thread loop. It owns the message queue that // RegisterHotKey targets. func (m *HotkeyManager) run(started chan struct{}) { runtime.LockOSThread() defer runtime.UnlockOSThread() // Force the message queue to exist before anybody tries to post to // us. PeekMessage with PM_NOREMOVE is the canonical incantation. var msg msgStruct procPeekMessageW.Call( uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 0, // PM_NOREMOVE ) close(started) for { // Non-blocking message pump: drain hotkey messages first, then // service one cmd, then sleep briefly. A blocking GetMessage // would freeze the cmd intake. for { r, _, _ := procPeekMessageW.Call( uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 1, // PM_REMOVE ) if r == 0 { break } if msg.message == wmHotKey { select { case m.events <- HotkeyEvent{ID: int32(msg.wParam)}: default: // Listener slow — drop to avoid stalling the pump. } } } select { case c := <-m.cmd: switch c.kind { case hkCmdRegister: err := registerHotKey(0, c.id, c.hotkey.Mods, c.hotkey.VK) c.reply <- err case hkCmdUnregister: err := unregisterHotKey(0, c.id) c.reply <- err case hkCmdStop: close(m.events) c.reply <- nil return } default: // brief sleep so we don't busy-loop. 30ms is well under any // human-perceptible hotkey latency. windows.SleepEx(30, false) } } } // ----------------------------------------------------------------------------- // raw syscalls var ( procRegisterHotKey = user32.NewProc("RegisterHotKey") procUnregisterHotKey = user32.NewProc("UnregisterHotKey") procPeekMessageW = user32.NewProc("PeekMessageW") ) const wmHotKey uint32 = 0x0312 type msgStruct struct { hwnd uintptr message uint32 wParam uintptr lParam uintptr time uint32 pt struct{ x, y int32 } } func registerHotKey(hwnd uintptr, id int32, mods, vk uint32) error { r, _, e := procRegisterHotKey.Call( hwnd, uintptr(id), uintptr(mods), uintptr(vk), ) if r == 0 { if errno, ok := e.(syscall.Errno); ok && errno != 0 { return fmt.Errorf("win32: RegisterHotKey: %w", errno) } return fmt.Errorf("win32: RegisterHotKey: unknown failure") } return nil } func unregisterHotKey(hwnd uintptr, id int32) error { r, _, e := procUnregisterHotKey.Call(hwnd, uintptr(id)) if r == 0 { if errno, ok := e.(syscall.Errno); ok && errno != 0 { return fmt.Errorf("win32: UnregisterHotKey: %w", errno) } } return nil }