//go:build windows package tray import ( "context" "fmt" "os" "path/filepath" "runtime" "sync" "sync/atomic" "syscall" "unsafe" "golang.org/x/sys/windows" ) const ( wmClose = 0x0010 wmDestroy = 0x0002 wmApp = 0x8000 trayCallbackMessage = wmApp + 1 hwndMessage = ^uintptr(2) nimAdd = 0x00000000 nimDelete = 0x00000002 nimSetVersion = 0x00000004 nifMessage = 0x00000001 nifIcon = 0x00000002 nifTip = 0x00000004 notifyIconVersion = 4 imageIcon = 1 lrLoadFromFile = 0x00000010 idiApplication = 32512 smCxSmallIcon = 49 smCySmallIcon = 50 ) var ( kernel32 = windows.NewLazySystemDLL("kernel32.dll") user32 = windows.NewLazySystemDLL("user32.dll") shell32 = windows.NewLazySystemDLL("shell32.dll") procGetModuleHandle = kernel32.NewProc("GetModuleHandleW") procRegisterClassEx = user32.NewProc("RegisterClassExW") procCreateWindowEx = user32.NewProc("CreateWindowExW") procDefWindowProc = user32.NewProc("DefWindowProcW") procDestroyWindow = user32.NewProc("DestroyWindow") procPostMessage = user32.NewProc("PostMessageW") procGetMessage = user32.NewProc("GetMessageW") procTranslateMsg = user32.NewProc("TranslateMessage") procDispatchMsg = user32.NewProc("DispatchMessageW") procPostQuitMessage = user32.NewProc("PostQuitMessage") procLoadIcon = user32.NewProc("LoadIconW") procLoadImage = user32.NewProc("LoadImageW") procDestroyIcon = user32.NewProc("DestroyIcon") procGetSystemMetric = user32.NewProc("GetSystemMetrics") procShellNotifyIcon = shell32.NewProc("Shell_NotifyIconW") procExtractIconEx = shell32.NewProc("ExtractIconExW") ) type notifyIconData struct { cbSize uint32 hwnd uintptr uid uint32 flags uint32 callbackMessage uint32 icon uintptr tip [128]uint16 state uint32 stateMask uint32 info [256]uint16 version uint32 infoTitle [64]uint16 infoFlags uint32 guidItem windows.GUID balloonIcon uintptr } type wndClassEx struct { cbSize uint32 style uint32 wndProc uintptr clsExtra int32 wndExtra int32 instance uintptr icon uintptr cursor uintptr background uintptr menuName *uint16 className *uint16 iconSmall uintptr } type point struct { x int32 y int32 } type msg struct { hwnd uintptr message uint32 wparam uintptr lparam uintptr time uint32 pt point } type windowsTray struct { controller *Controller wndProc uintptr hwnd atomic.Uintptr icon uintptr destroyIcon bool ready chan error done chan struct{} stopOnce sync.Once cleanupOnce sync.Once } func start(ctx context.Context, controller *Controller) (func(), error) { tray := &windowsTray{ controller: controller, ready: make(chan error, 1), done: make(chan struct{}), } go tray.run() if err := <-tray.ready; err != nil { return nil, err } stop := func() { tray.stop() } go func() { <-ctx.Done() stop() }() return stop, nil } func (t *windowsTray) run() { runtime.LockOSThread() defer runtime.UnlockOSThread() if err := t.init(); err != nil { hwnd := t.hwnd.Load() t.cleanup() destroyWindow(hwnd) t.ready <- err close(t.done) return } t.ready <- nil t.messageLoop() t.cleanup() close(t.done) } func (t *windowsTray) init() error { t.wndProc = windows.NewCallback(t.windowProc) instance, _, _ := procGetModuleHandle.Call(0) className, err := windows.UTF16PtrFromString(fmt.Sprintf("SGGAISkillManagerTrayWindow-%d", os.Getpid())) if err != nil { return err } class := wndClassEx{ cbSize: uint32(unsafe.Sizeof(wndClassEx{})), wndProc: t.wndProc, instance: instance, className: className, } if ret, _, callErr := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&class))); ret == 0 { return syscallError("RegisterClassExW", callErr) } hwnd, _, callErr := procCreateWindowEx.Call( 0, uintptr(unsafe.Pointer(className)), 0, 0, 0, 0, 0, 0, hwndMessage, 0, instance, 0, ) if hwnd == 0 { return syscallError("CreateWindowExW", callErr) } t.hwnd.Store(hwnd) icon, destroyIcon, err := loadTrayIcon(t.controller.config.IconPath) if err != nil { return err } t.icon = icon t.destroyIcon = destroyIcon nid := t.newNotifyIconData() if !shellNotifyIcon(nimAdd, &nid) { return fmt.Errorf("Shell_NotifyIconW add failed") } nid.version = notifyIconVersion if !shellNotifyIcon(nimSetVersion, &nid) { return fmt.Errorf("Shell_NotifyIconW set version failed") } return nil } func (t *windowsTray) messageLoop() { var message msg for { ret, _, _ := procGetMessage.Call(uintptr(unsafe.Pointer(&message)), 0, 0, 0) if int32(ret) <= 0 { return } procTranslateMsg.Call(uintptr(unsafe.Pointer(&message))) procDispatchMsg.Call(uintptr(unsafe.Pointer(&message))) } } func (t *windowsTray) stop() { t.stopOnce.Do(func() { if hwnd := t.hwnd.Load(); hwnd != 0 { procPostMessage.Call(hwnd, wmClose, 0, 0) <-t.done } }) } func (t *windowsTray) windowProc(hwnd uintptr, message uint32, wparam uintptr, lparam uintptr) uintptr { switch message { case trayCallbackMessage: if t.controller.handleTrayEvent(lparam) { return 0 } case wmClose: t.cleanup() procDestroyWindow.Call(hwnd) return 0 case wmDestroy: procPostQuitMessage.Call(0) return 0 } ret, _, _ := procDefWindowProc.Call(hwnd, uintptr(message), wparam, lparam) return ret } func (t *windowsTray) cleanup() { t.cleanupOnce.Do(func() { if hwnd := t.hwnd.Load(); hwnd != 0 { nid := t.newNotifyIconData() shellNotifyIcon(nimDelete, &nid) t.hwnd.Store(0) } if t.icon != 0 && t.destroyIcon { procDestroyIcon.Call(t.icon) } }) } func destroyWindow(hwnd uintptr) { if hwnd != 0 { procDestroyWindow.Call(hwnd) } } func (t *windowsTray) newNotifyIconData() notifyIconData { nid := notifyIconData{ cbSize: uint32(unsafe.Sizeof(notifyIconData{})), hwnd: t.hwnd.Load(), uid: 1, flags: nifMessage | nifIcon | nifTip, callbackMessage: trayCallbackMessage, icon: t.icon, } copyUTF16(nid.tip[:], t.controller.config.Tooltip) return nid } func loadTrayIcon(iconPath string) (uintptr, bool, error) { if icon, ok := loadIconFromFile(iconPath); ok { return icon, true, nil } if icon, ok := extractExecutableIcon(); ok { return icon, true, nil } if icon, _, _ := procLoadIcon.Call(0, idiApplication); icon != 0 { return icon, false, nil } return 0, false, fmt.Errorf("load tray icon failed") } func loadIconFromFile(iconPath string) (uintptr, bool) { if iconPath == "" { return 0, false } absolutePath, err := filepath.Abs(iconPath) if err != nil { return 0, false } if _, err := os.Stat(absolutePath); err != nil { return 0, false } path, err := windows.UTF16PtrFromString(absolutePath) if err != nil { return 0, false } width := systemMetric(smCxSmallIcon, 16) height := systemMetric(smCySmallIcon, 16) icon, _, _ := procLoadImage.Call( 0, uintptr(unsafe.Pointer(path)), imageIcon, uintptr(width), uintptr(height), lrLoadFromFile, ) return icon, icon != 0 } func extractExecutableIcon() (uintptr, bool) { exePath, err := os.Executable() if err != nil { return 0, false } exe, err := windows.UTF16PtrFromString(exePath) if err != nil { return 0, false } var largeIcon uintptr var smallIcon uintptr count, _, _ := procExtractIconEx.Call( uintptr(unsafe.Pointer(exe)), 0, uintptr(unsafe.Pointer(&largeIcon)), uintptr(unsafe.Pointer(&smallIcon)), 1, ) if count == 0 { return 0, false } if smallIcon != 0 { if largeIcon != 0 { procDestroyIcon.Call(largeIcon) } return smallIcon, true } return largeIcon, largeIcon != 0 } func systemMetric(index int32, fallback int) int { value, _, _ := procGetSystemMetric.Call(uintptr(index)) if value == 0 { return fallback } return int(value) } func shellNotifyIcon(command uintptr, data *notifyIconData) bool { ret, _, _ := procShellNotifyIcon.Call(command, uintptr(unsafe.Pointer(data))) return ret != 0 } func copyUTF16(destination []uint16, value string) { encoded, err := windows.UTF16FromString(value) if err != nil { return } if len(encoded) > len(destination) { encoded = encoded[:len(destination)] encoded[len(encoded)-1] = 0 } copy(destination, encoded) } func syscallError(operation string, err error) error { if errno, ok := err.(syscall.Errno); ok && errno == 0 { return fmt.Errorf("%s failed", operation) } return fmt.Errorf("%s failed: %w", operation, err) }