Files
clip-sync/internal/notify/notify_windows.go
T

163 lines
3.8 KiB
Go

//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")
}