Skip to content

Commit 7b95964

Browse files
johnlarkin1claude
andcommitted
feat: add session/weekly time window selection for menu bar percent and pace
Restores the time-window picker dropped during the upstream 0.31.1 sync. Lets the percent and pace values in the menu bar each track either the session or the weekly window independently. - MenuBarTimeWindow enum (session/weekly) - Two persisted settings with backward-compatible defaults (percent=session, pace=weekly) - Segmented pickers in the Display preferences pane, gated on display mode - StatusItemController selects the percent/pace window per setting (session -> menuBarPercentWindow / UsagePaceText.sessionPace, weekly -> secondary / UsageStore.weeklyPace) - Round-trip test for the new settings Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7866667 commit 7b95964

8 files changed

Lines changed: 151 additions & 8 deletions

Sources/CodexBar/MenuBarDisplayMode.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,20 @@ enum MenuBarDisplayMode: String, CaseIterable, Identifiable {
2626
}
2727
}
2828
}
29+
30+
/// Controls which time window drives the percent and pace values in the menu bar.
31+
enum MenuBarTimeWindow: String, CaseIterable, Identifiable {
32+
case session
33+
case weekly
34+
35+
var id: String {
36+
self.rawValue
37+
}
38+
39+
var label: String {
40+
switch self {
41+
case .session: "Session"
42+
case .weekly: "Weekly"
43+
}
44+
}
45+
}

Sources/CodexBar/PreferencesDisplayPane.swift

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,51 @@ struct DisplayPane: View {
8787
self.settings.menuBarDisplayMode != .both)
8888
.opacity(self.settings.menuBarShowsBrandIconWithPercent &&
8989
self.settings.menuBarDisplayMode == .both ? 1 : 0.5)
90+
VStack(alignment: .leading, spacing: 4) {
91+
Text("Time windows")
92+
.font(.body)
93+
Text("Choose which time window drives the percent and pace values.")
94+
.font(.footnote)
95+
.foregroundStyle(.tertiary)
96+
Grid(alignment: .leading, horizontalSpacing: 8, verticalSpacing: 6) {
97+
GridRow {
98+
Text("Percent:")
99+
.font(.callout)
100+
Picker(
101+
"Percent time window",
102+
selection: self.$settings.menuBarPercentTimeWindow)
103+
{
104+
ForEach(MenuBarTimeWindow.allCases) { window in
105+
Text(window.label).tag(window)
106+
}
107+
}
108+
.labelsHidden()
109+
.pickerStyle(.segmented)
110+
.frame(maxWidth: 160)
111+
}
112+
.disabled(self.settings.menuBarDisplayMode == .pace)
113+
.opacity(self.settings.menuBarDisplayMode == .pace ? 0.5 : 1)
114+
GridRow {
115+
Text("Pace:")
116+
.font(.callout)
117+
Picker(
118+
"Pace time window",
119+
selection: self.$settings.menuBarPaceTimeWindow)
120+
{
121+
ForEach(MenuBarTimeWindow.allCases) { window in
122+
Text(window.label).tag(window)
123+
}
124+
}
125+
.labelsHidden()
126+
.pickerStyle(.segmented)
127+
.frame(maxWidth: 160)
128+
}
129+
.disabled(self.settings.menuBarDisplayMode == .percent)
130+
.opacity(self.settings.menuBarDisplayMode == .percent ? 0.5 : 1)
131+
}
132+
}
133+
.disabled(!self.settings.menuBarShowsBrandIconWithPercent)
134+
.opacity(self.settings.menuBarShowsBrandIconWithPercent ? 1 : 0.5)
90135
}
91136

92137
Divider()

Sources/CodexBar/SettingsStore+Defaults.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,40 @@ extension SettingsStore {
259259
set { self.menuBarSeparatorStyleRaw = newValue.rawValue }
260260
}
261261

