75 lines
3.6 KiB
Markdown
75 lines
3.6 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. `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/SelfTest.swift` — `runSelfTest()` powers `--self-test`.
|
|
|
|
## 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.
|
|
- Don't add Accessibility/Screen Recording permission code: this app only reads
|
|
the running-app list, which needs no TCC grants.
|
|
- `.gitignore` covers `.build/`, `SimpleTopBar.app/`, `dist/` — build artifacts
|
|
should never be committed.
|