惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园_首页
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
J
Java Code Geeks
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Google DeepMind News
Google DeepMind News
F
Fortinet All Blogs
V
V2EX
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Engineering at Meta
Engineering at Meta
月光博客
月光博客
阮一峰的网络日志
阮一峰的网络日志
M
MIT News - Artificial intelligence
IT之家
IT之家
博客园 - 【当耐特】
U
Unit 42
云风的 BLOG
云风的 BLOG
L
LangChain Blog
小众软件
小众软件
Microsoft Security Blog
Microsoft Security Blog
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
宝玉的分享
宝玉的分享
N
Netflix TechBlog - Medium

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
We Built a Pure Go System Tray Library Because Every Alte...
Andrey Kolko · 2026-05-01 · via DEV Community

"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.


The Problem

Every Go system tray library requires CGO:

Library Stars CGO? The Catch
getlantern/systray 3.3K Yes (macOS, Linux) AppIndicator + GTK3 on Linux, Cocoa via CGO on macOS
fyne-io/systray fork Yes (macOS, Linux) Same CGO deps, fork of getlantern
energye/systray Yes Walk/LCL dependency

CGO means:

  • Need a C compiler installed (apt install gcc, Xcode, MinGW)
  • Cross-compilation breaks (GOOS=linux from macOS? Good luck with CGO)
  • Larger binaries, slower builds
  • CGO_ENABLED=0 doesn't work

Go is famous for "single binary, cross-compile anywhere." CGO breaks that promise.


The Solution: Native APIs via Pure Go FFI

We went platform-native without CGO:

Platform Native API Go FFI LOC
Windows Shell_NotifyIconW (shell32.dll) golang.org/x/sys/windows 1,027
macOS NSStatusBar / NSStatusItem (AppKit) go-webgpu/goffi (ObjC runtime) 1,385
Linux StatusNotifierItem (D-Bus SNI) godbus/dbus/v5 810

No C compiler. No shared libraries. No dlopen of GTK. Just Go talking directly to the OS.

Windows: Shell_NotifyIconW

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.

Key details:

  • Message-only HWND for callbacks (invisible, no taskbar entry)
  • NOTIFYICON_VERSION_4 for modern event dispatch
  • Explorer crash recovery — when explorer.exe restarts, tray icons disappear. We listen for the TaskbarCreated registered message and re-add the icon automatically.
  • Dark mode auto-switching — detect WM_SETTINGCHANGE + ImmersiveColorSet, read SystemUsesLightTheme registry key, swap HICON. Your tray icon adapts when the user toggles Windows dark mode.

macOS: NSStatusBar via ObjC Runtime

This is where it gets interesting. Calling AppKit without CGO requires speaking the Objective-C runtime protocol:

  1. objc_getClass("NSStatusBar") — get the class
  2. objc_msgSend(class, sel("systemStatusBar")) — get the shared status bar
  3. objc_msgSend(statusBar, sel("statusItemWithLength:"), -1.0) — create a status item

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.

tray.SetTemplateIcon(monochromePNG)

Enter fullscreen mode Exit fullscreen mode

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: D-Bus StatusNotifierItem

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:

  • Properties: Category, Id, Title, Status, IconPixmap, ToolTip, Menu
  • Methods: Activate (click), SecondaryActivate (middle-click), ContextMenu (right-click)
  • Signals: NewIcon, NewTitle, NewStatus

com.canonical.dbusmenu — the context menu:

  • A recursive tree of menu items with labels, types, toggle states
  • GetLayout returns the full tree, Event dispatches clicks

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.


The API

We went with a builder pattern inspired by Wails 3:

package main

import (
    "fmt"
    "os"
    "github.com/gogpu/systray"
)

func main() {
    tray := systray.New()

    menu := systray.NewMenu()
    menu.Add("Open", func() { fmt.Println("Opening...") })
    menu.AddSeparator()
    menu.AddCheckbox("Dark Mode", false, func() { fmt.Println("Toggled!") })
    menu.AddSubmenu("More...", systray.NewMenu().
        Add("About", func() { fmt.Println("v1.0") }).
        Add("Help", func() { fmt.Println("Help!") }))
    menu.AddSeparator()
    menu.Add("Quit", func() { tray.Remove(); os.Exit(0) })

    tray.SetIcon(iconPNG).
        SetDarkModeIcon(darkIconPNG).
        SetTooltip("My App").
        SetMenu(menu).
        Show()

    tray.OnClick(func() { fmt.Println("Clicked!") })
    tray.Run() // blocks, pumps platform messages
}

Enter fullscreen mode Exit fullscreen mode

Multiple trays are supported — each call to systray.New() creates an independent icon:

mainTray := systray.New().SetIcon(appIcon).SetMenu(mainMenu).Show()
statusTray := systray.New().SetIcon(statusIcon).SetTooltip("Status: OK").Show()

Enter fullscreen mode Exit fullscreen mode


Enterprise Research

We didn't guess at the architecture. Before writing code, we studied how the big frameworks do it:

Framework Tray Architecture Our Takeaway
Qt6 QPlatformSystemTrayIcon → 3 platform implementations Three-layer pattern (public API → interface → platform impl)
Wails 3 systemTrayImpl interface, native per-platform Builder API pattern, multiple tray support
SDL3 4 backends (AppIndicator, D-Bus, Win32, Cocoa) We chose D-Bus SNI directly, skipping AppIndicator
Electron nativeTheme.shouldUseDarkColors Dark mode detection via WM_SETTINGCHANGE
GLFW RemovePropW before DestroyWindow Destroy pattern (avoid deadlocks)
getlantern/systray GetMessage loop, hidden WS_OVERLAPPEDWINDOW Message pump pattern (we use HWND_MESSAGE instead)

The architecture follows Qt6's QPlatformSystemTrayIcon pattern:

systray.New()  →  SystemTray (public API, delegation)
                       │
                  PlatformTray (internal interface)
                       │
          ┌────────────┼────────────┐
     Win32 impl   macOS impl   Linux impl
     Shell_Notify  NSStatusBar   D-Bus SNI

Enter fullscreen mode Exit fullscreen mode


Why Not AppIndicator on Linux?

The tempting path: dlopen("libayatana-appindicator3.so.1") and let GTK3 handle everything. That's what getlantern/systray does (via CGO).

Problems:

  1. Pulls in GTK3 runtime — gigantic dependency for a tray icon
  2. It's just a wrapper around SNI — AppIndicator talks D-Bus SNI internally
  3. Icon caching bugs — AppIndicator caches icons by filename, causing stale icons
  4. Not available everywhere — minimal compositors (Sway, Hyprland) don't have AppIndicator

We cut out the middleman. D-Bus SNI directly via godbus — same protocol, no GTK, no CGO.


The Numbers

Total:     ~5,800 lines of Pure Go (6,900 with docs/CI/configs)
Tests:     74 (84% public API coverage)
Platforms: Windows ✅, macOS ✅, Linux ✅
Deps:      golang.org/x/sys, go-webgpu/goffi, godbus/dbus/v5
CGO:       Zero. Absolutely zero.

Enter fullscreen mode Exit fullscreen mode


Try It

go get github.com/gogpu/systray@v0.1.0

Enter fullscreen mode Exit fullscreen mode

Run the example:

git clone https://github.com/gogpu/systray
cd systray/examples/basic
go run .

Enter fullscreen mode Exit fullscreen mode

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.


Part of GoGPU

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:

Library What It Does
wgpu Pure Go WebGPU (Vulkan/Metal/DX12/GLES)
naga Shader compiler (WGSL → SPIR-V/MSL/GLSL/HLSL/DXIL)
gg 2D graphics (Skia-class rasterizer)
gogpu App framework, windowing, input
ui GUI toolkit (22+ widgets, Material 3)
systray System tray (this library)

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.