262+
private var menuBarPercentTimeWindowRaw: String? {
263+
get { self.defaultsState.menuBarPercentTimeWindowRaw }
264+
set {
265+
self.defaultsState.menuBarPercentTimeWindowRaw = newValue
266+
if let raw = newValue {
267+
self.userDefaults.set(raw, forKey: "menuBarPercentTimeWindow")
268+
} else {
269+
self.userDefaults.removeObject(forKey: "menuBarPercentTimeWindow")
270+
}
271+
}
272+
}
273+
274+
var menuBarPercentTimeWindow: MenuBarTimeWindow {
275+
get { MenuBarTimeWindow(rawValue: self.menuBarPercentTimeWindowRaw ?? "") ?? .session }
276+
set { self.menuBarPercentTimeWindowRaw = newValue.rawValue }
277+
}
278+
279+
private var menuBarPaceTimeWindowRaw: String? {
280+
get { self.defaultsState.menuBarPaceTimeWindowRaw }
281+
set {
282+
self.defaultsState.menuBarPaceTimeWindowRaw = newValue
283+
if let raw = newValue {
284+
self.userDefaults.set(raw, forKey: "menuBarPaceTimeWindow")
285+
} else {
286+
self.userDefaults.removeObject(forKey: "menuBarPaceTimeWindow")
287+
}
288+
}
289+
}
290+
291+
var menuBarPaceTimeWindow: MenuBarTimeWindow {
292+
get { MenuBarTimeWindow(rawValue: self.menuBarPaceTimeWindowRaw ?? "") ?? .weekly }
293+
set { self.menuBarPaceTimeWindowRaw = newValue.rawValue }
294+
}
295+
262296
private var kiroMenuBarDisplayModeRaw: String? {
263297
get { self.defaultsState.kiroMenuBarDisplayModeRaw }
264298
set {

Sources/CodexBar/SettingsStore+MenuObservation.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ extension SettingsStore {
2727
_ = self.menuBarShowsHighestUsage
2828
_ = self.menuBarDisplayMode
2929
_ = self.menuBarSeparatorStyle
30+
_ = self.menuBarPercentTimeWindow
31+
_ = self.menuBarPaceTimeWindow
3032
_ = self.kiroMenuBarDisplayMode
3133
_ = self.historicalTrackingEnabled
3234
_ = self.multiAccountMenuLayout

Sources/CodexBar/SettingsStore.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,10 @@ extension SettingsStore {
335335
?? MenuBarDisplayMode.percent.rawValue
336336
let menuBarSeparatorStyleRaw = userDefaults.string(forKey: "menuBarSeparatorStyle")
337337
?? MenuBarSeparatorStyle.dot.rawValue
338+
let menuBarPercentTimeWindowRaw = userDefaults.string(forKey: "menuBarPercentTimeWindow")
339+
?? MenuBarTimeWindow.session.rawValue
340+
let menuBarPaceTimeWindowRaw = userDefaults.string(forKey: "menuBarPaceTimeWindow")
341+
?? MenuBarTimeWindow.weekly.rawValue
338342
let kiroMenuBarDisplayModeRaw = userDefaults.string(forKey: "kiroMenuBarDisplayMode")
339343
?? KiroMenuBarDisplayMode.automatic.rawValue
340344
let historicalTrackingEnabled = userDefaults.object(forKey: "historicalTrackingEnabled") as? Bool ?? false
@@ -412,6 +416,8 @@ extension SettingsStore {
412416
menuBarShowsBrandIconWithPercent: menuBarShowsBrandIconWithPercent,
413417
menuBarDisplayModeRaw: menuBarDisplayModeRaw,
414418
menuBarSeparatorStyleRaw: menuBarSeparatorStyleRaw,
419+
menuBarPercentTimeWindowRaw: menuBarPercentTimeWindowRaw,
420+
menuBarPaceTimeWindowRaw: menuBarPaceTimeWindowRaw,
415421
kiroMenuBarDisplayModeRaw: kiroMenuBarDisplayModeRaw,
416422
historicalTrackingEnabled: historicalTrackingEnabled,
417423
multiAccountMenuLayoutRaw: multiAccountMenuLayoutRaw,

Sources/CodexBar/SettingsStoreState.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ struct SettingsDefaultsState {
2626
var menuBarShowsBrandIconWithPercent: Bool
2727
var menuBarDisplayModeRaw: String?
2828
var menuBarSeparatorStyleRaw: String?
29+
var menuBarPercentTimeWindowRaw: String?
30+
var menuBarPaceTimeWindowRaw: String?
2931
var kiroMenuBarDisplayModeRaw: String?
3032
var historicalTrackingEnabled: Bool
3133
var multiAccountMenuLayoutRaw: String

Sources/CodexBar/StatusItemController+Animation.swift

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,12 @@ extension StatusItemController {
708708
return spend
709709
}
710710

711-
let percentWindow = self.menuBarPercentWindow(for: provider, snapshot: snapshot)
711+
let percentWindow: RateWindow? = switch self.settings.menuBarPercentTimeWindow {
712+
case .session:
713+
self.menuBarPercentWindow(for: provider, snapshot: snapshot)
714+
case .weekly:
715+
snapshot?.secondary ?? self.menuBarPercentWindow(for: provider, snapshot: snapshot)
716+
}
712717
let mode = self.settings.menuBarDisplayMode
713718
let now = Date()
714719
let codexProjection = self.store.codexConsumerProjectionIfNeeded(
@@ -721,13 +726,21 @@ extension StatusItemController {
721726
case .percent:
722727
pace = nil
723728
case .pace, .both:
724-
let weeklyWindow =
725-
codexProjection?.rateWindow(for: .weekly)
726-
?? snapshot?.secondary
727-
// Abacus has no secondary window; pace is computed on primary monthly credits
728-
?? (provider == .abacus ? snapshot?.primary : nil)
729-
pace = weeklyWindow.flatMap { window in
730-
self.store.weeklyPace(provider: provider, window: window, now: now)
729+
switch self.settings.menuBarPaceTimeWindow {
730+
case .session:
731+
let sessionWindow = snapshot?.primary ?? snapshot?.secondary
732+
pace = sessionWindow.flatMap { window in
733+
UsagePaceText.sessionPace(provider: provider, window: window, now: now)
734+
}
735+
case .weekly:
736+
let weeklyWindow =
737+
codexProjection?.rateWindow(for: .weekly)
738+
?? snapshot?.secondary
739+
// Abacus has no secondary window; pace is computed on primary monthly credits
740+
?? (provider == .abacus ? snapshot?.primary : nil)
741+
pace = weeklyWindow.flatMap { window in
742+
self.store.weeklyPace(provider: provider, window: window, now: now)
743+
}
731744
}
732745
}
733746
let displayText = MenuBarDisplayText.displayText(

Tests/CodexBarTests/StatusItemAnimationTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,4 +1103,28 @@ struct StatusItemAnimationTests {
11031103
#expect(baselineAlpha < 0.01)
11041104
#expect(outputAlpha > 0.01)
11051105
}
1106+
1107+
@Test
1108+
func `menu bar time window settings round trip`() {
1109+
// These settings persist to UserDefaults.standard; clear leftovers so the defaults check is meaningful.
1110+
UserDefaults.standard.removeObject(forKey: "menuBarPercentTimeWindow")
1111+
UserDefaults.standard.removeObject(forKey: "menuBarPaceTimeWindow")
1112+
1113+
let settings = SettingsStore(
1114+
configStore: testConfigStore(suiteName: "StatusItemAnimationTests-timewindow"),
1115+
zaiTokenStore: NoopZaiTokenStore())
1116+
1117+
// Backward-compatible defaults: percent tracks the session, pace tracks the week.
1118+
#expect(settings.menuBarPercentTimeWindow == .session)
1119+
#expect(settings.menuBarPaceTimeWindow == .weekly)
1120+
1121+
settings.menuBarPercentTimeWindow = .weekly
1122+
settings.menuBarPaceTimeWindow = .session
1123+
1124+
#expect(settings.menuBarPercentTimeWindow == .weekly)
1125+
#expect(settings.menuBarPaceTimeWindow == .session)
1126+
1127+
#expect(settings.userDefaults.string(forKey: "menuBarPercentTimeWindow") == "weekly")
1128+
#expect(settings.userDefaults.string(forKey: "menuBarPaceTimeWindow") == "session")
1129+
}
11061130
}

0 commit comments

Comments
 (0)