feat: Windows notifications, benchmarks, GPG signing, docs site, one-liner installers
This commit is contained in:
@@ -2,12 +2,161 @@
|
||||
|
||||
package notify
|
||||
|
||||
// Notifier is a no-op on Windows. Native toast notifications require a
|
||||
// registered, packaged app (or a third-party module such as BurntToast), which
|
||||
// is out of scope for a single self-contained binary.
|
||||
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 no-op Windows notifier.
|
||||
// New returns a Windows notifier using Shell_NotifyIcon balloon tips.
|
||||
func New() Notifier { return notifier{} }
|
||||
|
||||
func (notifier) Notify(string, string) error { return nil }
|
||||
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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user