macos-ai-menubar-usage/AIMenubarUsage/Models/ProviderUsageSnapshot.swift
Felipe M. 00c4e33171
Initial commit: AI Menubar Usage — macOS menu bar app for Cursor and Claude.
Includes provider integrations, settings, NSStatusItem UI, tests, Makefile, and README with screenshot.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 20:41:40 +02:00

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)
}
}