codeburn/mac/Sources/CodeBurnMenubar/Data/MenubarStatusCache.swift
iamtoruk 515a7a1467 feat(menubar): drop launchd fetcher; GUI writes its own badge backstop
A launchd-spawned process gets separate TCC attribution from the LaunchServices
app, so the out-of-process refresher prompted for "access data from other apps"
on every run regardless of the GUI's grant. Remove it entirely: the in-app loop
(timer survives sleep, popover-open recovery) is the source of truth and now
writes menubar-status.json on each successful refresh. On upgrade, any leftover
com.codeburn.refresh LaunchAgent is unloaded and deleted.
2026-05-30 13:35:07 -07:00

43 lines
1.8 KiB
Swift

import Foundation
/// On-disk badge backstop. The app writes `menubar-status.json` on each successful
/// refresh and reads it back as a badge fallback after a restart, before the live
/// loop has produced a payload. Shares the `MenubarPayload` model with the live
/// path no separate data model.
struct MenubarStatusCache {
let statusPath: String
/// Default location under `~/.cache/codeburn/`.
static func standard() -> MenubarStatusCache {
let home = FileManager.default.homeDirectoryForCurrentUser.path
return MenubarStatusCache(statusPath: "\(home)/.cache/codeburn/menubar-status.json")
}
struct BadgeRead {
let payload: MenubarPayload
let ageSeconds: TimeInterval
}
/// Decodes the status file and returns it with its age (from the file's
/// mtime). Returns nil when the file is missing, corrupt, unreadable, or
/// older than `maxAgeSeconds` every failure mode silently falls back to
/// the in-memory payload, never crashes.
func readBadgePayload(maxAgeSeconds: TimeInterval) -> BadgeRead? {
guard let attrs = try? FileManager.default.attributesOfItem(atPath: statusPath),
let mtime = attrs[.modificationDate] as? Date else {
return nil
}
let age = Date().timeIntervalSince(mtime)
guard age >= 0, age <= maxAgeSeconds else { return nil }
guard let data = try? SafeFile.read(from: statusPath),
let payload = try? JSONDecoder().decode(MenubarPayload.self, from: data) else {
return nil
}
return BadgeRead(payload: payload, ageSeconds: age)
}
func writeStatus(_ payload: MenubarPayload) throws {
let data = try JSONEncoder().encode(payload)
try SafeFile.write(data, to: statusPath, mode: 0o600)
}
}