229 lines
7.3 KiB
Swift
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))
|
|
}
|
|
}
|