diff --git a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift index 9e7af3c..74d446d 100644 --- a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift +++ b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift @@ -48,6 +48,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { observeStore() startRefreshLoop() setupWakeObservers() + setupDistributedNotificationListener() + installLaunchAgentIfNeeded() Task { await updateChecker.checkIfNeeded() } } @@ -69,6 +71,68 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { } } + private func setupDistributedNotificationListener() { + DistributedNotificationCenter.default().addObserver( + forName: NSNotification.Name("com.codeburn.refresh"), + object: nil, + queue: .main + ) { [weak self] _ in + Task { @MainActor in self?.forceRefresh() } + } + } + + private func installLaunchAgentIfNeeded() { + let fm = FileManager.default + let agentName = "com.codeburn.refresh.plist" + let home = fm.homeDirectoryForCurrentUser.path + let destPath = "\(home)/Library/LaunchAgents/\(agentName)" + + let plist = """ + + + + + Label + com.codeburn.refresh + ProgramArguments + + /usr/bin/osascript + -l + JavaScript + -e + ObjC.import("Foundation"); $.NSDistributedNotificationCenter.defaultCenter.postNotificationNameObjectUserInfoDeliverImmediately("com.codeburn.refresh", $(), $(), true) + + StartInterval + 15 + RunAtLoad + + + +""" + + do { + let existing = try? String(contentsOfFile: destPath, encoding: .utf8) + if existing == plist { return } + + try fm.createDirectory(atPath: "\(home)/Library/LaunchAgents", withIntermediateDirectories: true) + try plist.write(toFile: destPath, atomically: true, encoding: .utf8) + + let unload = Process() + unload.launchPath = "/bin/launchctl" + unload.arguments = ["unload", destPath] + try? unload.run() + unload.waitUntilExit() + + let load = Process() + load.launchPath = "/bin/launchctl" + load.arguments = ["load", destPath] + try load.run() + load.waitUntilExit() + } catch { + NSLog("CodeBurn: LaunchAgent setup failed: \(error)") + } + } + private func forceRefresh() { Task { await store.refreshQuietly(period: .today) diff --git a/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift b/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift index 3b31e76..5e9190c 100644 --- a/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift +++ b/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift @@ -225,7 +225,7 @@ private func computeHistoryStats(history: [DailyHistoryEntry]) -> HistoryStats { }() let now = Date() let today = calendar.startOfDay(for: now) - let costByDate = Dictionary(uniqueKeysWithValues: history.map { ($0.date, $0.cost) }) + let costByDate = Dictionary(history.map { ($0.date, $0.cost) }, uniquingKeysWith: +) let lastWeekStart = calendar.date(byAdding: .day, value: -6, to: today) let priorWeekStart = calendar.date(byAdding: .day, value: -13, to: today) diff --git a/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift b/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift index 16fbb25..5b143b2 100644 --- a/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift +++ b/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift @@ -399,7 +399,7 @@ private func buildTrendBars(from days: [DailyHistoryEntry]) -> [TrendBar] { f.timeZone = .current return f }() - let entryByDate = Dictionary(uniqueKeysWithValues: days.map { ($0.date, $0) }) + let entryByDate = Dictionary(days.map { ($0.date, $0) }, uniquingKeysWith: { _, new in new }) let today = calendar.startOfDay(for: Date()) let todayKey = formatter.string(from: today) @@ -837,7 +837,7 @@ private func computeAllStats(payload: MenubarPayload) -> AllStats { peakDaySpend = "—" } - let costByDate = Dictionary(uniqueKeysWithValues: history.map { ($0.date, $0.cost) }) + let costByDate = Dictionary(history.map { ($0.date, $0.cost) }, uniquingKeysWith: +) var currentStreak = 0 for offset in 0..<400 {