fb66e63e87
* test: pin provider presentation policy * refactor: move usage window policy into descriptors * refactor: move usage menu policy into descriptors --------- Co-authored-by: Peter Steinberger <steipete@clawstudio.local>
181 lines
7.3 KiB
Swift
181 lines
7.3 KiB
Swift
import CodexBarCore
|
||
import Foundation
|
||
|
||
enum UsagePaceText {
|
||
struct WeeklyDetail {
|
||
let leftLabel: String
|
||
let rightLabel: String?
|
||
let expectedUsedPercent: Double
|
||
let stage: UsagePace.Stage
|
||
}
|
||
|
||
struct SessionEquivalentDetail: Equatable {
|
||
let leftText: String
|
||
let rightText: String
|
||
let accessibilityLabel: String
|
||
}
|
||
|
||
private enum DetailContext {
|
||
case session
|
||
case weekly
|
||
}
|
||
|
||
static func weeklySummary(provider: UsageProvider, pace: UsagePace, now: Date = .init()) -> String {
|
||
let detail = self.weeklyDetail(provider: provider, pace: pace, now: now)
|
||
if let rightLabel = detail.rightLabel {
|
||
return L("Pace: %@ · %@", detail.leftLabel, rightLabel)
|
||
}
|
||
return L("Pace: %@", detail.leftLabel)
|
||
}
|
||
|
||
static func weeklyDetail(provider: UsageProvider, pace: UsagePace, now: Date = .init()) -> WeeklyDetail {
|
||
WeeklyDetail(
|
||
leftLabel: self.detailLeftLabel(for: pace),
|
||
rightLabel: self.detailRightLabel(for: pace, provider: provider, context: .weekly, now: now),
|
||
expectedUsedPercent: pace.expectedUsedPercent,
|
||
stage: pace.stage)
|
||
}
|
||
|
||
static func sessionEquivalentDetail(forecast: SessionEquivalentForecast) -> SessionEquivalentDetail {
|
||
let leftText = Self.sessionQuotaEstimateText(forecast.estimatedWindowsToExhaustWeekly)
|
||
let rightText = Self.windowsUntilResetText(forecast.windowsUntilReset)
|
||
return SessionEquivalentDetail(
|
||
leftText: leftText,
|
||
rightText: rightText,
|
||
accessibilityLabel: L("%@ · %@", leftText, rightText))
|
||
}
|
||
|
||
private static func sessionQuotaEstimateText(_ value: Double) -> String {
|
||
let displayedEstimate: String
|
||
let unit: String
|
||
if value.isFinite, value > 0 {
|
||
let boundedValue = min(value, 1_000_000)
|
||
let roundedValue = (boundedValue * 10).rounded() / 10
|
||
displayedEstimate = roundedValue.formatted(
|
||
.number
|
||
.precision(.fractionLength(0...1))
|
||
.locale(codexBarLocalizedLocale()))
|
||
unit = roundedValue > 0 && roundedValue <= 1 ? L("session quota") : L("session quotas")
|
||
} else {
|
||
displayedEstimate = codexBarLocalizedInteger(0)
|
||
unit = L("session quotas")
|
||
}
|
||
let estimateValue = L("session_quota_estimate_value_format", displayedEstimate, unit)
|
||
return L("Estimated: %@", L("%@ left", estimateValue))
|
||
}
|
||
|
||
private static func windowsUntilResetText(_ count: Int) -> String {
|
||
let combinedText = String(
|
||
format: L("≈%d full 5h windows of weekly left · %d windows until reset"),
|
||
locale: codexBarLocalizedResourceLocale(),
|
||
arguments: [0, count])
|
||
guard let separatorRange = combinedText.range(of: " · ") else { return combinedText }
|
||
return String(combinedText[separatorRange.upperBound...])
|
||
}
|
||
|
||
private static func detailLeftLabel(for pace: UsagePace) -> String {
|
||
let deltaValue = Int(abs(pace.deltaPercent).rounded())
|
||
if deltaValue == 0 {
|
||
return L("On pace")
|
||
}
|
||
switch pace.stage {
|
||
case .onTrack:
|
||
return L("On pace")
|
||
case .slightlyAhead, .ahead, .farAhead:
|
||
return L("%d%% in deficit", deltaValue)
|
||
case .slightlyBehind, .behind, .farBehind:
|
||
return L("%d%% in reserve", deltaValue)
|
||
}
|
||
}
|
||
|
||
private static func detailRightLabel(
|
||
for pace: UsagePace,
|
||
provider: UsageProvider,
|
||
context: DetailContext,
|
||
now: Date) -> String?
|
||
{
|
||
let etaLabel: String?
|
||
if pace.willLastToReset {
|
||
etaLabel = self.combinedLastsLabel(for: pace, provider: provider)
|
||
} else if let etaSeconds = pace.etaSeconds {
|
||
let etaText = Self.durationText(seconds: etaSeconds, now: now)
|
||
if context == .session {
|
||
etaLabel = etaText == "now" ? L("Projected empty now") : L("Projected empty in %@", etaText)
|
||
} else {
|
||
etaLabel = etaText == "now" ? L("Runs out now") : L("Runs out in %@", etaText)
|
||
}
|
||
} else {
|
||
etaLabel = nil
|
||
}
|
||
|
||
guard let runOutProbability = pace.runOutProbability else { return etaLabel }
|
||
let roundedRisk = self.roundedRiskPercent(runOutProbability)
|
||
let riskLabel = L("(%d%% risk)", roundedRisk)
|
||
if let etaLabel {
|
||
return L("%@ %@", etaLabel, riskLabel)
|
||
}
|
||
return riskLabel
|
||
}
|
||
|
||
private static func combinedLastsLabel(for pace: UsagePace, provider: UsageProvider) -> String {
|
||
let paceCapability = ProviderDescriptorRegistry.descriptor(for: provider).pace
|
||
guard paceCapability.showsHeadroomHint else { return L("Lasts until reset") }
|
||
guard let speedLabel = self.speedHintLabel(for: pace) else {
|
||
return L("Lasts until reset")
|
||
}
|
||
return L("%@ · %@", L("Lasts until reset"), speedLabel)
|
||
}
|
||
|
||
private static func speedHintLabel(for pace: UsagePace) -> String? {
|
||
guard pace.deltaPercent < -15,
|
||
let multiplier = pace.speedMultiplierToReset,
|
||
multiplier >= 1.5
|
||
else { return nil }
|
||
return L("1.5× headroom")
|
||
}
|
||
|
||
private static func durationText(seconds: TimeInterval, now: Date) -> String {
|
||
let date = now.addingTimeInterval(seconds)
|
||
let countdown = UsageFormatter.resetCountdownDescription(from: date, now: now)
|
||
if countdown == "now" {
|
||
return "now"
|
||
}
|
||
if countdown.hasPrefix("in ") {
|
||
return String(countdown.dropFirst(3))
|
||
}
|
||
return countdown
|
||
}
|
||
|
||
private static func roundedRiskPercent(_ probability: Double) -> Int {
|
||
let percent = probability.clamped(to: 0...1) * 100
|
||
let rounded = (percent / 5).rounded() * 5
|
||
return Int(rounded)
|
||
}
|
||
|
||
static func sessionPace(provider: UsageProvider, window: RateWindow, now: Date) -> UsagePace? {
|
||
let paceCapability = ProviderDescriptorRegistry.descriptor(for: provider).pace
|
||
guard paceCapability.supportsSessionPace(window: window, now: now) else { return nil }
|
||
guard window.remainingPercent > 0 else { return nil }
|
||
guard let pace = UsagePace.weekly(window: window, now: now, defaultWindowMinutes: 300) else { return nil }
|
||
guard pace.expectedUsedPercent >= 3 else { return nil }
|
||
return pace
|
||
}
|
||
|
||
static func sessionDetail(provider: UsageProvider, window: RateWindow, now: Date = .init()) -> WeeklyDetail? {
|
||
guard let pace = sessionPace(provider: provider, window: window, now: now) else { return nil }
|
||
return WeeklyDetail(
|
||
leftLabel: Self.detailLeftLabel(for: pace),
|
||
rightLabel: Self.detailRightLabel(for: pace, provider: provider, context: .session, now: now),
|
||
expectedUsedPercent: pace.expectedUsedPercent,
|
||
stage: pace.stage)
|
||
}
|
||
|
||
static func sessionSummary(provider: UsageProvider, window: RateWindow, now: Date = .init()) -> String? {
|
||
guard let detail = sessionDetail(provider: provider, window: window, now: now) else { return nil }
|
||
if let rightLabel = detail.rightLabel {
|
||
return L("Pace: %@ · %@", detail.leftLabel, rightLabel)
|
||
}
|
||
return L("Pace: %@", detail.leftLabel)
|
||
}
|
||
}
|