diff --git a/README.md b/README.md index 4773f7b..b8fe63e 100644 --- a/README.md +++ b/README.md @@ -115,9 +115,10 @@ trust the three BarKeep commands. Codex and ChatGPT desktop share these hooks. | Feature | Permission | Why | |---|---|---| +| Busy Bar connection | Local Network | Required for the bar's local HTTP API over both its USB network interface and Wi-Fi. BarKeep asks on first launch. | | Notification forwarding | Full Disk Access | macOS stores delivered notifications in a TCC-protected SQLite DB (`~/Library/Group Containers/group.com.apple.usernoted/db2/db`). BarKeep polls it read-only; only titles/bodies matching your app filter are read, and they go straight to the bar over your local USB or Wi-Fi connection. | | Calendar auto-busy | Calendar (full access) | To know when you're in an event. | -| Everything else | none | Mic detection reads CoreAudio device state, not audio. | +| Microphone detection | None | Mic detection reads CoreAudio device state, not audio. | Grant Full Disk Access in System Settings → Privacy & Security → Full Disk Access → add the installed `BarKeep.app`. Official release builds use a stable diff --git a/Sources/BarKeep/AppState.swift b/Sources/BarKeep/AppState.swift index 50238ac..11edaf6 100644 --- a/Sources/BarKeep/AppState.swift +++ b/Sources/BarKeep/AppState.swift @@ -151,6 +151,7 @@ final class AppState { var calendarAccessGranted: Bool { calendarMonitor.accessGranted } let client: BusyBarClient + private let localNetworkPermissionTrigger = LocalNetworkPermissionTrigger() private let micMonitor = MicMonitor() private let notificationWatcher = NotificationWatcher() private let calendarMonitor = CalendarMonitor() @@ -205,6 +206,12 @@ final class AppState { self.pingHost = defaults.string(forKey: "pingHost") ?? "1.1.1.1" self.weatherCelsius = defaults.bool(forKey: "weatherCelsius") self.client = BusyBarClient(host: host, token: token) + localNetworkPermissionTrigger.onAccessAvailable = { [weak self] in + Task { @MainActor [weak self] in + await self?.refreshDeviceStatus() + } + } + localNetworkPermissionTrigger.requestAccess() slackSync.token = self.slackToken syncPingLoop() syncWeatherLoop() diff --git a/Sources/BarKeep/LocalNetworkPermissionTrigger.swift b/Sources/BarKeep/LocalNetworkPermissionTrigger.swift new file mode 100644 index 0000000..bdfbb6d --- /dev/null +++ b/Sources/BarKeep/LocalNetworkPermissionTrigger.swift @@ -0,0 +1,40 @@ +import Foundation +import Network + +/// Performs an explicit local-network operation so macOS presents its privacy +/// prompt before the Busy Bar client begins making direct-IP requests. +final class LocalNetworkPermissionTrigger: @unchecked Sendable { + private let queue = DispatchQueue(label: "dev.barkeep.local-network-permission") + private var browser: NWBrowser? + var onAccessAvailable: (@Sendable () -> Void)? + + func requestAccess() { + guard browser == nil else { return } + + let parameters = NWParameters() + parameters.includePeerToPeer = true + + let browser = NWBrowser( + for: .bonjour(type: "_barkeep._tcp", domain: nil), + using: parameters + ) + browser.stateUpdateHandler = { [weak self] state in + switch state { + case .ready: + self?.onAccessAvailable?() + self?.stop() + case .failed, .cancelled: + self?.stop() + default: + break + } + } + browser.start(queue: queue) + self.browser = browser + } + + private func stop() { + browser?.cancel() + browser = nil + } +} diff --git a/packaging/Info.plist b/packaging/Info.plist index b1eaf24..5f75af8 100644 --- a/packaging/Info.plist +++ b/packaging/Info.plist @@ -13,12 +13,18 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.0.8 + 1.0.9 LSMinimumSystemVersion 14.0 LSUIElement NSCalendarsFullAccessUsageDescription BarKeep sets your Busy Bar to busy during calendar events and shows a countdown to your next meeting. + NSLocalNetworkUsageDescription + BarKeep connects directly to your Busy Bar over USB or Wi-Fi on your local network. + NSBonjourServices + + _barkeep._tcp +