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 ¶
- Variables
- func BindIcon[S comparable](t *Tray, state *mvvm.Observable[S], icons Icons[S], period time.Duration) (stop func())
- func IsTemplate(iconPNG []byte) bool
- type Backend
- type Headless
- type Icons
- type Menu
- type MenuItem
- type Tray
- func (t *Tray) Attach() error
- func (t *Tray) Icon() []byte
- func (t *Tray) Menu() *Menu
- func (t *Tray) OnReady(fn func()) *Tray
- func (t *Tray) Quit()
- func (t *Tray) Run() error
- func (t *Tray) SetIcon(iconPNG []byte) *Tray
- func (t *Tray) SetMenu(m *Menu) *Tray
- func (t *Tray) SetTooltip(s string) *Tray
- func (t *Tray) Tooltip() string
- func (t *Tray) WithBackend(b Backend) *Tray
Constants ¶
This section is empty.
Variables ¶
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.
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.
func IsTemplate ¶ added in v0.7.0
IsTemplate reports whether these PNG bytes should be drawn as a TEMPLATE image: a shape the platform recolours to suit its menu bar.
It is decided from the picture rather than asked of the caller, because the answer is IN the picture. A monochrome glyph is a template -- macOS draws it dark on a light bar, light on a dark one, white while the item is pressed, and correct in a tinted bar without anybody choosing a colour. An icon that carries colour is not: a template is recoloured, so a green dot meant to say "this is running" would come out the same shade as everything around it, and the one thing it was for would be gone.
Anything that cannot be decoded is treated as a template, which is what every icon was before this existed.
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 (*Headless) Run ¶
Run marks the tray started, snapshots its state, signals readiness and blocks until Quit.
func (*Headless) Snapshot ¶ added in v0.5.0
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 ¶
type Menu struct {
Items []*MenuItem
}
Menu is an ordered list of items.
type MenuItem ¶
type MenuItem struct {
Label string
Tooltip string
Checked bool
Disabled bool
Separator bool
// Icon is a small PNG drawn to the left of the label, in the same encoding
// as the tray's own icon (see [New]). Nil leaves the row text-only.
//
// It is PNG bytes rather than an image.Image for the same reason the tray
// icon is: a caller ships one artefact that every backend decodes, instead
// of each backend agreeing on a pixel layout with the caller.
//
// A backend scales it to the height its platform draws menu rows at and
// keeps the aspect ratio, so an icon does not have to be authored at a
// particular size -- extra pixels become resolution, not dimensions. A
// monochrome glyph is drawn as a TEMPLATE (see [IsTemplate]) and so follows
// a light or dark menu; one that carries colour keeps its colour.
//
// Honoured today by the macOS backend. The Windows and Linux backends
// ignore it, and a row that carries one there simply draws as it did
// before -- the field is not a promise those platforms have kept yet.
Icon []byte
// OnClick is invoked when the item is activated. For a checkbox item the
// Checked field is toggled before OnClick runs.
OnClick func()
// then ignored).
Submenu *Menu
// contains filtered or unexported fields
}
MenuItem is one entry in a tray menu.
func IconItem ¶ added in v0.8.0
IconItem is a clickable menu item carrying a PNG icon; see MenuItem.Icon.
type Tray ¶
type Tray struct {
// contains filtered or unexported fields
}
Tray is a system-tray icon with a tooltip and a menu.
What it SHOWS -- icon, tooltip, menu -- is read by the platform loop and written by whoever changes it, and those are not the same goroutine: an icon bound to application state (see BindIcon) is written by a ticker while the loop is drawing. So the three are behind a lock. The backend and the ready callback are not: they are set while the tray is being built, before anything runs, and a tray whose backend changed underneath a running loop would be a different bug entirely.
func New ¶
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 ¶
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 ¶
Accessors used by backends, safe to call from the platform loop while another goroutine is setting them.
func (*Tray) SetTooltip ¶
SetTooltip sets the hover tooltip and refreshes if running.
func (*Tray) WithBackend ¶
WithBackend overrides the platform backend and returns the tray.