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
- 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
- func Checkbox(label string, checked bool, onToggle func(bool)) *MenuItem
- func IconItem(label string, iconPNG []byte, onClick func()) *MenuItem
- func Item(label string, onClick func()) *MenuItem
- func KeyItem(label string, iconPNG []byte, key string, mods Mods, onClick func()) *MenuItem
- func Separator() *MenuItem
- func SubMenu(label string, sub *Menu) *MenuItem
- type Mods
- type Tray
- func (t *Tray) Attach() error
- func (t *Tray) Close()
- 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) SetTitle(s string) *Tray
- func (t *Tray) SetTooltip(s string) *Tray
- func (t *Tray) SetVisible(v bool) *Tray
- func (t *Tray) Title() string
- func (t *Tray) Tooltip() string
- func (t *Tray) Visible() bool
- func (t *Tray) WithBackend(b Backend) *Tray
Constants ¶
const ( KeyUp = "\uF700" KeyDown = "\uF701" KeyLeft = "\uF702" KeyRight = "\uF703" KeyReturn = "\r" KeyEscape = "\x1b" // KeyDelete is the BACKSPACE key -- the one above Return, which macOS // prints as ⌫. The forward delete key is [KeyForwardDelete]. KeyDelete = "\b" KeyForwardDelete = "\x7f" KeyTab = "\t" KeySpace = " " )
The keys a menu draws as a GLYPH rather than as a character.
They are the values AppKit expects in a key equivalent: the Unicode private-use codes for the function keys, and the control characters for the three that have one. A caller writes KeyLeft and gets ←, drawn by the platform in the platform's own arrow.
There is no constant for a letter or a digit: "s" and "7" are the key equivalent, and inventing a name for each would be a table to keep in step with a keyboard.
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
LastTitle string
LastTip string
LastMenu *Menu
LastVisible bool
// 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
// Key is the row's key equivalent -- the ONE CHARACTER the platform draws
// at the right of the row, aligned in a column with every other row's.
//
// A character and not a name: "s" for S, "=" for the equals key. The keys
// with no character of their own have constants -- [KeyUp], [KeyDown],
// [KeyLeft], [KeyRight], [KeyReturn], [KeyEscape], [KeyDelete] -- because a
// menu draws an arrow there, not the word "Up".
//
// ⚠ IT IS DRAWN, AND ON macOS IT IS ALSO BOUND, but only where a menu's key
// equivalents are ever consulted: the main menu, and a menu while it is
// open. A tray menu is neither for as long as it is shut, so for a status
// item this is display. An application that also owns a main menu should
// expect the combination to work there too.
//
// Honoured today by the macOS backend. The Windows and Linux backends ignore
// it, and a row that carries one there draws as it did before.
Key string
// Mods are the modifiers shown with [MenuItem.Key]. Zero draws the character
// alone, which is what a bare key equivalent looks like.
Mods Mods
// 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.
func KeyItem ¶ added in v0.9.0
KeyItem is a clickable menu item with an icon and a key equivalent.
The long form of IconItem: a row that says what it does, shows a glyph for it, and names the key that does the same thing.
type Mods ¶ added in v0.9.0
type Mods uint
Mods are the modifier keys shown beside a row's MenuItem.Key.
The package's own flags rather than the platform's: AppKit's mask, a Windows accelerator table and a DBus menu's shortcut string agree on nothing but the four keys themselves, so the portable half names those and each backend translates.
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). It starts visible — see SetVisible.
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) Close ¶ added in v0.9.0
func (t *Tray) Close()
Close removes this tray's item from the menu bar, for a Tray that was Attached (not Run) and whose life ends before the process's own — e.g. one of several per-account items in a host that keeps running after one account is removed. Unlike Quit, it does not stop the platform loop: a caller with several Attached items (and one Holding the loop) can Close any of the Attached ones without taking the others down.
On a backend with no such capability, Close falls back to Quit, which is at least as safe as doing nothing when there is only one item running the whole loop by itself.
func (*Tray) Icon ¶
Accessors used by backends, safe to call from the platform loop while another goroutine is setting them.
func (*Tray) SetTitle ¶ added in v0.9.0
SetTitle sets text drawn directly in the menu bar alongside (or instead of) the icon — the way a system meter shows "42%" rather than only a glyph — and refreshes if running. An empty title leaves only the icon, which is also SetTitle's zero-value behavior on a Tray nobody has called it on.
Honoured today by the macOS backend, which owns real screen space for it (the button holds both an image and a title). The Windows and Linux backends ignore it, the same partial-platform-support shape MenuItem.Icon already has: a caller on those platforms simply sees what it drew before, not an error.
func (*Tray) SetTooltip ¶
SetTooltip sets the hover tooltip and refreshes if running.
func (*Tray) SetVisible ¶ added in v0.9.0
SetVisible shows or hides the tray's own icon without releasing the platform loop it may be Holding for other items Attached to it (e.g. go-aiquota/tray/menubar's control item, whose Hold is what every per-account item's Attach joins — closing or not-Run-ning it would take the whole loop down with it, which hiding does not). Honoured today by the macOS backend (NSStatusItem.visible); other backends ignore it, the same partial-platform-support shape SetTitle already has.
func (*Tray) WithBackend ¶
WithBackend overrides the platform backend and returns the tray.