163 lines
3.8 KiB
Go
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")
|
|
}
|