Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ClaudeMeter/Models/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ struct AppSettings: Codable, Equatable, Sendable {
/// Whether to show Sonnet usage in the popover
var isSonnetUsageShown: Bool

/// Whether to show the exact reset time alongside the relative description
var isResetTimeShown: Bool

/// Menu bar icon display style
var iconStyle: IconStyle

Expand All @@ -40,6 +43,7 @@ struct AppSettings: Codable, Equatable, Sendable {
isFirstLaunch: true,
cachedOrganizationId: nil,
isSonnetUsageShown: false,
isResetTimeShown: true,
iconStyle: .battery,
isColoredIcon: true
)
Expand All @@ -51,6 +55,7 @@ struct AppSettings: Codable, Equatable, Sendable {
case isFirstLaunch = "is_first_launch"
case cachedOrganizationId = "cached_organization_id"
case isSonnetUsageShown = "show_sonnet_usage"
case isResetTimeShown = "show_reset_time"
case iconStyle = "icon_style"
case isColoredIcon = "is_colored_icon"
}
Expand All @@ -67,6 +72,7 @@ extension AppSettings {
isFirstLaunch = try container.decodeIfPresent(Bool.self, forKey: .isFirstLaunch) ?? defaults.isFirstLaunch
cachedOrganizationId = try container.decodeIfPresent(UUID.self, forKey: .cachedOrganizationId)
isSonnetUsageShown = try container.decodeIfPresent(Bool.self, forKey: .isSonnetUsageShown) ?? defaults.isSonnetUsageShown
isResetTimeShown = try container.decodeIfPresent(Bool.self, forKey: .isResetTimeShown) ?? defaults.isResetTimeShown
iconStyle = try container.decodeIfPresent(IconStyle.self, forKey: .iconStyle) ?? defaults.iconStyle
isColoredIcon = try container.decodeIfPresent(Bool.self, forKey: .isColoredIcon) ?? defaults.isColoredIcon
}
Expand Down
12 changes: 10 additions & 2 deletions ClaudeMeter/Models/UsageLimit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,18 @@ extension UsageLimit {
count == 1 ? singular : "\(singular)s"
}

/// Exact reset time formatted in user's timezone for tooltip display
/// Exact reset time formatted in user's timezone for tooltip display (no year)
var resetTimeFormatted: String {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeZone = .current
formatter.setLocalizedDateFormatFromTemplate("MMMdjmm")
return formatter.string(from: resetAt)
}

/// Exact reset time-of-day only (no date), in the user's timezone.
var resetTimeOnlyFormatted: String {
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
formatter.timeZone = .current
return formatter.string(from: resetAt)
Expand Down
22 changes: 20 additions & 2 deletions ClaudeMeter/Views/MenuBar/UsageCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,24 @@ struct UsageCardView: View {
let icon: String
let windowDuration: TimeInterval?

/// Whether to append the exact reset time in parentheses.
var showsExactResetTime: Bool = true

/// When true, the exact reset time shows the time of day only (no date).
var usesTimeOnlyResetTimestamp: Bool = false

/// Exact reset time string, time-only or date+time depending on the card.
private var exactResetTime: String {
usesTimeOnlyResetTimestamp ? usageLimit.resetTimeOnlyFormatted : usageLimit.resetTimeFormatted
}

/// Reset label, optionally with the exact time appended in parentheses.
private var resetLabel: String {
showsExactResetTime
? "Resets \(usageLimit.resetDescription) (\(exactResetTime))"
: "Resets \(usageLimit.resetDescription)"
}

var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Header with icon and title
Expand Down Expand Up @@ -68,7 +86,7 @@ struct UsageCardView: View {
HStack(spacing: 4) {
Image(systemName: "clock")
.font(.caption)
Text("Resets \(usageLimit.resetDescription)")
Text(resetLabel)
.font(.caption)
}
.help(usageLimit.resetTimeFormatted)
Expand All @@ -91,7 +109,7 @@ struct UsageCardView: View {
.cornerRadius(12)
.accessibilityElement(children: .combine)
.accessibilityLabel("\(title): \(Int(usageLimit.percentage))% used, \(usageLimit.status.accessibilityDescription)")
.accessibilityValue("Resets \(usageLimit.resetDescription)")
.accessibilityValue(resetLabel)
}
}

Expand Down
10 changes: 7 additions & 3 deletions ClaudeMeter/Views/MenuBar/UsagePopoverView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,18 @@ struct UsagePopoverView: View {
title: "5-Hour Session",
usageLimit: usageData.sessionUsage,
icon: "gauge.with.dots.needle.67percent",
windowDuration: Constants.Pacing.sessionWindow
windowDuration: Constants.Pacing.sessionWindow,
showsExactResetTime: appModel.settings.isResetTimeShown,
usesTimeOnlyResetTimestamp: true
)

// Weekly usage card
UsageCardView(
title: "Weekly Usage",
usageLimit: usageData.weeklyUsage,
icon: "calendar",
windowDuration: Constants.Pacing.weeklyWindow
windowDuration: Constants.Pacing.weeklyWindow,
showsExactResetTime: appModel.settings.isResetTimeShown
)

// Sonnet usage card (conditional rendering)
Expand All @@ -109,7 +112,8 @@ struct UsagePopoverView: View {
title: "Weekly Sonnet",
usageLimit: sonnetUsage,
icon: "sparkles",
windowDuration: Constants.Pacing.weeklyWindow
windowDuration: Constants.Pacing.weeklyWindow,
showsExactResetTime: appModel.settings.isResetTimeShown
)
}
}
Expand Down
23 changes: 23 additions & 0 deletions ClaudeMeter/Views/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ struct SettingsView: View {
sessionKeySection
refreshIntervalSection
sonnetUsageSection
resetTimeSection
iconStyleSection
launchAtLoginSection
}
Expand Down Expand Up @@ -232,6 +233,28 @@ struct SettingsView: View {
.clipShape(RoundedRectangle(cornerRadius: 8))
}

// MARK: - Reset Time Section

private var resetTimeSection: some View {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Show Reset Time")
.font(.subheadline)
Text("Display the exact time each limit resets in the menu bar popover")
.font(.caption)
.foregroundStyle(.secondary)
}

Spacer()

Toggle("", isOn: $appModel.settings.isResetTimeShown)
.labelsHidden()
}
.padding()
.background(.quaternary.opacity(0.3))
.clipShape(RoundedRectangle(cornerRadius: 8))
}

// MARK: - Icon Style Section

private var iconStyleSection: some View {
Expand Down