Files
simpletopbar/AGENTS.md
T
TZGyn 373fda738e Collect other apps' menu bar items into a hamburger panel
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
2026-09-26 23:02:28 +08:00

95 lines
4.9 KiB
Markdown

# 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
```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`.
## 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.