Tools: We Built a Pure Go System Tray Library Because Every Alternative Requires CGO, GoGPU May 2026
The Problem
The Solution: Native APIs via Pure Go FFI
Windows: Shell_NotifyIconW
macOS: NSStatusBar via ObjC Runtime
Linux: D-Bus StatusNotifierItem
The API
Enterprise Research
Why Not AppIndicator on Linux?
The Numbers
Try It
Part of GoGPU "Love the no CGO — but quickly realized there's no code?"
— @cmilesio, gogpu/systray#1 Fair point. We published the repo with just a README and a dream. Three days later: 5,800+ lines of Pure Go, three platforms, 74 tests, 84% coverage, and a working system tray icon on Windows. Today we're releasing gogpu/systray v0.1.0 — the first Pure Go system tray library that works on Windows, macOS, and Linux without a C compiler. Every Go system tray library requires CGO: Go is famous for "single binary, cross-compile anywhere." CGO breaks that promise. We went platform-native without CGO: No C compiler. No shared libraries. No dlopen of GTK. Just Go talking directly to the OS. The Win32 approach is straightforward — Shell_NotifyIconW has been the tray API since Windows 95. We call it via golang.org/x/sys/windows, the same way the Go standard library talks to Windows. This is where it gets interesting. Calling AppKit without CGO requires speaking the Objective-C runtime protocol: We built a minimal ObjC runtime wrapper (~490 LOC) using goffi — our Pure Go FFI library. Same approach we use for the Metal GPU backend in gogpu/wgpu. The killer feature on macOS: template icons. This calls [NSImage setTemplate:YES], telling macOS the icon is a monochrome mask. The OS automatically renders it white on dark menu bars, black on light ones. No dark mode handling needed — Apple does it for you. Linux is the most complex platform. The "system tray" isn't a single API — it's a D-Bus protocol called StatusNotifierItem (SNI). We implement two D-Bus interfaces: org.kde.StatusNotifierItem — the tray icon itself: com.canonical.dbusmenu — the context menu: And a registration dance with org.kde.StatusNotifierWatcher — plus automatic re-registration when the desktop panel restarts. The PNG→ARGB conversion is a fun detail: SNI wants ARGB32 in network byte order (big-endian), so we decode the PNG with image/png and manually pack [A, R, G, B] bytes. All of this via godbus/dbus/v5 — the canonical Pure Go D-Bus library. Zero CGO. We went with a builder pattern inspired by Wails 3: Multiple trays are supported — each call to systray.New() creates an independent icon: We didn't guess at the architecture. Before writing code, we studied how the big frameworks do it: The architecture follows Qt6's QPlatformSystemTrayIcon pattern: The tempting path: dlopen("libayatana-appindicator3.so.1") and let GTK3 handle everything. That's what getlantern/systray does (via CGO). We cut out the middleman. D-Bus SNI directly via godbus — same protocol, no GTK, no CGO. A green icon appears in your system tray. Right-click for the menu. Toggle dark mode to see auto-switching. We need testers — especially on macOS and Linux (KDE, GNOME + AppIndicator extension, XFCE, Sway). File issues if something doesn't work. systray is standalone (go get github.com/gogpu/systray — no gogpu dependency), but it's designed to integrate with the GoGPU ecosystem — 800K+ lines of Pure Go GPU code: All Pure Go. All zero CGO. All cross-platform. Four gogpu libraries are listed in awesome-go: systray (GUI Interaction), ui (GUI Toolkits), gg (Images), gogpu (Game Development). If you build something with systray, let us know. Star ⭐ the repo if you find it useful — it helps others discover the project. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse