Add system tray icon, file logging, and fix Windows hotkey conflict

- Adds fyne.io/systray with a programmatic monkey face icon and Quit menu item
- Logs to discord-lobby-mute.log next to the exe so startup errors are visible
  without a console window
- Windows now uses raw Win32 RegisterHotKey in a locked goroutine instead of
  golang.design/x/hotkey, avoiding a deadlock where systray and the hotkey
  library both try to own the main thread message loop
- Build with: GOOS=windows GOARCH=amd64 go build -ldflags "-H windowsgui" -o discord-lobby-mute.exe .

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 19:24:20 +03:00
parent 4f94c4f20b
commit 75c113909d
8 changed files with 229 additions and 26 deletions
+76 -1
View File
@@ -2,7 +2,14 @@
package main
import "golang.design/x/hotkey"
import (
"fmt"
"runtime"
"syscall"
"unsafe"
"golang.design/x/hotkey"
)
var modMap = map[string]hotkey.Modifier{
"ctrl": hotkey.ModCtrl,
@@ -11,3 +18,71 @@ var modMap = map[string]hotkey.Modifier{
"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
}