//go:build windows package notify import ( "fmt" "sync" "syscall" "unsafe" ) // notifier shows balloon notifications via Shell_NotifyIconW. This is a // self-contained approach (no Start Menu shortcut or AppID registration // required), unlike WinRT toast notifications. The balloon appears in the // notification area and a small tray icon is kept for the daemon's lifetime. type notifier struct{} // New returns a Windows notifier using Shell_NotifyIcon balloon tips. func New() Notifier { return notifier{} } const ( nifInfo = 0x00000010 nimAdd = 0x00000000 nimModify = 0x00000001 niiInfo = 0x00000001 ) // hwndMessage is the HWND_MESSAGE (-3) pseudo-handle for a message-only window. const hwndMessage = ^uintptr(2) var ( kernel32 = syscall.NewLazyDLL("kernel32.dll") user32 = syscall.NewLazyDLL("user32.dll") shell32 = syscall.NewLazyDLL("shell32.dll") procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW") procRegisterClassExW = user32.NewProc("RegisterClassExW") procCreateWindowExW = user32.NewProc("CreateWindowExW") procDefWindowProcW = user32.NewProc("DefWindowProcW") procShellNotifyIconW = shell32.NewProc("Shell_NotifyIconW") ) type wndClassExW struct { cbSize uint32 style uint32 lpfnWndProc uintptr cbClsExtra int32 cbWndExtra int32 hInstance uintptr hIcon uintptr hCursor uintptr hbrBackground uintptr lpszMenuName *uint16 lpszClassName *uint16 hIconSm uintptr } type notifyIconDataW struct { cbSize uint32 hWnd uintptr uID uint32 uFlags uint32 uCallbackMessage uint32 hIcon uintptr szTip [128]uint16 dwState uint32 dwStateMask uint32 szInfo [256]uint16 uTimeout uint32 szInfoTitle [64]uint16 dwInfoFlags uint32 guidItem [16]byte hBalloonIcon uintptr } var ( notifyOnce sync.Once notifyHwnd uintptr notifyErr error iconMu sync.Mutex iconAdded bool ) func setup() { hInstance, _, _ := procGetModuleHandleW.Call(0) if hInstance == 0 { notifyErr = syscall.EINVAL return } className, _ := syscall.UTF16PtrFromString("clip-sync-notify") wc := wndClassExW{ cbSize: uint32(unsafe.Sizeof(wndClassExW{})), lpfnWndProc: procDefWindowProcW.Addr(), hInstance: hInstance, lpszClassName: className, } // Registration may fail if already registered (e.g. re-run) — that's fine. _, _, _ = procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))) hwnd, _, _ := procCreateWindowExW.Call( 0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(className)), 0, 0, 0, 0, 0, hwndMessage, 0, hInstance, 0, ) notifyHwnd = hwnd if hwnd == 0 { notifyErr = fmt.Errorf("notify: CreateWindowExW failed") } } func (notifier) Notify(title, body string) error { notifyOnce.Do(setup) if notifyErr != nil { return notifyErr } var nid notifyIconDataW nid.cbSize = uint32(unsafe.Sizeof(nid)) nid.hWnd = notifyHwnd nid.uID = 1 nid.uFlags = nifInfo nid.uTimeout = 5000 nid.dwInfoFlags = niiInfo if info, err := syscall.UTF16FromString(body); err == nil { copy(nid.szInfo[:], info) } if title16, err := syscall.UTF16FromString(title); err == nil { copy(nid.szInfoTitle[:], title16) } iconMu.Lock() defer iconMu.Unlock() cmd := uintptr(nimAdd) if iconAdded { cmd = nimModify } r, _, _ := procShellNotifyIconW.Call(cmd, uintptr(unsafe.Pointer(&nid))) if r != 0 { iconAdded = true return nil } // Fall back to the other command on failure (e.g. icon already/not yet present). other := uintptr(nimAdd) if cmd == nimAdd { other = nimModify } r, _, _ = procShellNotifyIconW.Call(other, uintptr(unsafe.Pointer(&nid))) if r != 0 { iconAdded = true return nil } return fmt.Errorf("notify: Shell_NotifyIconW failed") }