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
109 lines
5.7 KiB
Markdown
109 lines
5.7 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`.
|
|
|
|
## 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.
|