Includes provider integrations, settings, NSStatusItem UI, tests, Makefile, and README with screenshot. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
3.7 KiB
Swift
121 lines
3.7 KiB
Swift
import Foundation
|
|
|
|
enum MetricValue: Sendable, Equatable {
|
|
case percent(Double)
|
|
case integer(Int)
|
|
case currency(Double)
|
|
case text(String)
|
|
case date(Date)
|
|
|
|
var displayString: String {
|
|
switch self {
|
|
case .percent(let value):
|
|
return String(format: "%.0f%%", value)
|
|
case .integer(let value):
|
|
return NumberFormatter.localizedString(from: NSNumber(value: value), number: .decimal)
|
|
case .currency(let value):
|
|
return String(format: "$%.2f", value)
|
|
case .text(let value):
|
|
return value
|
|
case .date(let value):
|
|
return Self.dateFormatter.string(from: value)
|
|
}
|
|
}
|
|
|
|
var compactString: String {
|
|
menuBarCompactString
|
|
}
|
|
|
|
var menuBarCompactString: String {
|
|
switch self {
|
|
case .percent(let value):
|
|
return String(format: "%.0f%%", value)
|
|
case .integer(let value):
|
|
if value >= 1_000_000 {
|
|
return String(format: "%.1fM", Double(value) / 1_000_000)
|
|
}
|
|
if value >= 1_000 {
|
|
return String(format: "%.1fK", Double(value) / 1_000)
|
|
}
|
|
return "\(value)"
|
|
case .currency(let value):
|
|
return String(format: "$%.0f", value)
|
|
case .text(let value):
|
|
if value.count > 10 {
|
|
return String(value.prefix(10)) + "…"
|
|
}
|
|
return value
|
|
case .date(let value):
|
|
return Self.shortDateFormatter.string(from: value)
|
|
}
|
|
}
|
|
|
|
private static let dateFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .medium
|
|
formatter.timeStyle = .short
|
|
return formatter
|
|
}()
|
|
|
|
private static let shortDateFormatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .short
|
|
formatter.timeStyle = .short
|
|
return formatter
|
|
}()
|
|
}
|
|
|
|
struct ProviderUsageSnapshot: Sendable {
|
|
let providerID: ProviderID
|
|
let fetchedAt: Date
|
|
let values: [UsageMetric: MetricValue]
|
|
let isStale: Bool
|
|
|
|
func value(for metric: UsageMetric) -> MetricValue? {
|
|
values[metric]
|
|
}
|
|
|
|
func formatted(_ metric: UsageMetric, compact: Bool = false) -> String? {
|
|
guard let value = value(for: metric) else { return nil }
|
|
return compact ? value.menuBarCompactString : value.displayString
|
|
}
|
|
|
|
func menuBarDisplay(for metrics: [UsageMetric]) -> String? {
|
|
let parts = metrics.compactMap { formatted($0, compact: true) }
|
|
guard !parts.isEmpty else { return nil }
|
|
return parts.joined(separator: "·")
|
|
}
|
|
|
|
func firstPercent(in metrics: [UsageMetric]) -> Double? {
|
|
for metric in metrics {
|
|
if case .percent(let value)? = value(for: metric) {
|
|
return value
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
struct ProviderDisplayState: Sendable {
|
|
let providerID: ProviderID
|
|
let snapshot: ProviderUsageSnapshot?
|
|
let errorMessage: String?
|
|
let isLoading: Bool
|
|
|
|
static func loading(_ providerID: ProviderID) -> ProviderDisplayState {
|
|
ProviderDisplayState(providerID: providerID, snapshot: nil, errorMessage: nil, isLoading: true)
|
|
}
|
|
|
|
static func success(_ snapshot: ProviderUsageSnapshot) -> ProviderDisplayState {
|
|
ProviderDisplayState(
|
|
providerID: snapshot.providerID,
|
|
snapshot: snapshot,
|
|
errorMessage: nil,
|
|
isLoading: false
|
|
)
|
|
}
|
|
|
|
static func failure(_ providerID: ProviderID, message: String) -> ProviderDisplayState {
|
|
ProviderDisplayState(providerID: providerID, snapshot: nil, errorMessage: message, isLoading: false)
|
|
}
|
|
}
|