Fixes: - Sandbox: add LaunchAgent temporary-exception entitlement - Mirror URL: fix broken buildMirrorURL (URLComponents with ; separator) - Cleanup: add bounds check to prevent crash on missing source - State safety: pass MirrorConfig instead of mutating global @State - KVC: remove misleading do-catch around setValue:forKey: - Log cap: limit in-memory log to 2000 lines - CLI: fix race with calendar loading - launchCtl: separate stdout/stderr pipes Features: - Cancel button for long-running mirrors - Progress indicator for multi-route runs (Route X of Y) - Target event cache across routes Code quality: - Extract BlockMath, MirrorUtils, EventFilters, MirrorConfig - Add 45 unit tests across 3 test files - Refactor mergeGapMin to computed property - Make log editor read-only Build: - Bump version to 1.4.0 (build 18) - Add LSMinimumSystemVersion 15.5
8.3 KiB
BusyMirror — Agent Reference
This file is written for AI coding agents. It assumes you know nothing about the project.
Project Overview
BusyMirror is a macOS menu-bar utility that mirrors calendar events from a source calendar into one or more target calendars, creating busy-placeholder events so availability stays consistent across accounts and devices.
It is a single-platform macOS app written in Swift 5 and SwiftUI, using EventKit to read and write calendar data. The app runs as a menu-bar-only app (LSUIElement) with no Dock icon.
Key capabilities:
- Manual or route-driven multi-source mirroring
- Privacy modes: hide details (placeholder title) or mark events Private server-side (best-effort)
- DRY-RUN mode to preview changes without writing
- Scheduled headless runs via a self-installed
launchdLaunchAgent - Settings autosave/restore, plus Import/Export JSON
- CLI support for headless/scripted runs
Technology Stack
| Layer | Technology |
|---|---|
| Language | Swift 5.0 |
| UI Framework | SwiftUI + AppKit (menu bar, panels) |
| Calendar API | EventKit (EKEventStore, EKEvent, EKCalendar) |
| Persistence | UserDefaults (JSON-encoded settings), @AppStorage |
| Scheduling | launchd / launchctl (user LaunchAgent) |
| Build System | Xcode project (BusyMirror.xcodeproj) + Makefile |
| Target OS | macOS 15.5+ |
| Signing | Ad-hoc (CODE_SIGN_IDENTITY = "-") — not notarized |
No external Swift Package Manager dependencies are used. The project is self-contained.
Project Structure
BusyMirror/
├── BusyMirrorApp.swift # App entry point; defines Window + MenuBarExtra
├── ContentView.swift # Main UI, mirror engine, settings, CLI, scheduling (≈2600 lines)
├── MenuBarSupport.swift # `BusyMirrorAppController` (state coordinator) + menu bar view
├── Info.plist # LSUIElement, calendar usage descriptions
├── BusyMirror.entitlements # App sandbox + calendar access entitlement
└── Assets.xcassets/ # AppIcon set and accent color
BusyMirror.xcodeproj/ # Xcode project
BusyMirrorTests/ # Empty (no tests implemented)
BusyMirrorUITests/ # Empty (no tests implemented)
Architecture note: nearly all business logic lives inside ContentView.swift. This includes:
- SwiftUI view body and subviews
- EventKit mirror engine (read, deduplicate, merge, create/update/delete)
- Settings serialization/deserialization
- CLI argument parsing
launchdLaunchAgent installation/removal- Logging to file and UI
When making changes, be aware that ContentView.swift is a large monolithic file. Extracting helpers is fine, but keep the existing data flow ( @EnvironmentObject, @AppStorage, @State) intact.
Build and Release Commands
Makefile targets
make build-debug # Debug build via xcodebuild
make build-release # Release build via xcodebuild
make sign-app # Ad-hoc sign the Release app (strip xattr, codesign)
make app # Verify signed app exists
make package # Create BusyMirror-<version>-macOS.zip + .sha256
make clean # Clean derived data
Built products:
- Unsigned release:
build/DerivedData/Build/Products/Release/BusyMirror.app - Signed release:
build/ReleaseSigned/BusyMirror.app
Xcode
- Open
BusyMirror.xcodeproj. - Select BusyMirror scheme → My Mac.
- Product → Build (or Archive for distribution).
Versioning
MARKETING_VERSIONandCURRENT_PROJECT_VERSIONlive inproject.pbxproj.- The Makefile extracts
MARKETING_VERSIONautomatically for ZIP naming. - Update both Debug and Release build configurations when bumping the version.
Code Style Guidelines
- Language: all code, comments, and user-facing strings are in English.
- Concurrency:
@MainActoris required on methods that mutate SwiftUI@Stateor call EventKit on the main thread. The compiler enforces strict concurrency. - Formatting: standard Swift style (4-space indentation). No external linter is configured.
- Logging: use the
log(_:)method insideContentView; it appends to both the on-screen log editor and the persistent file log (~/Library/Logs/BusyMirror/BusyMirror.log). - Error handling: EventKit errors are caught and logged; they must never crash the app. The file logger swallows its own errors silently.
Testing
- There are no implemented tests.
BusyMirrorTests/andBusyMirrorUITests/exist as Xcode targets but contain no source files. - When adding logic, prefer extracting pure functions (e.g., block merging, gap calculation, filter logic) so they can be unit-tested later.
- Manual testing checklist for releases:
- Grant Calendar permission.
- Select a source and target, run DRY-RUN, verify log output.
- Toggle WRITE and run Mirror Now; verify placeholders appear in the target calendar.
- Move a source event and re-run; verify the placeholder updates.
- Test Cleanup Placeholders (dry-run and write).
- Add a route, install a schedule, verify the LaunchAgent plist is created in
~/Library/LaunchAgents/. - Trigger a menu-bar sync and confirm the window opens if not visible.
Security and Privacy Considerations
- Calendar data: the app reads and writes the user’s calendars via EventKit. It must handle permission denial gracefully.
- Sandbox: the app uses the macOS app sandbox (
com.apple.security.app-sandbox) and thecom.apple.security.personal-information.calendarsentitlement. - Signing: releases are ad-hoc signed only (
codesign --sign -). They are not notarized. Gatekeeper may block the app on first launch; users may need to right-click → Open. - Private events: the "Mark Private" feature uses Objective-C runtime tricks (
perform(Selector:), KVCsetValue:forKey:) because EventKit does not expose a public privacy API. This is best-effort and may silently fail on some calendar providers (e.g., some Exchange configurations). - Loop guard: a
sessionGuardset prevents mirroring an event into the same target twice in one run, and prefix-based detection (titlePrefix) prevents re-mirroring already-mirrored placeholders. - Logging: log files are written to the user’s
~/Library/Logs/BusyMirror/. No log data is transmitted externally.
CLI and Scheduling
The binary supports headless execution:
# Run saved routes (used by the LaunchAgent)
BusyMirror.app/Contents/MacOS/BusyMirror --run-saved-routes --write 1 --exit
# Manual route via 1-based UI indices
BusyMirror.app/Contents/MacOS/BusyMirror --routes "1->2,3" --write 1 --exit
Relevant flags: --privacy, --copy-notes, --all-day, --days-forward, --days-back, --merge-gap-hours, --mode, --exclude-titles, --exclude-organizers, --cleanup-only, --exit.
Scheduled runs are implemented by generating a launchd plist in ~/Library/LaunchAgents/com.cqrenet.BusyMirror.saved-routes.plist and bootstrapping it with launchctl. The app removes and re-bootstraps the agent on every "Install Schedule" click.
Key Files to Know
| File | Purpose |
|---|---|
BusyMirror/ContentView.swift |
UI, business logic, mirror engine, settings, CLI, scheduling |
BusyMirror/BusyMirrorApp.swift |
App struct, window scene, menu-bar extra |
BusyMirror/MenuBarSupport.swift |
@MainActor app controller + menu bar SwiftUI view |
BusyMirror/Info.plist |
LSUIElement, calendar usage descriptions |
BusyMirror/BusyMirror.entitlements |
Sandbox + calendar entitlement |
Makefile |
Reproducible build, sign, and package targets |
CHANGELOG.md |
Release notes (human-readable) |
ROADMAP.md |
Planned features |
Notes for Agents
- Do not add third-party dependencies unless the user explicitly asks. The project intentionally has zero external packages.
- If you refactor
ContentView.swift, preserve@AppStoragekeys andUserDefaultskeys exactly; users have existing settings on disk. - The mirror engine is tightly coupled to SwiftUI state. Extract helpers for testability, but do not break the
@MainActor/@Stateflow without careful review. - When modifying build settings, update both Debug and Release configurations in
project.pbxproj, and updateCHANGELOG.mdif the change is user-visible. - Do not run
git commit,git push, or similar operations unless explicitly asked.