Files
steipete--codexbar/Sources/CodexBarWidget/BurnDownWidgetProvider.swift
T
2026-07-13 12:22:33 +08:00

229 lines
7.3 KiB
Swift

import AppIntents
import CodexBarCore
import WidgetKit
enum BurnProviderChoice: String, AppEnum {
case codex
case claude
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Provider")
static let caseDisplayRepresentations: [BurnProviderChoice: DisplayRepresentation] = [
.codex: DisplayRepresentation(title: "Codex"),
.claude: DisplayRepresentation(title: "Claude"),
]
var provider: UsageProvider {
switch self {
case .codex: .codex
case .claude: .claude
}
}
}
enum BurnWindowChoice: String, AppEnum {
case session
case weekly
static let typeDisplayRepresentation = TypeDisplayRepresentation(name: "Usage window")
static let caseDisplayRepresentations: [BurnWindowChoice: DisplayRepresentation] = [
.session: DisplayRepresentation(title: "Session (5-hour)"),
.weekly: DisplayRepresentation(title: "Weekly (7-day)"),
]
}
struct BurnDownSelectionIntent: AppIntent, WidgetConfigurationIntent {
static let title: LocalizedStringResource = "Burn Down"
static let description = IntentDescription("Select the provider and usage window to display.")
@Parameter(title: "Provider", default: .codex)
var provider: BurnProviderChoice
@Parameter(title: "Usage window", default: .session)
var window: BurnWindowChoice
init() {
self.provider = .codex
self.window = .session
}
}
struct BurnProviderSelectionIntent: AppIntent, WidgetConfigurationIntent {
static let title: LocalizedStringResource = "Burn Down Provider"
static let description = IntentDescription("Select the provider to display.")
@Parameter(title: "Provider", default: .codex)
var provider: BurnProviderChoice
init() {
self.provider = .codex
}
}
struct BurnDownEntry: TimelineEntry {
let date: Date
let provider: UsageProvider
let window: BurnWindowChoice
let snapshot: WidgetSnapshot
}
struct CombinedBurnDownEntry: TimelineEntry {
let date: Date
let provider: UsageProvider
let snapshot: WidgetSnapshot
}
struct BurnDownState {
private static let sessionWindowMinutes = 5 * 60
private static let weeklyWindowMinutes = 7 * 24 * 60
let entry: WidgetSnapshot.ProviderEntry
let selection: BurnWindowChoice
let now: Date
init?(
snapshot: WidgetSnapshot,
provider: UsageProvider,
selection: BurnWindowChoice,
now: Date = Date())
{
guard let entry = snapshot.entries.first(where: { $0.provider == provider }) else { return nil }
self.entry = entry
self.selection = selection
self.now = now
}
var secondaryGloballyCapsPrimary: Bool {
switch self.entry.provider {
case .codex, .claude: true
default: false
}
}
var secondaryExhausted: Bool {
guard self.secondaryGloballyCapsPrimary, let secondary = self.secondaryWindow else { return false }
guard secondary.remainingPercent <= 0 else { return false }
return secondary.resetsAt.map { $0 > self.now } ?? true
}
var primaryWindow: RateWindow? {
guard let primary = self.window(minutes: Self.sessionWindowMinutes) else { return nil }
guard self.secondaryExhausted, primary.remainingPercent > 0 else { return primary }
return RateWindow(
usedPercent: 100,
windowMinutes: primary.windowMinutes,
resetsAt: primary.resetsAt,
resetDescription: primary.resetDescription,
nextRegenPercent: primary.nextRegenPercent)
}
var secondaryWindow: RateWindow? {
self.window(minutes: Self.weeklyWindowMinutes)
}
var selectedWindow: RateWindow? {
switch self.selection {
case .session: self.primaryWindow
case .weekly: self.secondaryWindow
}
}
var blankPrimaryChart: Bool {
self.selection == .session
&& self.secondaryExhausted
&& self.window(minutes: Self.sessionWindowMinutes) != nil
}
var selectedResetOverride: Date? {
self.blankPrimaryChart ? self.secondaryWindow?.resetsAt : nil
}
private func window(minutes: Int) -> RateWindow? {
[self.entry.primary, self.entry.secondary]
.compactMap(\.self)
.first { $0.windowMinutes == minutes }
}
}
enum BurnDownRefreshSchedule {
private static let maximumInterval: TimeInterval = 30 * 60
static func nextRefresh(
snapshot: WidgetSnapshot,
provider: UsageProvider,
now: Date = Date()) -> Date
{
let fallback = now.addingTimeInterval(self.maximumInterval)
guard let entry = snapshot.entries.first(where: { $0.provider == provider }) else { return fallback }
let nextReset = [entry.primary?.resetsAt, entry.secondary?.resetsAt]
.compactMap(\.self)
.filter { $0 > now }
.min()?
.addingTimeInterval(1)
return min(fallback, nextReset ?? fallback)
}
}
struct BurnDownTimelineProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> BurnDownEntry {
BurnDownEntry(
date: Date(),
provider: .codex,
window: .session,
snapshot: WidgetPreviewData.snapshot())
}
func snapshot(for configuration: BurnDownSelectionIntent, in context: Context) async -> BurnDownEntry {
BurnDownEntry(
date: Date(),
provider: configuration.provider.provider,
window: configuration.window,
snapshot: WidgetSnapshotStore.load() ?? WidgetPreviewData.snapshot())
}
func timeline(
for configuration: BurnDownSelectionIntent,
in context: Context) async -> Timeline<BurnDownEntry>
{
let entry = BurnDownEntry(
date: Date(),
provider: configuration.provider.provider,
window: configuration.window,
snapshot: WidgetSnapshotStore.load() ?? WidgetPreviewData.emptySnapshot())
let refresh = BurnDownRefreshSchedule.nextRefresh(snapshot: entry.snapshot, provider: entry.provider)
return Timeline(entries: [entry], policy: .after(refresh))
}
}
struct CombinedBurnDownTimelineProvider: AppIntentTimelineProvider {
func placeholder(in context: Context) -> CombinedBurnDownEntry {
CombinedBurnDownEntry(
date: Date(),
provider: .codex,
snapshot: WidgetPreviewData.snapshot())
}
func snapshot(
for configuration: BurnProviderSelectionIntent,
in context: Context) async -> CombinedBurnDownEntry
{
CombinedBurnDownEntry(
date: Date(),
provider: configuration.provider.provider,
snapshot: WidgetSnapshotStore.load() ?? WidgetPreviewData.snapshot())
}
func timeline(
for configuration: BurnProviderSelectionIntent,
in context: Context) async -> Timeline<CombinedBurnDownEntry>
{
let entry = CombinedBurnDownEntry(
date: Date(),
provider: configuration.provider.provider,
snapshot: WidgetSnapshotStore.load() ?? WidgetPreviewData.emptySnapshot())
let refresh = BurnDownRefreshSchedule.nextRefresh(snapshot: entry.snapshot, provider: entry.provider)
return Timeline(entries: [entry], policy: .after(refresh))
}
}