Build a macOS menu‑bar app in Go.
menuet is a small library for programs that live in the macOS menu bar — the NSStatusBar at the top‑right of the screen. You write Go; menuet handles the AppKit bridge.
Introduction
A menuet app is a single Go binary with no main window. You set what shows in the bar, return a slice of menu items for the dropdown, and call one blocking run loop. Everything else — styling, notifications, shortcuts, updates — is opt‑in.
menuet requires macOS. The current module path is github.com/caseymrm/menuet/v2, and the library is MIT licensed, so closed‑source apps are fine as long as you keep the copyright.
Installation
Add the module to your project:
go run is fine while you iterate, but a few features — notifications, start‑at‑login, and auto‑update — only work from inside a proper .app bundle. See Bundling & distribution.
Your first app
Three calls make a working app: App() for the singleton, SetMenuState for the bar title, and RunApplication to start the loop (it never returns).
Call SetMenuState from anywhere, any time — a background goroutine here updates the clock once a second. menuet debounces rapid updates for you.
The status item
MenuState controls what appears in the bar. Set a plain Title, an Image (a file in your Resources directory or a URL, sized to 22pt tall), or Runs for per‑segment styled text — covered next.
Styled titles & text
A TextRun is one styled segment. Concatenate runs in MenuState.Runs (for the bar) or Regular.Runs (for a row) to mix weight, color, monospaced digits, and pill badges. Each style field’s zero value means “inherit,” so a run can change just one attribute.
Colors are either fixed RGBA or semantic — names like LabelSecondary or SystemRed that resolve to dynamic AppKit colors and adapt to light/dark automatically. Runs also support Underline, Strikethrough, Background, and Shadow.
Above: Sportsbar’s actual menu, rendered live from a committed snapshot.
Toggle-style apps
For apps whose primary action is a toggle — mute, pause, force‑awake — set App().Clicked. A left click fires your callback without opening the menu; a right click (or Ctrl‑click) still opens it for secondary actions. Leave it nil for the standard “any click opens the menu” behavior.
Global shortcuts
Give a Regular a Shortcut and it does two things: shows the key combo in the menu (⌘R), and registers a system‑wide hotkey that fires Clicked even when your app isn’t frontmost. Combine modifiers with ModCmd | ModShift; key codes are constants like KeyR.
Search in the menu
A Search item puts a live text field in the menu. Its Results callback fires on every keystroke and returns the items to show below it. Enter activates the top result; the last query is remembered across opens.
Note: apps using Search can’t ship on the Mac App Store — it relies on a private selector. Direct‑distribution apps are unaffected.
Notifications
Show a native banner with App().Notification. Add an action button, an inline reply, or a stable Identifier (re‑using one updates the existing notification rather than stacking). Respond to button taps via App().NotificationResponder. Notifications require a signed app bundle.
Alerts
App().Alert shows a modal dialog and blocks until the user responds, returning which button they pressed and any input values. Add text or password Inputs to collect a value inline.
Persisting state
Defaults() wraps NSUserDefaults for small bits of persistent state — strings, integers, booleans, or any JSON‑able value via Marshal / Unmarshal.
Auto-update
Set AutoUpdate.Version and AutoUpdate.Repo and menuet checks GitHub releases once a day. If a newer release exists it offers an update, downloads the .zip asset, swaps the bundle in place, and restarts. Set AllowPrerelease to opt into prereleases.
Start at login
menuet adds a “Start at Login” item to the menu automatically once you set an App().Label. On macOS 13+ it registers via the Service Management framework, so users can revoke it from System Settings → Login Items; older systems fall back to a LaunchAgent. Hide the item with HideStartup(), or relabel it (and Quit) with StartAtLoginLabel / QuitLabel.
Bundling & distribution
Notifications, start‑at‑login, and auto‑update all need a real .app bundle — these are macOS requirements, not menuet’s. The shared menuet.mk Makefile assembles a minimal one. From your app directory:
make run builds and launches the bundle. To sign for notifications and distribution, set IDENTITY to a Developer ID certificate and run make sign — ad‑hoc signing isn’t enough.
Web preview
menuet ships with a snapshot mode that captures your menu as JSON so it can be rendered as HTML — for screenshots, README mockups, or this site’s apps directory. Run:
Set MENUET_SNAPSHOT_PATH=file.json and your app writes a snapshot at startup and exits cleanly instead of entering the AppKit run loop. MENUET_SNAPSHOT_DELAY (default 2s) gives data-fetching goroutines time to populate state first. The shared menuet.mk has a web-preview target already wired up.
The pure-Go github.com/caseymrm/menuet/v2/html package renders the snapshot to a safe HTML fragment — no code execution, every value validated against a whitelist. It’s what powers every menu mockup you see on this site.
That’s the whole library.
For the complete type‑by‑type reference, see the GoDoc. Or see it all working in a real app.