//go:build windows package main import ( "fmt" "runtime" "syscall" "unsafe" "golang.design/x/hotkey" ) var modMap = map[string]hotkey.Modifier{ "ctrl": hotkey.ModCtrl, "shift": hotkey.ModShift, "alt": hotkey.ModAlt, "win": hotkey.ModWin, "super": hotkey.ModWin, } var ( user32 = syscall.NewLazyDLL("user32.dll") procRegisterHotKey = user32.NewProc("RegisterHotKey") procGetMessageW = user32.NewProc("GetMessageW") ) // winMsg mirrors the Win32 MSG struct. type winMsg struct { hwnd uintptr message uint32 wParam uintptr lParam uintptr time uint32 ptX int32 ptY int32 } func startHotkey(keybind string) (<-chan struct{}, error) { mods, key, err := parseKeybind(keybind) if err != nil { return nil, err } // hotkey.Modifier and hotkey.Key values on Windows are the native Win32 // MOD_* and VK_* constants, so they can be passed directly to RegisterHotKey. var modFlags uintptr for _, m := range mods { modFlags |= uintptr(m) } vk := uintptr(key) ch := make(chan struct{}, 1) ready := make(chan error, 1) go func() { runtime.LockOSThread() // Intentionally no defer UnlockOSThread — this goroutine owns its OS // thread for the lifetime of the app to keep the hotkey message queue alive. ret, _, e := procRegisterHotKey.Call(0, 1, modFlags, vk) if ret == 0 { ready <- fmt.Errorf("RegisterHotKey: %w", e) return } close(ready) const wmHotkey = 0x0312 var msg winMsg for { r, _, _ := procGetMessageW.Call( uintptr(unsafe.Pointer(&msg)), 0, wmHotkey, wmHotkey, ) if r == 0 || r == ^uintptr(0) { // WM_QUIT or error return } select { case ch <- struct{}{}: default: } } }() if err := <-ready; err != nil { return nil, err } return ch, nil }