Pure-Go (CGO_ENABLED=0) interop with the macOS notification and event-bus
mechanisms. Everything is reached through
ebitengine/purego — dlopen +
objc_msgSend and dlsym'd libSystem C functions — so it links with no cgo
and never shells out to osascript. It is the macOS counterpart to the Linux
go-freedesktop/notifications work.
| API | Backing | Cross-process | Payload | Needs Run? |
|---|---|---|---|---|
Subscribe / Post / Cancel |
Darwin notify(3) |
yes | none (named signal) | no |
SubscribeDistributed / PostDistributed |
NSDistributedNotificationCenter |
yes | map[string]string userInfo |
subscribe: yes; post: no |
PostUserNotification |
NSUserNotificationCenter |
— | title/subtitle/body | no (best-effort, see below) |
// notify(3) event bus — subscribe to a system key, no run loop needed.
tok, _ := notify.Subscribe("com.apple.system.timezone", func() {
log.Println("time zone changed")
})
defer notify.Cancel(tok)
// NSDistributedNotificationCenter — cross-process, with a userInfo payload.
go notify.Run(ctx) // drives the Foundation run loop on its own thread
notify.SubscribeDistributed("com.example.event", func(ui map[string]string) {
log.Println("got", ui)
})
notify.PostDistributed("com.example.event", map[string]string{"k": "v"})-
notify(3) uses
notify_register_checkpolled everyPollInterval(default 50 ms), notnotify_register_dispatchornotify_register_file_descriptor. A dispatch block cannot be synthesised underCGO_ENABLED=0, and — verified exhaustively on-device — a mach-port-backed notify file descriptor living in the process breaksNSDistributedNotificationCentermach delivery. The shared-memory check path touches no fd and no mach port. The trade-off is up to one poll interval of latency and per-interval coalescing of repeated posts, which matchesnotify(3)'s own coalescing. -
Run(ctx)drives a Foundation run loop pinned to its goroutine's OS thread and services the Objective-C work queued by distributed subscriptions. Call it once on a dedicated goroutine; it blocks untilctxis cancelled. Distributed posting does not require it. -
PostUserNotificationis best-effort and deprecated-API-based. macOS only shows the banner when the process has a bundle identity (a real.appwith anInfo.plist); from a bare CLI+defaultUserNotificationCenterusually returns nil, reported asErrNoUserCenter. The modernUNUserNotificationCenterhard-requires a bundled, signed app plus an authorization prompt, so it is intentionally not wrapped.
Initialising libnotify's registration machinery can disrupt
NSDistributedNotificationCenter delivery that is established afterwards in
the same process — a macOS libnotify/distnoted interaction, not a bug in this
package (each mechanism is fully reliable on its own). If a process needs both,
start Run and your distributed subscriptions before the first Subscribe,
or isolate the two in separate processes.
Darwin only. Every exported symbol is defined on all platforms so consumers
cross-compile; on non-darwin GOOS the functions return ErrUnsupported.
The darwin lane runs real, on-device round trips (post → receive) for both the
notify(3) bus and NSDistributedNotificationCenter, asserting the handler
fires and the userInfo survives. The OS-independent logic (name validation,
token bookkeeping, dictionary marshalling) is covered to 100%. CGO_ENABLED=0
throughout — purego needs no cgo.
CGO_ENABLED=0 go test ./...
BSD-3-Clause. See LICENSE.