tray

package module
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: BSD-3-Clause Imports: 12 Imported by: 0

README

tray — cross-platform system tray for go-widgets

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

Usage

menu := tray.NewMenu().Add(
    tray.Item("Open", func() { open() }),
    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

Status

  • Core (Tray, Menu, MenuItem, item activation/toggle, Backend interface, headless backend) — done, 100% covered, builds on every arch.
  • Native backends are opt-in via the tray_native build tag, so the core keeps its 100% coverage gate while the native code is compile-verified per-OS in CI. A tray can only be runtime-verified on a live desktop session.
    • darwin — implemented: NSStatusItem+NSMenu via ebitengine/purego, CGO=0. Compile-verified; runtime confirmation on a real macOS session is pending. Build/run your app with -tags tray_native.
    • windows / linux — next increments (currently nil under the tag → ErrNoBackend).

Without the tag (or a native backend for the OS), Run returns ErrNoBackend; supply one — including the headless backend — via WithBackend.

go run -tags tray_native ./cmd/yourapp   # link the macOS NSStatusItem backend

BSD-3-Clause. Copyright the go-widgets authors.

Documentation

Overview

Package tray is a cross-platform system-tray (menu-bar) widget for go-widgets.

A tray icon is OS-integration, not a pixel-blitted widget, so it cannot live in the pure-blitting toolkit. This package models the tray, its menu and menu items in a platform-agnostic core, and drives them through a small Backend interface implemented per-OS:

  • darwin: NSStatusItem + NSMenu via purego + the Objective-C runtime
  • windows: Shell_NotifyIcon + TrackPopupMenu via x/sys/windows
  • linux: StatusNotifierItem + com.canonical.dbusmenu over DBus

All CGO_ENABLED=0. A headless backend backs tests and display-less CI.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNoApplication reports that +[NSApplication sharedApplication] yielded
	// nil, which on a Mac with a window server means one thing: AppKit is not
	// loaded in this process, so the NSApplication class does not exist and the
	// class lookup returned the nil class.
	ErrNoApplication = errors.New("tray: +[NSApplication sharedApplication] returned nil (AppKit is not loaded in this process)")

	// ErrNoStatusBar reports that +[NSStatusBar systemStatusBar] yielded nil.
	// There is no menu bar to put anything in: no window server, or a session
	// that has none.
	ErrNoStatusBar = errors.New("tray: +[NSStatusBar systemStatusBar] returned nil (no menu bar in this session)")

	// ErrNoStatusItem reports that -[NSStatusBar statusItemWithLength:] yielded
	// nil: the menu bar exists but refused this process an item in it.
	ErrNoStatusItem = errors.New("tray: -[NSStatusBar statusItemWithLength:] returned nil (the menu bar refused this process an item)")

	// ErrNoTargetClass reports that the Objective-C class carrying the menu
	// action could not be created. Every clickable row needs an instance of it
	// as its target, and a row whose target is nil draws perfectly and answers
	// no click.
	ErrNoTargetClass = errors.New("tray: the Objective-C action target class could not be created")
)

Errors reported by the native macOS backend when AppKit hands back nothing. They are stable and may be tested with errors.Is.

View Source
var ErrNoBackend = errors.New("tray: no backend for this platform")

ErrNoBackend is returned by Run when no platform backend has been set (and none was selected for the current OS).

Functions

func BindIcon added in v0.5.0

func BindIcon[S comparable](t *Tray, state *mvvm.Observable[S], icons Icons[S], period time.Duration) (stop func())

BindIcon makes the tray's icon follow state.

Whenever state changes the icon becomes that state's entry, animating through its frames over period when there is more than one. It is a package function rather than a method because a method cannot carry its own type parameter, and the state a caller watches is theirs to name — a string, an enum, a bool.

A state with no entry leaves the icon alone rather than blanking it: a tray that goes blank on an unmapped state looks broken, and it is the caller's map that is incomplete, not the tray.

The returned function stops the animation and unsubscribes. It is safe to call more than once, and it must be called: neither the goroutine nor the subscription ends on its own.

Types

type Backend

type Backend interface {
	// Run shows the tray and blocks on the platform event loop until Quit.
	Run(t *Tray) error
	// Refresh re-applies the tray's icon, tooltip and menu after a change.
	Refresh(t *Tray)
	// Quit stops the event loop started by Run.
	Quit()
}

Backend drives a Tray on a specific platform.

type Headless

type Headless struct {
	Started   bool
	Refreshes int
	LastIcon  []byte
	LastTip   string
	LastMenu  *Menu
	// contains filtered or unexported fields
}

Headless is a display-less Backend for tests and CI. It records the tray state applied to it and blocks Run until Quit, so a tray can be exercised end-to-end without a real desktop session.

func NewHeadless

func NewHeadless() *Headless

