tray

package
v0.1.7 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 1 Imported by: 0

README

tray

A system-tray / menu-bar icon with a menu, cgo-free. Build a Config, hand it to Run, and Run drives the OS event loop until Stop.

import (
	"runtime"

	"github.com/crgimenes/native/tray"
)

func main() {
	runtime.LockOSThread() // Run owns the process's UI event loop

	err := tray.Run(tray.Config{
		Title:   "myapp",
		Tooltip: "myapp is running",
		Items: []tray.Item{
			{Title: "Open", OnClick: openUI},
			{Separator: true},
			{Title: "Quit", OnClick: tray.Stop},
		},
	})
	if err != nil {
		// errors.Is(err, tray.ErrUnsupported) where there is no backend (Linux)
		log.Fatal(err)
	}
}

API

Symbol Description
Run(cfg Config) error Show the tray and drive the OS event loop until Stop. Blocks; call from the main goroutine (locked to the main OS thread). Returns ErrUnsupported / ErrAlreadyRunning.
Stop() Hide the tray and make Run return. Safe from any goroutine; no-op when idle.
Config Title, Tooltip, Icon []byte (PNG), Items []Item.
Item Title, Disabled, Separator, OnClick func().
ErrUnsupported, ErrAlreadyRunning Sentinels.

No native handles cross the boundary.

Threading

Run owns the process's UI event loop, so it must be called from the main goroutine, locked to the main OS thread (runtime.LockOSThread() first thing in main). Item.OnClick runs on that UI thread — keep it short or hand work to another goroutine. Stop is the exception: it is safe to call from anywhere (a menu item's OnClick typically just calls tray.Stop).

Only one tray runs per process; a second Run returns ErrAlreadyRunning.

Platforms

OS Backend Status
macOS NSStatusItem + NSMenu (AppKit via the objc runtime) ✅ runs locally
Windows Shell_NotifyIconW + a hidden window + TrackPopupMenu ✅ builds + CI
Linux ErrUnsupported
Icon support (Config.Icon, a PNG)
OS Behavior
macOS ✅ Renders the PNG, scaled into the menu bar. With no icon it shows Title, or a bullet, so the item is always clickable.
Windows ⚠️ Config.Icon is ignored. The tray shows the application's default icon. Honoring a custom PNG needs a GDI+ PNG→HICON conversion — a known follow-up. The tray is always visible regardless.
Linux n/a — ErrUnsupported (no backend).

So a caller can always pass Config.Icon and it "shows when possible": a real icon on macOS, the default app icon on Windows, nothing on Linux.

Why Linux is unsupported. A Linux tray is a StatusNotifierItem plus a com.canonical.dbusmenu export over D-Bus — a dependency this module avoids — and it is fragmented across desktops (GNOME needs a shell extension to show it at all). Rather than ship something flaky, Linux returns a clear ErrUnsupported.

Effect vs. binding

A unit test can confirm the unsupported path and that Stop is a safe no-op, but not that an icon appeared and a menu item fired — that needs a display and the main thread. examples/tray is the manual vehicle:

go run ./examples/tray

Set TRAY_AUTOCLOSE=1 to have it stop itself after a couple of seconds (a non-interactive smoke test that the icon comes up and the loop tears down).

Conventions

Part of native; follows the shared shape — public API in a tag-free tray.go, per-platform tray_darwin.go / tray_windows.go, and a tray_other.go (!darwin && !windows) that returns ErrUnsupported so every GOOS builds.

Documentation

Overview

Package tray puts an icon with a menu in the system tray / menu bar, cgo-free.

Build a Config, hand it to Run, and Run blocks driving the OS event loop until Stop is called. Each backend binds what the OS already ships — NSStatusItem on macOS, Shell_NotifyIcon on Windows — with no cgo and no bundled libraries.

err := tray.Run(tray.Config{
	Title: "myapp",
	Items: []tray.Item{
		{Title: "Open", OnClick: openUI},
		{Separator: true},
		{Title: "Quit", OnClick: tray.Stop},
	},
})

Threading: Run owns the process's UI event loop, so it must be called from the main goroutine, locked to the main OS thread:

func main() {
	runtime.LockOSThread()
	tray.Run(cfg)
}

A menu item's OnClick runs on that UI thread; keep it short or hand work to another goroutine. Stop, by contrast, is safe to call from any goroutine.

Backends: macOS and Windows are implemented. Linux is not — a StatusNotifierItem tray means a D-Bus dependency this module avoids and it is fragmented across desktops, so Run returns ErrUnsupported there.

Index

Constants

This section is empty.

Variables

View Source
var ErrAlreadyRunning = errors.New("tray: already running")

ErrAlreadyRunning is returned by Run when a tray is already active in this process; only one tray may run at a time.

View Source
var ErrUnsupported = errors.New("tray: not supported on this platform")

ErrUnsupported is returned by Run on a platform with no tray backend.

Functions

func Run

func Run(cfg Config) error

Run shows the tray and drives the OS event loop until Stop is called. It blocks and must be called from the main goroutine (see the package doc on threading). It returns ErrUnsupported on platforms with no backend and ErrAlreadyRunning if a tray is already active.

func Stop

func Stop()

Stop hides the tray and makes Run return. It is safe to call from any goroutine and is a no-op when no tray is running.

Types

type Config

type Config struct {
	// Title is a short text label. macOS shows it in the menu bar (next to the
	// icon, or alone when Icon is empty). Windows ignores it.
	Title string

	// Tooltip is shown on hover.
	Tooltip string

	// Icon is a PNG image for the menu bar / tray. macOS renders it, scaled to
	// fit; with no icon it falls back to Title. Windows ignores this field and
	// shows the application's default icon (honoring a custom PNG there is a
	// follow-up). Linux has no backend. It is always safe to set.
	Icon []byte

	// Items are the menu entries, top to bottom.
	Items []Item
}

Config describes the tray icon and its menu. It is read once by Run.

type Item

type Item struct {
	// Title is the menu text. Ignored when Separator is true.
	Title string

	// Disabled greys the item out and suppresses OnClick.
	Disabled bool

	// Separator makes this a divider line instead of a clickable item; all
	// other fields are ignored.
	Separator bool

	// OnClick is called on the UI thread when the item is chosen.
	OnClick func()
}

Item is one entry in the tray menu.

Jump to

Keyboard shortcuts

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