Files
simpletopbar/AGENTS.md
T
TZGyn 9b5031aff2 Document the upgrade steps and always print them after a release
Readers and future sessions need one obvious place that says how to move
an installed app to the latest version, and the release flow now requires
surfacing those commands in every release report.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-26 23:05:12 +08:00

5.7 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 — @main entry. MenuBarExtra with .menuBarExtraStyle(.window) hosting TopBarView. AppDelegate handles the --self-test flag and the single-instance guard. LoginItem wraps SMAppService.
  • Sources/SimpleTopBar/AppListModel.swift — @MainActor ObservableObject singleton. refresh() reads NSWorkspace.shared.runningApplications and splits them by activationPolicy into regularApps and backgroundApps (both sorted by name). Subscribes to the NSWorkspace launch/terminate/ activate/hide/unhide notifications to refresh automatically.
  • Sources/SimpleTopBar/TopBarView.swift — the panel. menuBarSection is the hamburger tray of other apps' menu bar items; section(...) renders the regular apps; backgroundSection is the collapsible dropdown for background apps. AppRow shows icon + name + active dot with a right-click Activate/Hide/Quit menu.
  • Sources/SimpleTopBar/MenuBarItems.swift — MenuBarItemManager enumerates other apps' status items via the Accessibility API (kAXExtrasMenuBarAttribute → kAXChildrenAttribute) and presses them (kAXPressAction, falling back to kAXShowMenuAction). MenuBarHider owns the zero-width spacer NSStatusItem used to push items off the menu bar. AccessibilityPermission wraps AXIsProcessTrusted / 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 when AXIsProcessTrusted() is false, and TopBarView shows a "Grant Accessibility…" button in that state. Never assume permission.
  • Enumeration must be resilient: AXUIElementCopyAttributeValue can fail per-app or per-element, so every helper returns nil instead of force-unwrapping. The enumerate() routine is nonisolated static so --self-test can run it.
  • Items are keyed by pid-index; MenuBarItem equality 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.
  • AppListModel drives all of this: backgroundExpanded, backgroundVisibleCount, visibleBackgroundApps, hiddenBackgroundCount, canShowMoreBackgroundApps, toggleBackgroundExpanded(), showMoreBackgroundApps().
  • The pure helpers clampedVisibleCount and nextVisibleCount are the overflow math and are covered by --self-test. Keep them pure and nonisolated (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.

Release process

  1. ./release.sh <version> — builds, zips SimpleTopBar.app to dist/SimpleTopBar.zip, and prints its sha256 (it does not tag or upload).
  2. Commit and push main.
  3. tea releases create --tag v<version> --asset dist/SimpleTopBar.zip --repo TZGyn/simpletopbar --login gitea.tzgyn.dev.
  4. Bump version/sha256 in ~/.cache/homebrew-tap/Casks/simpletopbar.rb, push, then git -C /opt/homebrew/Library/Taps/tzgyn/homebrew-tap pull --ff-only.

Always print the upgrade script after any release or version bump — the user wants it in the final answer every time. Keep it to the cask upgrade commands (see the README release section for the exact block), because the app is installed through the tap.

Gotchas

  • AppListModel is @MainActor; the NSWorkspace observers hop back with Task { @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 activationPolicy is reliable but must happen on the main thread; keep all NSWorkspace access inside the model.
  • The menu bar tray needs Accessibility permission; do not add Screen Recording or Apple Events usage. MenuBarHider must always be restored on quit, but because a status item dies with its process, a crash cleans up on its own.
  • .gitignore covers .build/, SimpleTopBar.app/, dist/ — build artifacts should never be committed.