Documentation
¶
Overview ¶
Package statusitem puts an item in the macOS menu bar — the thing everyone calls a "tray icon" — from pure Go, with CGO_ENABLED=0. It reaches AppKit through github.com/go-macos/objc, which reaches it through purego: no cgo, no osascript, no Objective-C source file anywhere in the build.
A MenuItem carries a title, an optional key equivalent and a Go func. That func is called when the row is chosen.
item, err := statusitem.New("⌘", []statusitem.MenuItem{
{Title: "Preferences…", Key: ",", Do: openPreferences},
{}, // a separator
{Title: "Quit", Key: "q", Do: quit},
})
if err != nil {
return err
}
defer item.Close()
It needs a host application, and it says so ¶
This package does NOT create an application. It expects to be called from a process that already has an NSApplication whose run loop is running on the process main OS thread — in this fleet that is go-widgets/window, in a plain binary it is objc.RunApp. Everything about a status item depends on that loop:
- +[NSStatusBar systemStatusBar] and -statusItemWithLength: succeed without it, so an item can be BUILT before the loop starts. New is therefore safe to call during start-up.
- Nothing is ever DRAWN and no row is ever chosen without it. A status item in a process whose main thread never runs a loop is an object with no window, and it is indistinguishable — from Go — from one that works.
If there is no NSApplication at all, New creates the shared one (+sharedApplication) as a side effect, because -systemStatusBar is meaningless before AppKit has an application object. It does not call -finishLaunching and it does not touch the activation policy: a host owns those, and changing them under a host would move its Dock tile or its main menu.
The main thread ¶
AppKit is main-thread-only, and violating that does not fail politely: the observed failure mode in this fleet's own tray code was an Objective-C exception and a SIGABRT, intermittently, depending on which OS thread the Go scheduler happened to be running the goroutine on.
So every AppKit call this package makes is marshalled onto the process main thread, and every exported function is safe to call from any goroutine. -[NSThread isMainThread] decides how: on the main thread the work runs inline (so New works before the loop is started), and off it the work is queued with -performSelectorOnMainThread:withObject:waitUntilDone:NO and waited for with a timeout. The timeout is not decoration. waitUntilDone:YES from a goroutine in a process whose main thread is not running a loop blocks that goroutine FOREVER, with no error and no stack that mentions this package; after MainHopTimeout the call reports ErrNoMainLoop instead.
Handlers do not run on the main thread ¶
MenuItem.Do is called on a fresh goroutine, not on the main thread that delivered the click. A handler that blocks — a network fetch, a lock, a channel nobody is reading — would otherwise freeze the menu bar of the whole session, not merely this application. The cost is that two rapid choices can run concurrently and that a handler touching AppKit must marshal itself back; that is the cheaper of the two mistakes.
Portability ¶
Every exported symbol is defined on all platforms, so a consumer cross-compiles without a build tag of its own; off darwin the entry points report ErrUnsupported. What is NOT stubbed out is the menu model — item validation, separator and disabled-row classification, tag assignment and tag dispatch all live in the portable file and behave identically everywhere, which is what lets them be tested to the last branch on a Linux runner with no window server in sight.
Index ¶
Constants ¶
const MainHopTimeout = 5 * time.Second
MainHopTimeout is how long an exported call made from a goroutine other than the main one waits for the process main thread to service its AppKit work before reporting ErrNoMainLoop.
It is generous because it is not a performance budget: a main thread that is running a loop at all services the request in microseconds, and one that is not will never service it. The only case in between is a main thread busy inside a handler of its own, and five seconds of that is already a bug elsewhere.
Variables ¶
var ( // ErrUnsupported is returned by every entry point on non-darwin platforms // (a menu bar of this shape is AppKit's, and AppKit is macOS-only). ErrUnsupported = errors.New("statusitem: unsupported on this platform (darwin only)") // ErrClosed reports use of an [Item] that has already been removed from // the menu bar. ErrClosed = errors.New("statusitem: status item already removed") // ErrEmptyTitle reports an empty status-item title. AppKit accepts one and // draws a zero-width item: present in the menu bar, impossible to see and // impossible to click. There is no image parameter here to make an empty // title mean something, so it is refused rather than shipped as a mystery. ErrEmptyTitle = errors.New("statusitem: an empty title would be an invisible status item") // ErrHasNUL reports a title or key equivalent containing a NUL byte. The // Objective-C string bridge is +stringWithUTF8String:, which terminates at // the first NUL, so such a string would be silently TRUNCATED rather than // rejected — the caller would see a shorter menu row and no error at all. ErrHasNUL = errors.New("statusitem: title or key equivalent contains a NUL byte") // ErrSeparatorNotEmpty reports a row with an empty Title that also carries // a Do or a Key. An empty title is how this package spells "separator", and // a separator cannot be chosen, so the Do could never run. Accepting it // would mean silently dropping a handler the caller wrote on purpose. ErrSeparatorNotEmpty = errors.New("statusitem: a row with an empty title is a separator and can carry neither Do nor Key") // ErrKeyNotOneRune reports a key equivalent that is not exactly one // character. -[NSMenuItem setKeyEquivalent:] accepts any string, shows it // in the row, and then never matches it against a keystroke: the shortcut // is drawn and dead. ErrKeyNotOneRune = errors.New("statusitem: a key equivalent must be exactly one character") // ErrNoMainLoop reports that the process main thread did not service an // AppKit request within [MainHopTimeout]. It means no run loop is running // there — the caller is a goroutine in a process that has not started one, // or has stopped it. Without this the goroutine would block forever. ErrNoMainLoop = errors.New("statusitem: the main thread did not service the request (no run loop is running there)") // ErrNoTargetClass reports that the runtime class carrying the menu action // could not be created. Every menu row needs it as its target, so there is // nothing to hand back: an item whose rows have a nil target draws perfectly // and answers no click. ErrNoTargetClass = errors.New("statusitem: the Objective-C action target class could not be created") // ErrNoApplication reports that +[NSApplication sharedApplication] yielded // nil. AppKit has no application object, so there is no menu bar to join. ErrNoApplication = errors.New("statusitem: +[NSApplication sharedApplication] returned nil") // ErrNoButton reports that the status item has no -button. The item exists // but has nothing to draw a title in, which is the shape AppKit takes when // the process may not draw in the menu bar at all. ErrNoButton = errors.New("statusitem: the status item has no button to put a title in") // ErrNoSymbol means the name is not a symbol this system has, or is not a // name at all. The item keeps whatever it was showing: one that quietly // became blank is one nobody can find. ErrNoSymbol = errors.New("statusitem: no such system symbol") // ErrNoStatusBar reports that +[NSStatusBar systemStatusBar] returned nil. // There is no menu bar to put an item in: no window server, or a session // that has none. ErrNoStatusBar = errors.New("statusitem: +[NSStatusBar systemStatusBar] returned nil (no menu bar in this session)") )
Errors reported by the package. They are stable and may be tested with errors.Is.
Functions ¶
This section is empty.
Types ¶
type Item ¶
type Item struct {
// contains filtered or unexported fields
}
Item is a status item in the menu bar. On non-darwin platforms one can never be created, so no value of this type is ever handed out by New; the type exists so that consumer code naming it still compiles.
func New ¶
New reports ErrUnsupported: the menu bar this package fills is AppKit's.
It validates its arguments FIRST, so that a developer working on Linux gets exactly the complaint macOS would make about a malformed menu — an empty title, a separator carrying a handler, a two-character shortcut — instead of a blanket ErrUnsupported that hides the defect until the code is built on a Mac.
func (*Item) Close ¶
Close reports ErrUnsupported. There is nothing in a menu bar to remove.
func (*Item) OnScreen ¶ added in v0.2.0
OnScreen reports ErrUnsupported: there is no menu bar here.
func (*Item) SetMenu ¶
SetMenu reports ErrUnsupported. The rows are still validated first, for the same reason New validates.
func (*Item) SetSymbol ¶ added in v0.3.0
SetSymbol reports ErrUnsupported: there is no menu bar here to draw in.
type MenuItem ¶
type MenuItem struct {
// Title is the text of the row. An empty Title makes the row a separator,
// in which case Key and Do must both be zero.
Title string
// Key is an optional key equivalent: exactly one character, taken with
// Command. "," gives ⌘, — the conventional Preferences shortcut.
Key string
// Do is called when the row is chosen. A nil Do makes the row disabled.
Do func()
}
MenuItem is one row of a status item's menu.
The zero value is a separator. A row with a Title and no Do is a disabled row — a heading, or a value shown for information. A row with a Title and a Do is chooseable, and the Do is called on a fresh goroutine (see the package documentation for why not on the main thread).
func (MenuItem) IsSeparator ¶
IsSeparator reports whether the row is a separator, which is exactly the case where Title is empty. It is the rule the rest of the package applies, exported so that a caller building rows programmatically can apply the same one instead of guessing at it.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
statusitemdemo
command
Command statusitemdemo puts a status item in the macOS menu bar and waits for it to be used.
|
Command statusitemdemo puts a status item in the macOS menu bar and waits for it to be used. |