import CodexBarCoreimport SwiftUI /// SwiftUI card used inside the NSMenu to mirror Apple's rich menu panels.struct UsageMenuCardView: View { struct Model { struct Metric: Identifiable { let id: String let title: String let percentLeft: Double let resetText: String? var percentLabel: String { String(format: "%.0f%% left", self.percentLeft) } } enum SubtitleStyle { case info case loading case error } let providerName: String let email: String let subtitleText: String let subtitleStyle: SubtitleStyle let planText: String? let metrics: [Metric] let creditsText: String? let placeholder: String? let progressColor: Color } let model: Model var body: some View { VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline) { Text(self.model.providerName) .font(.headline) .fontWeight(.semibold) Spacer() Text(self.model.email) .font(.subheadline) .foregroundStyle(.secondary) } HStack(alignment: .top) { Text(self.model.subtitleText) .font(.footnote) .foregroundStyle(self.subtitleColor) .lineLimit(1) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) Spacer() if let plan = self.model.planText { Text(plan) .font(.footnote) .foregroundStyle(.secondary) .lineLimit(1) } } } Divider() if self.model.metrics.isEmpty { if let placeholder = self.model.placeholder { Text(placeholder) .foregroundStyle(.secondary) .font(.subheadline) } } else { VStack(alignment: .leading, spacing: 12) { ForEach(self.model.metrics) { metric in VStack(alignment: .leading, spacing: 6) { Text(metric.title) .font(.subheadline) .fontWeight(.medium) UsageProgressBar(percentLeft: metric.percentLeft, tint: self.model.progressColor) HStack(alignment: .firstTextBaseline) { Text(metric.percentLabel) .font(.footnote) Spacer() if let reset = metric.resetText { Text(reset) .font(.footnote) .foregroundStyle(.secondary) } } } } if let credits = self.model.creditsText { VStack(alignment: .leading, spacing: 2) { Text("Credits") .font(.subheadline) .fontWeight(.medium) Text(credits) .font(.footnote) } } } .padding(.bottom, self.model.creditsText == nil ? 8 : 2) } } .padding(.horizontal, 16) .padding(.top, 4) .padding(.bottom, 4) .frame(minWidth: 300, maxWidth: 300, alignment: .leading) } private var subtitleColor: Color { switch self.model.subtitleStyle { case .info: .secondary case .loading: .secondary case .error: Color(nsColor: .systemRed) } }} // MARK: - Model factory extension UsageMenuCardView.Model { struct Input { let provider: UsageProvider let metadata: ProviderMetadata let snapshot: UsageSnapshot? let credits: CreditsSnapshot? let creditsError: String? let account: AccountInfo let isRefreshing: Bool let lastError: String? } static func make(_ input: Input) -> UsageMenuCardView.Model { let email = Self.email( for: input.provider, snapshot: input.snapshot, account: input.account) let planText = Self.plan(for: input.provider, snapshot: input.snapshot, account: input.account) let metrics = Self.metrics(metadata: input.metadata, snapshot: input.snapshot) let creditsText = Self.creditsLine(metadata: input.metadata, credits: input.credits, error: input.creditsError) let subtitle = Self.subtitle( snapshot: input.snapshot, isRefreshing: input.isRefreshing, lastError: input.lastError) let placeholder = input.snapshot == nil && !input.isRefreshing && input.lastError == nil ? "No usage yet" : nil return UsageMenuCardView.Model( providerName: input.metadata.displayName, email: email, subtitleText: subtitle.text, subtitleStyle: subtitle.style, planText: planText, metrics: metrics, creditsText: creditsText, placeholder: placeholder, progressColor: Self.progressColor(for: input.provider)) } private static func email( for provider: UsageProvider, snapshot: UsageSnapshot?, account: AccountInfo) -> String { switch provider { case .codex: if let email = snapshot?.accountEmail, !email.isEmpty { return email } if let email = account.email, !email.isEmpty { return email } case .claude: if let email = snapshot?.accountEmail, !email.isEmpty { return email } } return "" } private static func plan(for provider: UsageProvider, snapshot: UsageSnapshot?, account: AccountInfo) -> String? { switch provider { case .codex: if let plan = snapshot?.loginMethod, !plan.isEmpty { return self.planDisplay(plan) } if let plan = account.plan, !plan.isEmpty { return Self.planDisplay(plan) } case .claude: if let plan = snapshot?.loginMethod, !plan.isEmpty { return self.planDisplay(plan) } } return nil } private static func planDisplay(_ text: String) -> String { let cleaned = UsageFormatter.cleanPlanName(text) return cleaned.isEmpty ? text : cleaned } private static func subtitle( snapshot: UsageSnapshot?, isRefreshing: Bool, lastError: String?) -> (text: String, style: SubtitleStyle) { if let lastError, !lastError.isEmpty { return (UsageFormatter.truncatedSingleLine(lastError, max: 80), .error) } if isRefreshing, snapshot == nil { return ("Refreshing...", .loading) } if let updated = snapshot?.updatedAt { return (UsageFormatter.updatedString(from: updated), .info) } return ("Not fetched yet", .info) } private static func metrics(metadata: ProviderMetadata, snapshot: UsageSnapshot?) -> [Metric] { guard let snapshot else { return [] } var metrics: [Metric] = [] metrics.append(Metric( id: "primary", title: metadata.sessionLabel, percentLeft: Self.clamped(snapshot.primary.remainingPercent), resetText: Self.resetText(for: snapshot.primary))) metrics.append(Metric( id: "secondary", title: metadata.weeklyLabel, percentLeft: Self.clamped(snapshot.secondary.remainingPercent), resetText: Self.resetText(for: snapshot.secondary))) if metadata.supportsOpus, let opus = snapshot.tertiary { metrics.append(Metric( id: "tertiary", title: metadata.opusLabel ?? "Sonnet", percentLeft: Self.clamped(opus.remainingPercent), resetText: Self.resetText(for: opus))) } return metrics } private static func creditsLine( metadata: ProviderMetadata, credits: CreditsSnapshot?, error: String?) -> String? { guard metadata.supportsCredits else { return nil } if let credits { return UsageFormatter.creditsString(from: credits.remaining) } if let error, !error.isEmpty { return UsageFormatter.truncatedSingleLine(error, max: 80) } return metadata.creditsHint } private static func clamped(_ value: Double) -> Double { min(100, max(0, value)) } private static func progressColor(for provider: UsageProvider) -> Color { switch provider { case .codex: Color(red: 73 / 255, green: 163 / 255, blue: 176 / 255) case .claude: Color(red: 204 / 255, green: 124 / 255, blue: 94 / 255) } } private static func resetText(for window: RateWindow) -> String? { if let date = window.resetsAt { return "Resets \(UsageFormatter.resetDescription(from: date))" } if let desc = window.resetDescription, !desc.isEmpty { return desc } return nil }}
import CodexBarCore
import SwiftUI
/// SwiftUI card used inside the NSMenu to mirror Apple's rich menu panels.
struct UsageMenuCardView: View {
struct Model {
struct Metric: Identifiable {
let id: String
let title: String
let percentLeft: Double
let resetText: String?
var percentLabel: String {
String(format: "%.0f%% left", self.percentLeft)
}
}
enum SubtitleStyle {
case info
case loading
case error
}
let providerName: String
let email: String
let subtitleText: String
let subtitleStyle: SubtitleStyle
let planText: String?
let metrics: [Metric]
let creditsText: String?
let placeholder: String?
let progressColor: Color
}
let model: Model
var body: some View {
VStack(alignment: .leading, spacing: 10) {
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .firstTextBaseline) {
Text(self.model.providerName)
.font(.headline)
.fontWeight(.semibold)
Spacer()
Text(self.model.email)
.font(.subheadline)
.foregroundStyle(.secondary)
}
HStack(alignment: .top) {
Text(self.model.subtitleText)
.font(.footnote)
.foregroundStyle(self.subtitleColor)
.lineLimit(1)
.multilineTextAlignment(.leading)
.fixedSize(horizontal: false, vertical: true)
Spacer()
if let plan = self.model.planText {
Text(plan)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
}
Divider()
if self.model.metrics.isEmpty {
if let placeholder = self.model.placeholder {
Text(placeholder)
.foregroundStyle(.secondary)
.font(.subheadline)
}
} else {
VStack(alignment: .leading, spacing: 12) {
ForEach(self.model.metrics) { metric in
VStack(alignment: .leading, spacing: 6) {
Text(metric.title)
.font(.subheadline)
.fontWeight(.medium)
UsageProgressBar(percentLeft: metric.percentLeft, tint: self.model.progressColor)
HStack(alignment: .firstTextBaseline) {
Text(metric.percentLabel)
.font(.footnote)
Spacer()
if let reset = metric.resetText {
Text(reset)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
}
if let credits = self.model.creditsText {
VStack(alignment: .leading, spacing: 2) {
Text("Credits")
.font(.subheadline)
.fontWeight(.medium)
Text(credits)
.font(.footnote)
}
}
}
.padding(.bottom, self.model.creditsText == nil ? 8 : 2)
}
}
.padding(.horizontal, 16)
.padding(.top, 4)
.padding(.bottom, 4)
.frame(minWidth: 300, maxWidth: 300, alignment: .leading)
}
private var subtitleColor: Color {
switch self.model.subtitleStyle {
case .info: .secondary
case .loading: .secondary
case .error: Color(nsColor: .systemRed)
}
}
}
// MARK: - Model factory
extension UsageMenuCardView.Model {
struct Input {
let provider: UsageProvider
let metadata: ProviderMetadata
let snapshot: UsageSnapshot?
let credits: CreditsSnapshot?
let creditsError: String?
let account: AccountInfo
let isRefreshing: Bool
let lastError: String?
}
static func make(_ input: Input) -> UsageMenuCardView.Model {
let email = Self.email(
for: input.provider,
snapshot: input.snapshot,
account: input.account)
let planText = Self.plan(for: input.provider, snapshot: input.snapshot, account: input.account)
let metrics = Self.metrics(metadata: input.metadata, snapshot: input.snapshot)
let creditsText = Self.creditsLine(metadata: input.metadata, credits: input.credits, error: input.creditsError)
let subtitle = Self.subtitle(
snapshot: input.snapshot,
isRefreshing: input.isRefreshing,
lastError: input.lastError)
let placeholder = input.snapshot == nil && !input.isRefreshing && input.lastError == nil ? "No usage yet" : nil
return UsageMenuCardView.Model(
providerName: input.metadata.displayName,
email: email,
subtitleText: subtitle.text,
subtitleStyle: subtitle.style,
planText: planText,
metrics: metrics,
creditsText: creditsText,
placeholder: placeholder,
progressColor: Self.progressColor(for: input.provider))
}
private static func email(
for provider: UsageProvider,
snapshot: UsageSnapshot?,
account: AccountInfo) -> String
{
switch provider {
case .codex:
if let email = snapshot?.accountEmail, !email.isEmpty { return email }
if let email = account.email, !email.isEmpty { return email }
case .claude:
if let email = snapshot?.accountEmail, !email.isEmpty { return email }
}
return ""
}
private static func plan(for provider: UsageProvider, snapshot: UsageSnapshot?, account: AccountInfo) -> String? {
switch provider {
case .codex:
if let plan = snapshot?.loginMethod, !plan.isEmpty { return self.planDisplay(plan) }
if let plan = account.plan, !plan.isEmpty { return Self.planDisplay(plan) }
case .claude:
if let plan = snapshot?.loginMethod, !plan.isEmpty { return self.planDisplay(plan) }
}
return nil
}
private static func planDisplay(_ text: String) -> String {
let cleaned = UsageFormatter.cleanPlanName(text)
return cleaned.isEmpty ? text : cleaned
}
private static func subtitle(
snapshot: UsageSnapshot?,
isRefreshing: Bool,
lastError: String?) -> (text: String, style: SubtitleStyle)
{
if let lastError, !lastError.isEmpty {
return (UsageFormatter.truncatedSingleLine(lastError, max: 80), .error)
}
if isRefreshing, snapshot == nil {
return ("Refreshing...", .loading)
}
if let updated = snapshot?.updatedAt {
return (UsageFormatter.updatedString(from: updated), .info)
}
return ("Not fetched yet", .info)
}
private static func metrics(metadata: ProviderMetadata, snapshot: UsageSnapshot?) -> [Metric] {
guard let snapshot else { return [] }
var metrics: [Metric] = []
metrics.append(Metric(
id: "primary",
title: metadata.sessionLabel,
percentLeft: Self.clamped(snapshot.primary.remainingPercent),
resetText: Self.resetText(for: snapshot.primary)))
metrics.append(Metric(
id: "secondary",
title: metadata.weeklyLabel,
percentLeft: Self.clamped(snapshot.secondary.remainingPercent),
resetText: Self.resetText(for: snapshot.secondary)))
if metadata.supportsOpus, let opus = snapshot.tertiary {
metrics.append(Metric(
id: "tertiary",
title: metadata.opusLabel ?? "Sonnet",
percentLeft: Self.clamped(opus.remainingPercent),
resetText: Self.resetText(for: opus)))
}
return metrics
}
private static func creditsLine(
metadata: ProviderMetadata,
credits: CreditsSnapshot?,
error: String?) -> String?
{
guard metadata.supportsCredits else { return nil }
if let credits {
return UsageFormatter.creditsString(from: credits.remaining)
}
if let error, !error.isEmpty {
return UsageFormatter.truncatedSingleLine(error, max: 80)
}
return metadata.creditsHint
}
private static func clamped(_ value: Double) -> Double {
min(100, max(0, value))
}
private static func progressColor(for provider: UsageProvider) -> Color {
switch provider {
case .codex:
Color(red: 73 / 255, green: 163 / 255, blue: 176 / 255)
case .claude:
Color(red: 204 / 255, green: 124 / 255, blue: 94 / 255)
}
}
private static func resetText(for window: RateWindow) -> String? {
if let date = window.resetsAt {
return "Resets \(UsageFormatter.resetDescription(from: date))"
}
if let desc = window.resetDescription, !desc.isEmpty {
return desc
}
return nil
}
}