import Foundation public enum ResetTimeDisplayStyle: String, Codable, Sendable { case countdown case absolute} public enum UsageFormatter { public static func usageLine(remaining: Double, used: Double, showUsed: Bool) -> String { let percent = showUsed ? used : remaining let clamped = min(100, max(0, percent)) let suffix = showUsed ? "used" : "left" return String(format: "%.0f%% %@", clamped, suffix) } public static func resetCountdownDescription(from date: Date, now: Date = .init()) -> String { let seconds = max(0, date.timeIntervalSince(now)) if seconds < 1 { return "now" } let totalMinutes = max(1, Int(ceil(seconds / 60.0))) let days = totalMinutes / (24 * 60) let hours = (totalMinutes / 60) % 24 let minutes = totalMinutes % 60 if days > 0 { if hours > 0 { return "in \(days)d \(hours)h" } return "in \(days)d" } if hours > 0 { if minutes > 0 { return "in \(hours)h \(minutes)m" } return "in \(hours)h" } return "in \(totalMinutes)m" } public static func resetDescription(from date: Date, now: Date = .init()) -> String { // Human-friendly phrasing: today / tomorrow / date+time. let calendar = Calendar.current if calendar.isDate(date, inSameDayAs: now) { return date.formatted(date: .omitted, time: .shortened) } if let tomorrow = calendar.date(byAdding: .day, value: 1, to: now), calendar.isDate(date, inSameDayAs: tomorrow) { return "tomorrow, \(date.formatted(date: .omitted, time: .shortened))" } return date.formatted(date: .abbreviated, time: .shortened) } public static func resetLine( for window: RateWindow, style: ResetTimeDisplayStyle, now: Date = .init()) -> String? { if let date = window.resetsAt { let text = style == .countdown ? self.resetCountdownDescription(from: date, now: now) : self.resetDescription(from: date, now: now) return "Resets \(text)" } if let desc = window.resetDescription { let trimmed = desc.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return nil } if trimmed.lowercased().hasPrefix("resets") { return trimmed } return "Resets \(trimmed)" } return nil } public static func updatedString(from date: Date, now: Date = .init()) -> String { let delta = now.timeIntervalSince(date) if abs(delta) < 60 { return "Updated just now" } if let hours = Calendar.current.dateComponents([.hour], from: date, to: now).hour, hours < 24 { #if os(macOS) let rel = RelativeDateTimeFormatter() rel.unitsStyle = .abbreviated return "Updated \(rel.localizedString(for: date, relativeTo: now))" #else let seconds = max(0, Int(now.timeIntervalSince(date))) if seconds < 3600 { let minutes = max(1, seconds / 60) return "Updated \(minutes)m ago" } let wholeHours = max(1, seconds / 3600) return "Updated \(wholeHours)h ago" #endif } else { return "Updated \(date.formatted(date: .omitted, time: .shortened))" } } public static func creditsString(from value: Double) -> String { let number = NumberFormatter() number.numberStyle = .decimal number.maximumFractionDigits = 2 // Use explicit locale for consistent formatting on all systems number.locale = Locale(identifier: "en_US_POSIX") let formatted = number.string(from: NSNumber(value: value)) ?? String(format: "%.2f", value) return "\(formatted) left" } /// Formats a USD value with proper negative handling and thousand separators. /// Uses Swift's modern FormatStyle API (iOS 15+/macOS 12+) for robust, locale-aware formatting. public static func usdString(_ value: Double) -> String { value.formatted(.currency(code: "USD").locale(Locale(identifier: "en_US"))) } /// Formats a currency value with the specified currency code. /// Uses FormatStyle with explicit en_US locale to ensure consistent formatting /// regardless of the user's system locale (e.g., pt-BR users see $54.72 not US$ 54,72). public static func currencyString(_ value: Double, currencyCode: String) -> String { value.formatted(.currency(code: currencyCode).locale(Locale(identifier: "en_US"))) } public static func tokenCountString(_ value: Int) -> String { let absValue = abs(value) let sign = value < 0 ? "-" : "" let units: [(threshold: Int, divisor: Double, suffix: String)] = [ (1_000_000_000, 1_000_000_000, "B"), (1_000_000, 1_000_000, "M"), (1000, 1000, "K"), ] for unit in units where absValue >= unit.threshold { let scaled = Double(absValue) / unit.divisor let formatted: String if scaled >= 10 { formatted = String(format: "%.0f", scaled) } else { var s = String(format: "%.1f", scaled) if s.hasSuffix(".0") { s.removeLast(2) } formatted = s } return "\(sign)\(formatted)\(unit.suffix)" } let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.usesGroupingSeparator = true formatter.locale = Locale(identifier: "en_US_POSIX") return formatter.string(from: NSNumber(value: value)) ?? "\(value)" } public static func creditEventSummary(_ event: CreditEvent) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium let number = NumberFormatter() number.numberStyle = .decimal number.maximumFractionDigits = 2 let credits = number.string(from: NSNumber(value: event.creditsUsed)) ?? "0" return "\(formatter.string(from: event.date)) · \(event.service) · \(credits) credits" } public static func creditEventCompact(_ event: CreditEvent) -> String { let formatter = DateFormatter() formatter.dateFormat = "MMM d" let number = NumberFormatter() number.numberStyle = .decimal number.maximumFractionDigits = 2 let credits = number.string(from: NSNumber(value: event.creditsUsed)) ?? "0" return "\(formatter.string(from: event.date)) — \(event.service): \(credits)" } public static func creditShort(_ value: Double) -> String { if value >= 1000 { let k = value / 1000 return String(format: "%.1fk", k) } return String(format: "%.0f", value) } public static func truncatedSingleLine(_ text: String, max: Int = 80) -> String { let single = text .replacingOccurrences(of: "\n", with: " ") .trimmingCharacters(in: .whitespacesAndNewlines) guard single.count > max else { return single } let idx = single.index(single.startIndex, offsetBy: max) return "\(single[..<idx])…" } public static func modelDisplayName(_ raw: String) -> String { var cleaned = raw.trimmingCharacters(in: .whitespacesAndNewlines) guard !cleaned.isEmpty else { return raw } let patterns = [ #"(?:-|\s)\d{8}$"#, #"(?:-|\s)\d{4}-\d{2}-\d{2}$"#, #"\s\d{4}\s\d{4}$"#, ] for pattern in patterns { if let range = cleaned.range(of: pattern, options: .regularExpression) { cleaned.removeSubrange(range) break } } if let trailing = cleaned.range(of: #"[ \t-]+$"#, options: .regularExpression) { cleaned.removeSubrange(trailing) } return cleaned.isEmpty ? raw : cleaned } public static func modelCostDetail(_ model: String, costUSD: Double?, totalTokens: Int? = nil) -> String? { let costDetail: String? = if let label = CostUsagePricing.codexDisplayLabel(model: model) { label } else if let costUSD { self.usdString(costUSD) } else { nil } let tokenDetail = totalTokens.map(self.tokenCountString) let parts = [costDetail, tokenDetail].compactMap(\.self) guard !parts.isEmpty else { return nil } return parts.joined(separator: " · ") } /// Cleans a provider plan string: strip ANSI/bracket noise, drop boilerplate words, collapse whitespace, and /// ensure a leading capital if the result starts lowercase. public static func cleanPlanName(_ text: String) -> String { let stripped = TextParsing.stripANSICodes(text) let withoutCodes = stripped.replacingOccurrences( of: #"^\s*(?:\[\d{1,3}m\s*)+"#, with: "", options: [.regularExpression]) let withoutBoilerplate = withoutCodes.replacingOccurrences( of: #"(?i)\b(claude|codex|account|plan)\b"#, with: "", options: [.regularExpression]) var cleaned = withoutBoilerplate .replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression) .trimmingCharacters(in: .whitespacesAndNewlines) if cleaned.isEmpty { cleaned = stripped.trimmingCharacters(in: .whitespacesAndNewlines) } if cleaned.lowercased() == "oauth" { return "Ollama" } // Capitalize first letter only if lowercase, preserving acronyms like "AI" if let first = cleaned.first, first.isLowercase { return cleaned.prefix(1).uppercased() + cleaned.dropFirst() } return cleaned }}
import Foundation
public enum ResetTimeDisplayStyle: String, Codable, Sendable {
case countdown
case absolute
}
public enum UsageFormatter {
public static func usageLine(remaining: Double, used: Double, showUsed: Bool) -> String {
let percent = showUsed ? used : remaining
let clamped = min(100, max(0, percent))
let suffix = showUsed ? "used" : "left"
return String(format: "%.0f%% %@", clamped, suffix)
}
public static func resetCountdownDescription(from date: Date, now: Date = .init()) -> String {
let seconds = max(0, date.timeIntervalSince(now))
if seconds < 1 { return "now" }
let totalMinutes = max(1, Int(ceil(seconds / 60.0)))
let days = totalMinutes / (24 * 60)
let hours = (totalMinutes / 60) % 24
let minutes = totalMinutes % 60
if days > 0 {
if hours > 0 { return "in \(days)d \(hours)h" }
return "in \(days)d"
}
if hours > 0 {
if minutes > 0 { return "in \(hours)h \(minutes)m" }
return "in \(hours)h"
}
return "in \(totalMinutes)m"
}
public static func resetDescription(from date: Date, now: Date = .init()) -> String {
// Human-friendly phrasing: today / tomorrow / date+time.
let calendar = Calendar.current
if calendar.isDate(date, inSameDayAs: now) {
return date.formatted(date: .omitted, time: .shortened)
}
if let tomorrow = calendar.date(byAdding: .day, value: 1, to: now),
calendar.isDate(date, inSameDayAs: tomorrow)
{
return "tomorrow, \(date.formatted(date: .omitted, time: .shortened))"
}
return date.formatted(date: .abbreviated, time: .shortened)
}
public static func resetLine(
for window: RateWindow,
style: ResetTimeDisplayStyle,
now: Date = .init()) -> String?
{
if let date = window.resetsAt {
let text = style == .countdown
? self.resetCountdownDescription(from: date, now: now)
: self.resetDescription(from: date, now: now)
return "Resets \(text)"
}
if let desc = window.resetDescription {
let trimmed = desc.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
if trimmed.lowercased().hasPrefix("resets") { return trimmed }
return "Resets \(trimmed)"
}
return nil
}
public static func updatedString(from date: Date, now: Date = .init()) -> String {
let delta = now.timeIntervalSince(date)
if abs(delta) < 60 {
return "Updated just now"
}
if let hours = Calendar.current.dateComponents([.hour], from: date, to: now).hour, hours < 24 {
#if os(macOS)
let rel = RelativeDateTimeFormatter()
rel.unitsStyle = .abbreviated
return "Updated \(rel.localizedString(for: date, relativeTo: now))"
#else
let seconds = max(0, Int(now.timeIntervalSince(date)))
if seconds < 3600 {
let minutes = max(1, seconds / 60)
return "Updated \(minutes)m ago"
}
let wholeHours = max(1, seconds / 3600)
return "Updated \(wholeHours)h ago"
#endif
} else {
return "Updated \(date.formatted(date: .omitted, time: .shortened))"
}
}
public static func creditsString(from value: Double) -> String {
let number = NumberFormatter()
number.numberStyle = .decimal
number.maximumFractionDigits = 2
// Use explicit locale for consistent formatting on all systems
number.locale = Locale(identifier: "en_US_POSIX")
let formatted = number.string(from: NSNumber(value: value)) ?? String(format: "%.2f", value)
return "\(formatted) left"
}
/// Formats a USD value with proper negative handling and thousand separators.
/// Uses Swift's modern FormatStyle API (iOS 15+/macOS 12+) for robust, locale-aware formatting.
public static func usdString(_ value: Double) -> String {
value.formatted(.currency(code: "USD").locale(Locale(identifier: "en_US")))
}
/// Formats a currency value with the specified currency code.
/// Uses FormatStyle with explicit en_US locale to ensure consistent formatting
/// regardless of the user's system locale (e.g., pt-BR users see $54.72 not US$ 54,72).
public static func currencyString(_ value: Double, currencyCode: String) -> String {
value.formatted(.currency(code: currencyCode).locale(Locale(identifier: "en_US")))
}
public static func tokenCountString(_ value: Int) -> String {
let absValue = abs(value)
let sign = value < 0 ? "-" : ""
let units: [(threshold: Int, divisor: Double, suffix: String)] = [
(1_000_000_000, 1_000_000_000, "B"),
(1_000_000, 1_000_000, "M"),
(1000, 1000, "K"),
]
for unit in units where absValue >= unit.threshold {
let scaled = Double(absValue) / unit.divisor
let formatted: String
if scaled >= 10 {
formatted = String(format: "%.0f", scaled)
} else {
var s = String(format: "%.1f", scaled)
if s.hasSuffix(".0") { s.removeLast(2) }
formatted = s
}
return "\(sign)\(formatted)\(unit.suffix)"
}
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.usesGroupingSeparator = true
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter.string(from: NSNumber(value: value)) ?? "\(value)"
}
public static func creditEventSummary(_ event: CreditEvent) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
let number = NumberFormatter()
number.numberStyle = .decimal
number.maximumFractionDigits = 2
let credits = number.string(from: NSNumber(value: event.creditsUsed)) ?? "0"
return "\(formatter.string(from: event.date)) · \(event.service) · \(credits) credits"
}
public static func creditEventCompact(_ event: CreditEvent) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "MMM d"
let number = NumberFormatter()
number.numberStyle = .decimal
number.maximumFractionDigits = 2
let credits = number.string(from: NSNumber(value: event.creditsUsed)) ?? "0"
return "\(formatter.string(from: event.date)) — \(event.service): \(credits)"
}
public static func creditShort(_ value: Double) -> String {
if value >= 1000 {
let k = value / 1000
return String(format: "%.1fk", k)
}
return String(format: "%.0f", value)
}
public static func truncatedSingleLine(_ text: String, max: Int = 80) -> String {
let single = text
.replacingOccurrences(of: "\n", with: " ")
.trimmingCharacters(in: .whitespacesAndNewlines)
guard single.count > max else { return single }
let idx = single.index(single.startIndex, offsetBy: max)
return "\(single[..<idx])…"
}
public static func modelDisplayName(_ raw: String) -> String {
var cleaned = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty else { return raw }
let patterns = [
#"(?:-|\s)\d{8}$"#,
#"(?:-|\s)\d{4}-\d{2}-\d{2}$"#,
#"\s\d{4}\s\d{4}$"#,
]
for pattern in patterns {
if let range = cleaned.range(of: pattern, options: .regularExpression) {
cleaned.removeSubrange(range)
break
}
}
if let trailing = cleaned.range(of: #"[ \t-]+$"#, options: .regularExpression) {
cleaned.removeSubrange(trailing)
}
return cleaned.isEmpty ? raw : cleaned
}
public static func modelCostDetail(_ model: String, costUSD: Double?, totalTokens: Int? = nil) -> String? {
let costDetail: String? = if let label = CostUsagePricing.codexDisplayLabel(model: model) {
label
} else if let costUSD {
self.usdString(costUSD)
} else {
nil
}
let tokenDetail = totalTokens.map(self.tokenCountString)
let parts = [costDetail, tokenDetail].compactMap(\.self)
guard !parts.isEmpty else { return nil }
return parts.joined(separator: " · ")
}
/// Cleans a provider plan string: strip ANSI/bracket noise, drop boilerplate words, collapse whitespace, and
/// ensure a leading capital if the result starts lowercase.
public static func cleanPlanName(_ text: String) -> String {
let stripped = TextParsing.stripANSICodes(text)
let withoutCodes = stripped.replacingOccurrences(
of: #"^\s*(?:\[\d{1,3}m\s*)+"#,
with: "",
options: [.regularExpression])
let withoutBoilerplate = withoutCodes.replacingOccurrences(
of: #"(?i)\b(claude|codex|account|plan)\b"#,
with: "",
options: [.regularExpression])
var cleaned = withoutBoilerplate
.replacingOccurrences(of: #"\s+"#, with: " ", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines)
if cleaned.isEmpty {
cleaned = stripped.trimmingCharacters(in: .whitespacesAndNewlines)
}
if cleaned.lowercased() == "oauth" {
return "Ollama"
}
// Capitalize first letter only if lowercase, preserving acronyms like "AI"
if let first = cleaned.first, first.isLowercase {
return cleaned.prefix(1).uppercased() + cleaned.dropFirst()
}
return cleaned
}
}