NewHeadless returns a ready headless backend.

func (*Headless) Quit

func (h *Headless) Quit()

Quit unblocks Run (idempotent).

func (*Headless) Refresh

func (h *Headless) Refresh(t *Tray)

Refresh snapshots the tray's current state.

func (*Headless) Run

func (h *Headless) Run(t *Tray) error

Run marks the tray started, snapshots its state, signals readiness and blocks until Quit.

func (*Headless) Snapshot added in v0.5.0

func (h *Headless) Snapshot() (icon []byte, tip string, menu *Menu)

Snapshot returns the state this backend last recorded, under the lock.

Reading the fields directly is safe only while nothing can refresh concurrently. That used to be every caller; an icon bound to an observable state refreshes from the animator's own goroutine, so a reader that wants to watch it happen needs this.

type Icons added in v0.5.0

type Icons[S comparable] map[S][][]byte

Icons is what the menu bar shows for each state of whatever the tray is watching: one entry per state, and each entry is one or more PNG frames.

A single frame is a still icon. Several frames are an animation, which is the point — "something is happening" is the one thing a menu bar can say without the user opening anything, and a still icon cannot say it.

type Menu struct {
	Items []*MenuItem
}

Menu is an ordered list of items.

func NewMenu

func NewMenu() *Menu

NewMenu returns an empty menu.

func (m *Menu) Add(items ...*MenuItem) *Menu

Add appends items and returns the menu for chaining.

func (m *Menu) Find(path ...int) *MenuItem

Find returns the item at the given path of indices (descending into submenus), or nil if the path is invalid.

type MenuItem struct {
	Label     string
	Tooltip   string
	Checked   bool
	Disabled  bool
	Separator bool
	// OnClick is invoked when the item is activated. For a checkbox item the
	// Checked field is toggled before OnClick runs.
	OnClick func()
	// Submenu, when non-nil, makes this item open a nested menu (its OnClick is
	// then ignored).
	Submenu *Menu
	// contains filtered or unexported fields
}

MenuItem is one entry in a tray menu.

func Checkbox

func Checkbox(label string, checked bool, onToggle func(bool)) *MenuItem

Checkbox is a toggleable item; onToggle receives the new checked state.

func Item

func Item(label string, onClick func()) *MenuItem

Item is a plain clickable menu item.

func Separator

func Separator() *MenuItem

Separator is a divider line.

func SubMenu(label string, sub *Menu) *MenuItem

SubMenu is an item that opens a nested menu.

func (it *MenuItem) Activate()

Activate dispatches a click on the item: it flips a checkbox's state and invokes the appropriate callback. Separators, disabled items and submenu parents do nothing. Backends call this when the user picks an item.

type Tray

type Tray struct {
	// contains filtered or unexported fields
}

Tray is a system-tray icon with a tooltip and a menu.

func New

func New(iconPNG []byte) *Tray

New creates a tray showing iconPNG (PNG-encoded bytes). The platform backend is selected automatically; use WithBackend to override (eg. for tests).

func (*Tray) Attach

func (t *Tray) Attach() error

Attach shows the tray inside a host-owned event loop and returns immediately, instead of Run's block-until-Quit. Use it from an application that already drives the platform's main run loop (its own window): Run would try to start a second loop, whereas Attach just registers the tray with the running one. It must be called on the platform's main/UI thread. Returns ErrNoBackend when the active backend does not support attaching.

func (*Tray) Icon

func (t *Tray) Icon() []byte

Accessors used by backends.

func (*Tray) Menu

func (t *Tray) Menu() *Menu

func (*Tray) OnReady

func (t *Tray) OnReady(fn func()) *Tray

OnReady registers a callback run once the tray is live (after Run starts).

func (*Tray) Quit

func (t *Tray) Quit()

Quit stops the tray's event loop.

func (*Tray) Run

func (t *Tray) Run() error

Run shows the tray and blocks until Quit. It errors if no backend is set.

func (*Tray) SetIcon

func (t *Tray) SetIcon(iconPNG []byte) *Tray

SetIcon replaces the icon (PNG bytes) and refreshes if running.

func (*Tray) SetMenu

func (t *Tray) SetMenu(m *Menu) *Tray

SetMenu sets the tray menu and refreshes if running.

func (*Tray) SetTooltip

func (t *Tray) SetTooltip(s string) *Tray

SetTooltip sets the hover tooltip and refreshes if running.

func (*Tray) Tooltip

func (t *Tray) Tooltip() string

func (*Tray) WithBackend

func (t *Tray) WithBackend(b Backend) *Tray

WithBackend overrides the platform backend and returns the tray.

Directories

Path Synopsis
examples
traydemo command
Command traydemo is a runnable go-widgets/tray example.
Command traydemo is a runnable go-widgets/tray example.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL