A system-tray / menu-bar icon with menus, submenus, checkboxes and separators.
A tray is OS-integration, not a pixel-blitted widget, so it lives outside the
pure-blitting toolkit and drives the native APIs through a small Backend
interface — all CGO_ENABLED=0:
| platform | native API | mechanism |
|---|---|---|
| darwin | NSStatusItem + NSMenu/NSMenuItem |
purego + the Objective-C runtime |
| windows | Shell_NotifyIcon + TrackPopupMenu |
golang.org/x/sys/windows syscalls |
| linux | StatusNotifierItem + com.canonical.dbusmenu |
pure-Go DBus |
menu := tray.NewMenu().Add(
tray.Item("Open", func() { open() }),
tray.IconItem("Pause", pausePNG, func() { pause() }), // a glyph beside the label
tray.Checkbox("Notifications", true, func(on bool) { setNotify(on) }),
tray.SubMenu("Recent", tray.NewMenu().Add(tray.Item("file.txt", nil))),
tray.Separator(),
tray.Item("Quit", func() { t.Quit() }),
)
t := tray.New(iconPNG).SetTooltip("My App").SetMenu(menu)
t.OnReady(func() { /* live */ })
t.Run() // blocks on the platform event loop until Quit- Core (
Tray,Menu,MenuItem, item activation/toggle,Backendinterface, headless backend) — done, 100% covered, builds on every arch. - Native backends are on by default.
tray.New(icon).SetMenu(m).Run()puts an icon in the menu bar of the platform you built for, with no build tag and nothing else to know.- darwin —
NSStatusItem+NSMenuvia ebitengine/purego, CGO=0. Runtime-confirmed on a real macOS session: the item is the thing that leaves the menu bar when the program stops and comes back when it starts, and clicking it opens its menu. - windows / linux — implemented and compile-verified; runtime
confirmation pending. They ignore
MenuItem.Icon: a row carrying one draws as it did before, which is a gap, not a promise kept. - anything else —
defaultBackendis nil andRunreportsErrNoBackend, which is the difference between "there is no tray here" and "your tray silently does nothing".
- darwin —
A caller that wants no native tray at all — a test, a headless service — passes
one in: WithBackend(tray.NewHeadless()).
The native backends were opt-in behind -tags tray_native, so that the core
could keep a 100% coverage figure over the whole package. The cost was paid by
every caller: Run returned ErrNoBackend, nothing appeared anywhere, and
nothing said why. A program that did the obvious thing got a tray that quietly
did not exist.
The coverage gate now selects by SHAPE instead — everything that is not a
platform file (_darwin, _linux, _windows, _android, _js, _other) is
held at 100% — which is what the rest of this fleet does, and which gates a new
portable file the day it is written rather than the day somebody remembers it.
A library's default behaviour is not the place to keep its CI tidy.
BSD-3-Clause. Copyright the go-widgets authors.