Modules
Module: a menubar (agent) app
The template ships a window app: a Dock icon, one window, quits when the window closes. Two of the three source projects were agent apps instead — no Dock icon, living in the menu bar or at the edge of the screen — and every rule below is something one of them learned the expensive way. Switch when your product is something that stays out of the way all day.
Everything else in the template — the licence stack, the updater, the main menu, Settings, the release pipeline — carries over unchanged.
1. Become an agent, twice
mac/App/Resources/Info.plist: addLSUIElement=true. It governs the process from the instant of launch, so there is no Dock icon flash.AppDelegate.applicationDidFinishLaunching: callNSApp.setActivationPolicy(.accessory)as well, and return false fromapplicationShouldTerminateAfterLastWindowClosed— closing Settings must not quit the app.
Both, not one: the plist key alone flashes nothing at launch but leaves the policy to drift later; the runtime call alone flashes a Dock icon at launch.
2. The activation dance, for every window with a text field
An .accessory app cannot make a window key, so a text field in it cannot be
typed into. When a real window opens (the licence gate, Settings):
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
and when the last such window closes, go back to .accessory. Becoming
.regular earns a Dock icon and a slot in the menu bar; it does not
populate a menu — the main menu from MainMenu.swift still has to be installed
at launch, or ⌘V is dead in the licence field. Three comments in a source
project claimed .regular was what "makes ⌘V work"; every part of that looked
done.
Also activate before anything that puts a window on screen by itself —
UpdateController.checkForUpdates() already does, because Sparkle's window
otherwise opens behind whatever the user is looking at.
3. Quit needs a home
With no menu bar of your own, ⌘Q only reaches the app while one of its windows is active. Keep Quit in Settings → General (the template already has it) and in the status item's menu. On the licence gate, Quit must be reachable before a key is entered — somebody who cannot find their key must not be left with Force Quit, thirty seconds after paying.
4. The status item, or the panel
MenuBarExtra(.window)in SwiftUI is the quick route for a popover-style panel from the menu bar. It needs a SwiftUIApp, which conflicts with the template's AppKit shell (main.swiftexplains why the delegate must exist before the run loop); anNSStatusItemowned byAppDelegatekeeps that.- A floating
NSPanel(a widget at the screen edge) has rules nothing but the window server can confirm:- assign
levelafterisFloatingPanel: the latter's setter resets the level, silently, and the panel loses stacking arguments to other apps' HUDs weeks later on somebody else's Mac; - set
NSHostingView.sizingOptions = [], or SwiftUI resizes the window to its intrinsic size and becomes a second authority on its frame; .nonactivatingPanelwithcanBecomeKeyoverridden totruewhen it has a text field;- dock it to
NSScreen.screens.first, recomputed on every change, so it cannot be stranded on an unplugged display.
- assign
- Verify those properties by reading the window back
(
CGWindowListCopyWindowInfo), and never while the screen is locked: a locked display reports window bounds at roughly 0.9×, so every check fails naming numbers that match nothing in the source.
5. The licence in an app that never quits
LicenseController.revalidateIfDue() runs once per launch in the window app,
which is a check every few days in practice. An agent app may run for weeks, so
check on a timer as well — hourly is plenty; it only talks to the server once
revalidation is due. Keep the product's promise about when the answer lands:
never yank the product out from under somebody mid-task. Apply a lapse at the
next natural boundary — the next time a window opens, or at next launch.
6. Screen sharing and presence
If the app ever draws over other apps (a celebration, an overlay), set
sharingType = .none on that window — it is the only reliable way to keep it
out of a screen share, because macOS exposes no API for "my screen is being
shared". The window then cannot be screenshotted by anyone, the user included;
that is the flag working. Do not read ~/Library/DoNotDisturb/ to detect
Focus — it needs Full Disk Access, which is a lot to ask for a sound effect.
7. The site
- The FAQ and privacy page say the app "runs only when you open it" in spirit; an agent app runs at login. Say so, and say it does nothing over the network besides the licence check and updates.
- Screens rendered for the landing page (
docs/modules/marketing-shots.md) must show the status item or panel, not a window the product does not have.
Verify
mac/scripts/verify-menu.shstill passes — the main menu is installed even though no menu bar is drawn while.accessory.- Paste a licence key with ⌘V into the gate, with the app in the background beforehand.
- Close Settings: the app keeps running. Quit from Settings: it quits.
This page is docs/modules/menubar.md in the repository, copied 2026-09-25.