The menu bar icon is now a hamburger whose panel gathers every other
app's status item via the Accessibility API, so tray icons no longer
need to be hunted down across the menu bar. Items can be opened from
the panel, and a best-effort toggle pushes them off the real menu bar
so they only live there.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
4.9 KiB
4.9 KiB
AGENTS.md
SimpleTopBar is a native macOS menu-bar app written in Swift/SwiftUI. Single executable SwiftPM package, no external dependencies (all Apple frameworks).
Architecture
Sources/SimpleTopBar/App.swift—@mainentry.MenuBarExtrawith.menuBarExtraStyle(.window)hostingTopBarView.AppDelegatehandles the--self-testflag and the single-instance guard.LoginItemwrapsSMAppService.Sources/SimpleTopBar/AppListModel.swift—@MainActorObservableObjectsingleton.refresh()readsNSWorkspace.shared.runningApplicationsand splits them byactivationPolicyintoregularAppsandbackgroundApps(both sorted by name). Subscribes to theNSWorkspacelaunch/terminate/ activate/hide/unhide notifications to refresh automatically.Sources/SimpleTopBar/TopBarView.swift— the panel.menuBarSectionis the hamburger tray of other apps' menu bar items;section(...)renders the regular apps;backgroundSectionis the collapsible dropdown for background apps.AppRowshows icon + name + active dot with a right-click Activate/Hide/Quit menu.Sources/SimpleTopBar/MenuBarItems.swift—MenuBarItemManagerenumerates other apps' status items via the Accessibility API (kAXExtrasMenuBarAttribute→kAXChildrenAttribute) and presses them (kAXPressAction, falling back tokAXShowMenuAction).MenuBarHiderowns the zero-width spacerNSStatusItemused to push items off the menu bar.AccessibilityPermissionwrapsAXIsProcessTrusted/AXIsProcessTrustedWithOptions.Sources/SimpleTopBar/SelfTest.swift—runSelfTest()powers--self-test.
The menu bar tray (Accessibility)
- Requires Accessibility permission.
MenuBarItemManager.refresh()short-circuits to an empty list whenAXIsProcessTrusted()is false, andTopBarViewshows a "Grant Accessibility…" button in that state. Never assume permission. - Enumeration must be resilient:
AXUIElementCopyAttributeValuecan fail per-app or per-element, so every helper returnsnilinstead of force-unwrapping. Theenumerate()routine isnonisolated staticso--self-testcan run it. - Items are keyed by
pid-index;MenuBarItemequality ignores the AX element. - Reading another app's UI is the only TCC permission needed; there is no Screen Recording or Apple Events usage.
The background dropdown
This is the core feature. Requirements:
- The background-app section is a dropdown, not a flat list. Collapsed, it is one header row showing the count.
- Expanded, it shows at most
AppListModel.backgroundPageSize(5) apps. Any overflow is hidden behind a "Show N more" row, so the panel never grows unbounded. Collapsing resets the page. AppListModeldrives all of this:backgroundExpanded,backgroundVisibleCount,visibleBackgroundApps,hiddenBackgroundCount,canShowMoreBackgroundApps,toggleBackgroundExpanded(),showMoreBackgroundApps().- The pure helpers
clampedVisibleCountandnextVisibleCountare the overflow math and are covered by--self-test. Keep them pure andnonisolated(no AppKit, no actor state).
Commands
swift build # debug build
swift run SimpleTopBar # dev run (menu bar icon appears)
swift run SimpleTopBar --self-test # headless overflow + classification check, exit 0/1
./build-app.sh 0.1.0 # release build + .app bundle + icon + codesign
./release.sh 0.1.0 # build-app.sh + dist/SimpleTopBar.zip + sha256
scripts/make-icon.sh # regenerate SimpleTopBar.app/Contents/Resources/Icon.icns
There is no test target and no linter/formatter configured. Verification = build
--self-test.
Bundle build & signing
build-app.sh does: release build → handwritten Info.plist (NOT generated
from Package.swift) → make-icon.sh → codesign. It resolves a
Developer ID Application identity from the login keychain and falls back to
ad-hoc signing (-) if none is present. LSUIElement is true → no Dock icon.
Bump CFBundleShortVersionString/CFBundleVersion via the build-app.sh
version argument and pass the same version to release.sh.
Gotchas
AppListModelis@MainActor; theNSWorkspaceobservers hop back withTask { @MainActor in ... }because the notification closure is nonisolated.- The app excludes itself from the list via
Bundle.main.bundleIdentifier, and the single-instance guard terminates stale copies with the same bundle id. - Reading
activationPolicyis reliable but must happen on the main thread; keep allNSWorkspaceaccess inside the model. - The menu bar tray needs Accessibility permission; do not add Screen Recording
or Apple Events usage.
MenuBarHidermust always be restored on quit, but because a status item dies with its process, a crash cleans up on its own. .gitignorecovers.build/,SimpleTopBar.app/,dist/— build artifacts should never be committed.