# 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 using a bare `Settings { EmptyView() }` scene, because the app owns its status item manually instead of using `MenuBarExtra`. `AppDelegate.setupStatusItem()` creates the `NSStatusItem`, sets `autosaveName = "SimpleTopBar"`, and shows `TopBarView` inside an `NSPopover`. Handles `--self-test`, `--dump-items`, `--diag`, 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. - Control Center is special: its extras menu bar lists **every module it owns**, including ones not in the bar, which are parked off-screen at `y≈982`. `enumerate()` drops items whose AX position `y > 60` and de-duplicates by title per app, otherwise the tray fills with identical "Control Center" tiles. - Icons: every Control Center module shares the same app icon, so `symbolName(for:)` maps titles (Wi‑Fi, Battery, Now Playing, …) to distinct SF Symbols, falling back to the owning app's icon. Note the non-breaking hyphen in "Wi‑Fi" (`\u{2011}`) is normalised before matching. - 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 ```bash 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 ` — 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 --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. ## Status item placement (why not MenuBarExtra) On a full menu bar, a newly created status item is placed leftmost, which on a notched MacBook is under the notch and therefore invisible. `MenuBarExtra` gives no control over this, so the app creates its own `NSStatusItem` and: - Sets `autosaveName = "SimpleTopBar"`. - On first run, writes `"NSStatusItem Preferred Position SimpleTopBar" = 100` into `UserDefaults.standard`. This private-ish key is what macOS reads to place the item, and 100 lands it at the visible right side. Because it is only written when absent, the user's own ⌘-drag wins and persists via the autosave name. `--diag` runs full startup and writes every status item's AX position to `/tmp/simpletopbar-items.txt`; use it to verify the icon is not under the notch (x should be ~1200, not ~765). ## Hiding other menu bar items (cover) `MenuBarHider` drives `MenuBarCover`, which hides other apps' icons by drawing our own frosted window ("cover") over them instead of moving them off-screen. Because the icons keep their real positions, `press` opens their menus in place with no reveal. Verified with `--diag-cover` (cover windows at level 26) and `--diag-press` (the WireGuard menu opens at a positive x, not off-screen). - `MenuBarCover.shouldHide` covers everything except the Apple keep-list (Wi-Fi, Battery, Clock, Control Center, Spotlight/Search). - `runs(from:)` merges adjacent item frames into contiguous cover windows; `subtracting(_:from:)` carves out the hamburger so it is never covered (`MenuBarItemManager.refresh` passes `avoid:` = our own item's frame). - Frames come from AX (`kAXPositionAttribute` + `kAXSizeAttribute`) with a **top-left origin**; `windowFrame(for:primaryHeight:)` flips Y for NSWindow. - The cover window level is 26 (just above status items at 25, below menus at ~101), so opened menus draw on top of it. - `ignoresMouseEvents = false` makes the covered strip swallow clicks so the hidden icons cannot be clicked through. ### Legacy: off-screen spacer An earlier approach used a zero-width spacer `NSStatusItem` grown to width 10_000 to push icons off the left edge. It is no longer used because it required a visible reveal to open a menu. Findings if it is ever revived: preferred position ~250 kept Wi-Fi while hiding apps, and `debug.spacerPos` overrode it. ## 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` is restored on quit, and because the cover windows belong to the process, a crash removes them automatically. - `.gitignore` covers `.build/`, `SimpleTopBar.app/`, `dist/` — build artifacts should never be committed.