Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 0.29.1 — Unreleased

### Added
- Display: add optional workday markers for weekly progress bars (#1102). Thanks @Yuxin-Qiao!

### Fixed
- Menu bar: show extra-usage spend as currency text for Claude and Cursor when that metric is selected (#1107). Thanks @Yuxin-Qiao!
Expand Down
59 changes: 59 additions & 0 deletions Sources/CodexBar/MenuCardQuotaWarningMarkers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,63 @@ extension UsageMenuCardView.Model {
.map { showUsed ? 100 - Double($0) : Double($0) }
.filter { $0 > 0 && $0 < 100 }
}

/// Merges quota warning markers with optional work-day boundary markers.
/// Preserves original warning-marker ordering when workdayMarkers is empty,
/// sorts the combined set when workday markers are present.
static func mergedMarkerPercents(
warningMarkers: [Double],
workdayMarkers: [Double]) -> [Double]
{
let combined = warningMarkers + workdayMarkers
return workdayMarkers.isEmpty ? combined : combined.sorted()
}

/// Combines quota warning markers with optional work-day boundary markers
/// into a single sorted array. Workday markers are only applied when
/// includeWorkdayMarkers is true and windowMinutes == 10080.
static func markerPercents(
thresholds: [Int]?,
showUsed: Bool,
workDays: Int?,
windowMinutes: Int?,
includeWorkdayMarkers: Bool) -> [Double]
{
let warningMarkers = Self.warningMarkerPercents(thresholds: thresholds, showUsed: showUsed)
let workdayMarkers = includeWorkdayMarkers
? workDayMarkerPercents(workDays: workDays, windowMinutes: windowMinutes)
: []
return Self.mergedMarkerPercents(warningMarkers: warningMarkers, workdayMarkers: workdayMarkers)
}

static func weeklyMarkerPercents(input: Input, windowMinutes: Int?) -> [Double] {
UsageMenuCardView.Model.markerPercents(
thresholds: input.quotaWarningThresholds[.weekly],
showUsed: input.usageBarsShowUsed,
workDays: input.workDaysPerWeek,
windowMinutes: windowMinutes,
includeWorkdayMarkers: true)
}

static func codexLaneMarkerPercents(
input: Input,
lane: CodexConsumerProjection.RateLane,
windowMinutes: Int?) -> [Double]
{
UsageMenuCardView.Model.markerPercents(
thresholds: input.quotaWarningThresholds[lane.quotaWarningWindow],
showUsed: input.usageBarsShowUsed,
workDays: input.workDaysPerWeek,
windowMinutes: windowMinutes,
includeWorkdayMarkers: lane == .weekly)
}
}

/// Returns boundary percentages for work day markers on a weekly progress bar.
/// Only valid when windowMinutes == 10080 (standard 7-day week).
/// nil workDays means feature is disabled.
func workDayMarkerPercents(workDays: Int?, windowMinutes: Int?) -> [Double] {
guard workDays != nil, windowMinutes == 10080 else { return [] }
guard let wd = workDays, wd >= 2, wd <= 7 else { return [] }
return (1..<wd).map { Double($0) * 100.0 / Double(wd) }
}
14 changes: 8 additions & 6 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,7 @@ extension UsageMenuCardView.Model {
let hidePersonalInfo: Bool
let weeklyPace: UsagePace?
let quotaWarningThresholds: [QuotaWarningWindow: [Int]]
let workDaysPerWeek: Int?
let now: Date

init(
Expand All @@ -722,6 +723,7 @@ extension UsageMenuCardView.Model {
hidePersonalInfo: Bool,
weeklyPace: UsagePace? = nil,
quotaWarningThresholds: [QuotaWarningWindow: [Int]] = [:],
workDaysPerWeek: Int? = nil,
now: Date)
{
self.provider = provider
Expand All @@ -746,6 +748,7 @@ extension UsageMenuCardView.Model {
self.hidePersonalInfo = hidePersonalInfo
self.weeklyPace = weeklyPace
self.quotaWarningThresholds = quotaWarningThresholds
self.workDaysPerWeek = workDaysPerWeek
self.now = now
}
}
Expand Down Expand Up @@ -1383,9 +1386,7 @@ extension UsageMenuCardView.Model {
detailRightText: paceDetail?.rightLabel,
pacePercent: paceDetail?.pacePercent,
paceOnTop: paceDetail?.paceOnTop ?? true,
warningMarkerPercents: Self.warningMarkerPercents(
thresholds: input.quotaWarningThresholds[.weekly],
showUsed: input.usageBarsShowUsed))
warningMarkerPercents: Self.weeklyMarkerPercents(input: input, windowMinutes: weekly.windowMinutes))
}

private static func codexRateMetrics(
Expand Down Expand Up @@ -1431,9 +1432,10 @@ extension UsageMenuCardView.Model {
detailRightText: paceDetail?.rightLabel,
pacePercent: paceDetail?.pacePercent,
paceOnTop: paceDetail?.paceOnTop ?? true,
warningMarkerPercents: Self.warningMarkerPercents(
thresholds: input.quotaWarningThresholds[lane.quotaWarningWindow],
showUsed: input.usageBarsShowUsed))
warningMarkerPercents: Self.codexLaneMarkerPercents(
input: input,
lane: lane,
windowMinutes: window.windowMinutes))
}
}

