diff --git a/AGENTS.md b/AGENTS.md index 94a9b9a..c7ccc2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,9 @@ ## 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. +**BusyMirror** is a macOS utility (standard app + menu bar extra) 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. +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 standard app (Dock icon, ⌘Q) and also has a `MenuBarExtra` for quick sync/status. Key capabilities: - Manual or route-driven multi-source mirroring @@ -44,7 +44,7 @@ BusyMirror/ ├── EventFilters.swift # Work-hours, title, and organizer filters ├── MenuBarSupport.swift # `BusyMirrorAppController` (state coordinator) + menu bar view ├── AppLogStore.swift # File-backed log store with rotation (AppLogStore enum) -├── Info.plist # LSUIElement, calendar usage descriptions +├── Info.plist # calendar/reminders usage descriptions ├── BusyMirror.entitlements # App sandbox + calendar access entitlement └── Assets.xcassets/ # AppIcon set and accent color @@ -146,7 +146,7 @@ Scheduled runs are implemented by generating a `launchd` plist in `~/Library/Lau | `BusyMirror/BusyMirrorApp.swift` | App struct, window scene, menu-bar extra | | `BusyMirror/MenuBarSupport.swift` | `@MainActor` app controller + menu bar SwiftUI view | | `BusyMirror/AppLogStore.swift` | File-backed log with rotation (`~/Library/Logs/BusyMirror/`) | -| `BusyMirror/Info.plist` | `LSUIElement`, calendar usage descriptions | +| `BusyMirror/Info.plist` | calendar/reminders usage descriptions | | `BusyMirror/BusyMirror.entitlements` | Sandbox + calendar entitlement | | `Makefile` | Reproducible build, sign, and package targets | | `CHANGELOG.md` | Release notes (human-readable) | diff --git a/BusyMirror.xcodeproj/project.pbxproj b/BusyMirror.xcodeproj/project.pbxproj index 61a96d3..4688a65 100644 --- a/BusyMirror.xcodeproj/project.pbxproj +++ b/BusyMirror.xcodeproj/project.pbxproj @@ -410,7 +410,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 25; + CURRENT_PROJECT_VERSION = 26; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BusyMirror/Info.plist; @@ -421,7 +421,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.8.1; + MARKETING_VERSION = 1.8.2; PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -440,7 +440,7 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 25; + CURRENT_PROJECT_VERSION = 26; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = BusyMirror/Info.plist; @@ -451,7 +451,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.8.1; + MARKETING_VERSION = 1.8.2; PRODUCT_BUNDLE_IDENTIFIER = com.cqrenet.BusyMirror; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; diff --git a/BusyMirror/BusyMirrorApp.swift b/BusyMirror/BusyMirrorApp.swift index 4534134..9049be1 100644 --- a/BusyMirror/BusyMirrorApp.swift +++ b/BusyMirror/BusyMirrorApp.swift @@ -23,12 +23,12 @@ struct BusyMirrorApp: App { .environmentObject(appController) } - // A plain Window (not a Settings scene) — LSUIElement (accessory) - // apps don't get the standard app menu, so Cmd+, / the automatic - // "Settings…" command has no menu to live in and SettingsLink has - // nothing reliable to trigger. openWindow(id:) is the same mechanism - // that already reliably opens the main window from the menu bar, so - // reuse it here instead. + // A plain Window rather than a Settings scene: this was originally + // required because the app was LSUIElement (accessory) and got no + // standard app menu for Cmd+,/SettingsLink to hook into. Now that + // it's a standard app that menu exists, but openWindow(id:) already + // works reliably (same mechanism as the main window) so there's no + // reason to switch back. Window("Preferences", id: BusyMirrorSceneID.preferencesWindow) { PreferencesView() .environmentObject(appController) diff --git a/BusyMirror/Info.plist b/BusyMirror/Info.plist index 4401fe3..b350ca5 100644 --- a/BusyMirror/Info.plist +++ b/BusyMirror/Info.plist @@ -2,8 +2,6 @@ - LSUIElement - LSMinimumSystemVersion 15.5 NSCalendarsFullAccessUsageDescription diff --git a/CHANGELOG.md b/CHANGELOG.md index b7d6a09..08b7ea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to BusyMirror will be documented in this file. +## [1.8.2] - 2026-08-27 + +### Changed +- **Standard app instead of menu-bar-only.** Removed `LSUIElement` from `Info.plist`: BusyMirror now shows a Dock icon, appears in Cmd+Tab, and gets the standard app menu (Cmd+Q to quit, among others). The menu bar extra stays as a secondary quick-access point. This also fixes the practical problem it was causing: with no Dock icon and no accessible app menu, there was no reliable way to quit the app to let an update replace the bundle — you had to know the menu bar dropdown's "Quit BusyMirror" existed and use exactly that. Verified via `lsappinfo` (`type="Foreground"`, previously `UIElement`) since this app type change isn't something a screenshot would catch either. + ## [1.8.1] - 2026-08-26 ### Fixed diff --git a/README.md b/README.md index 8036a2a..f214087 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ BusyMirror mirrors meetings between your calendars so your availability stays consistent across accounts/devices. -On macOS, BusyMirror now runs as a menu bar app. Use the menu bar icon to sync manually or open the main window; it no longer appears in the Dock. +On macOS, BusyMirror runs as a standard app (Dock icon, ⌘Q to quit) and also has a menu bar icon for quick sync/status without opening the main window. ## What it does (current) - Route-driven mirroring (multi-source): define Source → Targets routes and run them in one go. @@ -13,7 +13,7 @@ On macOS, BusyMirror now runs as a menu bar app. Use the menu bar icon to sync m - DRY-RUN mode: see what would be created/updated/deleted without writing. - Activity Log in the app plus persistent file logging on disk. - In-app scheduling: install or remove a `launchd` LaunchAgent from the `Scheduled runs` section. -- Menu bar controls: trigger `Sync Now`, open the main window, or quit without keeping a Dock icon around. +- Menu bar controls: trigger `Sync Now`, open the main window, open Preferences, or quit. - Overlap modes: `allow`, `skipCovered`, `fillGaps`. - Merge adjacent events with a configurable gap. - Time window controls (days back/forward) and Work Hours filter. diff --git a/ROADMAP.md b/ROADMAP.md index bbacb7a..689cec1 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -14,7 +14,9 @@ - 1.4.0: unit-test suite (45 tests), Cancel button, progress indicator, sandbox LaunchAgent fix, mirror URL fix, engine refactor into `MirrorConfig` - CLI diagnostics: `--help`, `--list-calendars [--json]`, `--status [--json]`, real exit codes (2 = no access, 3 = no saved routes), last-run tracking (time/ok/summary) - 1.7.0: **Event-driven background sync**, replacing the hourly `launchd StartInterval` poll. Auto-sync logic lives in `BusyMirrorAppController` (an app-lifetime object, not tied to `ContentView`'s window lifecycle — closing the main window used to tear down the `EKEventStoreChanged` observer along with it, which would have made auto-sync a no-op whenever the window was closed). Once saved routes exist: registers as a login item (`SMAppService.mainApp`), removes any old `launchd` schedule, watches `EKEventStoreChanged` (debounced ~3s) and `NSWorkspace.didWakeNotification`, plus a 30-min fallback timer as a safety net. Auto-sync always writes (`writeEnabled: true`), independent of the interactive dry-run toggle. -- 1.8.0: **V2 UI polish.** `ContentView.swift` split from ~2000 lines into focused view files — `CalendarsSectionView`, `RoutesSectionView`, `ScheduleSectionView`, `LogSectionView` (state stays owned by `ContentView`/`@AppStorage`; these are view-layer extractions, not a full MVVM rewrite — the settings-persistence model didn't need touching and touching it is exactly how the 1.6.0/1.6.1 data-loss bug happened). Real `Settings { }` scene (⌘,) via `PreferencesView`, hosting the pure `@AppStorage`-backed defaults (time window, privacy/mirroring defaults, work hours, skip filters) that used to live in the main window; this surfaced a latent bug — moving fields out of `ContentView` meant they stopped re-triggering `saveSettingsToDefaults()`, so on next launch `applySnapshot` would have silently reverted a preference changed in the new Settings window using the stale blob. Fixed by splitting launch-time restore (`restoreLaunchState`, routes/selections only, since @AppStorage fields already self-restore) from Import's full restore (`applySnapshot`, unchanged). Menu bar icon now reflects idle/syncing/error state, and the dropdown shows last-sync time/result and whether auto-sync is armed. +- 1.8.0: **V2 UI polish.** `ContentView.swift` split from ~2000 lines into focused view files — `CalendarsSectionView`, `RoutesSectionView`, `ScheduleSectionView`, `LogSectionView` (state stays owned by `ContentView`/`@AppStorage`; these are view-layer extractions, not a full MVVM rewrite — the settings-persistence model didn't need touching and touching it is exactly how the 1.6.0/1.6.1 data-loss bug happened). A preferences window hosting the pure `@AppStorage`-backed defaults (time window, privacy/mirroring defaults, work hours, skip filters) that used to live in the main window; this surfaced a latent bug — moving fields out of `ContentView` meant they stopped re-triggering `saveSettingsToDefaults()`, so on next launch `applySnapshot` would have silently reverted a preference changed in the new window using the stale blob. Fixed by splitting launch-time restore (`restoreLaunchState`, routes/selections only, since @AppStorage fields already self-restore) from Import's full restore (`applySnapshot`, unchanged). Menu bar icon now reflects idle/syncing/error state, and the dropdown shows last-sync time/result and whether auto-sync is armed. +- 1.8.1: the preferences window (originally a `Settings{}` scene) didn't actually open — `LSUIElement` apps get no standard app menu, so Cmd+,/`SettingsLink` had no menu to hook into. Replaced with a plain `Window` opened via `openWindow(id:)`, plus a "Preferences…" menu bar item as a second entry point. +- 1.8.2: **standard app, not menu-bar-only.** Removed `LSUIElement` — Dock icon, Cmd+Tab, standard app menu (Cmd+Q) are back. The accessory-app design (no Dock icon) turned out to make the app hard to quit reliably, which blocked replacing the bundle during updates. Menu bar extra stays as a secondary quick-access point. ## Next 1. **MCP server (thin external wrapper, not embedded in the app).** So agents driving BusyMirror don't have to shell out to the CLI and regex-parse log lines. A small standalone stdio-transport script (Node/Python) maps MCP tools 1:1 onto the CLI's `--json` output: `list_calendars`, `list_routes`, `run_route`, `run_saved_routes`, `get_status`. Deliberately kept out of the Swift app itself — no MCP SDK dependency in the signed binary (AGENTS.md's zero-external-packages rule stays intact), and MCP hosts spawn server processes on demand anyway, so there's no need for the app to run one persistently.