systray v0.2.0 - Multi Tray Callbacks and Dynamic Menus


gogpu systray v0.2.0 was published on July 27, 2026. It replaces shared tray state with per instance callback routing on macOS and private session bus connections on Linux, closing a correctness problem for processes that create more than one tray. This is a stable release, not a prerelease.

The full release notes and downloads are on the GitHub release page.

The previous macOS path used the global activeTray singleton. Version v0.2.0 replaces it with trayRegistryMap, keyed by the ObjC target. Native callbacks can therefore resolve the tray instance that owns the event instead of relying on one process wide active value.

Linux receives the same isolation at the session bus layer. Each tray now calls dbus.ConnectSessionBus() and gets a private connection. Each SNI bus name also has a unique PID-{trayID} suffix. The release notes tie both changes to the multi tray bug tracked as issue 7.

This is the main operational fix in the release. A desktop agent may expose separate controls for a scraper, local queue, or pipeline worker. If those controls live in one process, callback ownership must remain deterministic as tray instances are created and used. The new macOS registry and Linux connection model remove shared state from that routing path.

The new MenuItem update methods are SetLabel(), SetChecked(), SetDisabled(), and SetIcon(). They mutate an existing native item instead of requiring the caller to rebuild the complete menu. An optional MenuItemUpdater platform interface handles dispatch of those live changes.

The platform implementations use their native update mechanisms:

  • Windows calls SetMenuItemInfoW with MIIM_STRING and MIIM_STATE.

  • macOS finds an NSMenuItem through itemWithTag: and applies setTitle, setState, or setEnabled.

  • Linux emits the D-Bus ItemsPropertiesUpdated signal for the individual item.

For status oriented tools, this permits a menu label, checkbox, disabled state, or icon to follow current worker state without replacing unrelated items. The scope is narrow: the release adds item mutation, not a broader menu synchronization model.

The native mapping also gives platform operators a clear fault boundary. A stale Windows item points toward the SetMenuItemInfoW path. On macOS, lookup by itemWithTag: becomes the relevant boundary. Linux sends a property signal over the tray connection. The notes do not claim new retry, persistence, or acknowledgement semantics around these calls. Treat the methods as direct presentation state updates, not as confirmation that the worker action represented by an item has completed.

The menu builder API has a source breaking return type change. Menu.Add(), AddCheckbox(), AddSubmenu(), and AddWithIcon() now return *MenuItem rather than *Menu. That returned handle is what makes later calls such as SetLabel() possible.

Code that chained additions through the returned *Menu must be split into explicit statements:

menu := systray.NewMenu()
openItem := menu.Add("Open", openFn)
menu.AddSeparator()
quitItem := menu.Add("Quit", quitFn)

openItem.SetLabel("Open File")
quitItem.SetDisabled(true)

Menu.AddSeparator() still returns *Menu, since a separator has no dynamic properties to update. Audit any fluent calls that start with the four changed methods before upgrading. The dependency change called out in the notes is goffi from v0.6.0 to v0.6.2.

Standalone calls that ignore the result can keep doing so in Go. The required edits are concentrated in chains that expected an add method to return the menu. Keep the new handle when runtime state must change the item. Otherwise, an explicit add statement is enough. This limits the migration to menu construction sites and any code that will consume the new update methods.