Expand Down
19 changes: 19 additions & 0 deletions Sources/CodexBar/PreferencesDisplayPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ struct DisplayPane: View {
title: L("show_quota_warning_markers_title"),
subtitle: L("show_quota_warning_markers_subtitle"),
binding: self.$settings.quotaWarningMarkersVisible)
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text(L("weekly_progress_work_days_title"))
.font(.body)
Text(L("weekly_progress_work_days_subtitle"))
.font(.footnote)
.foregroundStyle(.tertiary)
}
Spacer()
Picker(L("weekly_progress_work_days_title"), selection: self.$settings.weeklyProgressWorkDays) {
Text("Off").tag(nil as Int?)
Text("4 days").tag(4 as Int?)
Text("5 days").tag(5 as Int?)
Text("7 days").tag(7 as Int?)
}
.labelsHidden()
.pickerStyle(.menu)
.frame(maxWidth: 100)
}
PreferenceToggleRow(
title: L("show_reset_time_as_clock_title"),
subtitle: L("show_reset_time_as_clock_subtitle"),
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/PreferencesProvidersPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,7 @@ struct ProvidersPane: View {
.session: self.quotaWarningMarkerThresholds(provider: provider, window: .session),
.weekly: self.quotaWarningMarkerThresholds(provider: provider, window: .weekly),
],
workDaysPerWeek: self.settings.weeklyProgressWorkDays,
now: now)
return UsageMenuCardView.Model.make(input)
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,8 @@
"show_usage_as_used_subtitle" = "Progress bars fill as you consume quota (instead of showing remaining).";
"show_quota_warning_markers_title" = "Show quota warning markers";
"show_quota_warning_markers_subtitle" = "Draw threshold tick marks on usage bars when quota warnings are configured.";
"weekly_progress_work_days_title" = "Weekly progress work days";
"weekly_progress_work_days_subtitle" = "Draw day-boundary tick marks on weekly usage bars.";
"show_reset_time_as_clock_title" = "Show reset time as clock";
"show_reset_time_as_clock_subtitle" = "Display reset times as absolute clock values instead of countdowns.";
"show_provider_changelog_links_title" = "Show provider changelog links";
Expand Down
12 changes: 12 additions & 0 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,18 @@ extension SettingsStore {
}
}

var weeklyProgressWorkDays: Int? {
get { self.defaultsState.weeklyProgressWorkDays }
set {
self.defaultsState.weeklyProgressWorkDays = newValue
if let newValue {
self.userDefaults.set(newValue, forKey: "weeklyProgressWorkDays")
} else {
self.userDefaults.removeObject(forKey: "weeklyProgressWorkDays")
}
}
}

var usageBarsShowUsed: Bool {
get { self.defaultsState.usageBarsShowUsed }
set {
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStore+MenuObservation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ extension SettingsStore {
_ = self.quotaWarningWindowEnabled(.weekly)
_ = self.quotaWarningSoundEnabled
_ = self.quotaWarningMarkersVisible
_ = self.weeklyProgressWorkDays
_ = self.usageBarsShowUsed
_ = self.resetTimesShowAbsolute
_ = self.providerChangelogLinksEnabled
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ extension SettingsStore {
if Self.isRunningTests, quotaWarningMarkersVisibleDefault == nil {
userDefaults.set(true, forKey: "quotaWarningMarkersVisible")
}
let weeklyProgressWorkDays = userDefaults.object(forKey: "weeklyProgressWorkDays") as? Int
let usageBarsShowUsed = userDefaults.object(forKey: "usageBarsShowUsed") as? Bool ?? false
let resetTimesShowAbsolute = userDefaults.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false
let providerChangelogLinksEnabled = userDefaults.object(
Expand Down Expand Up @@ -400,6 +401,7 @@ extension SettingsStore {
quotaWarningWeeklyEnabled: quotaWarnings.weeklyEnabled,
quotaWarningSoundEnabled: quotaWarnings.soundEnabled,
quotaWarningMarkersVisible: quotaWarningMarkersVisible,
weeklyProgressWorkDays: weeklyProgressWorkDays,
usageBarsShowUsed: usageBarsShowUsed,
resetTimesShowAbsolute: resetTimesShowAbsolute,
providerChangelogLinksEnabled: providerChangelogLinksEnabled,
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStoreState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ struct SettingsDefaultsState {
var quotaWarningWeeklyEnabled: Bool
var quotaWarningSoundEnabled: Bool
var quotaWarningMarkersVisible: Bool
var weeklyProgressWorkDays: Int?
var usageBarsShowUsed: Bool
var resetTimesShowAbsolute: Bool
var providerChangelogLinksEnabled: Bool
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/StatusItemController+MenuCardModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ extension StatusItemController {
.session: self.quotaWarningMarkerThresholds(provider: target, window: .session),
.weekly: self.quotaWarningMarkerThresholds(provider: target, window: .weekly),
],
workDaysPerWeek: self.settings.weeklyProgressWorkDays,
now: now)
return UsageMenuCardView.Model.make(input)
}
Expand Down
Loading