Skip to content
Closed
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
33 changes: 25 additions & 8 deletions Sources/CodexBar/PreferencesMenuBarPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import SwiftUI

@MainActor
struct MenuBarPane: View {
private static let maxOverviewProviders = SettingsStore.mergedOverviewProviderLimit

@State private var isOverviewProviderPopoverPresented = false
@Bindable var settings: SettingsStore
@Bindable var store: UsageStore

static func overviewProviderLimitText(limit: Int = Self.maxOverviewProviders) -> String {
static func overviewProviderLimitText(limit: Int) -> String {
L("overview_choose_providers", String(limit))
}

Expand Down Expand Up @@ -75,6 +73,25 @@ struct MenuBarPane: View {

self.overviewProviderRow
.disabled(!self.settings.mergeIcons)

LabeledContent {
HStack(spacing: 4) {
Text("\(self.settings.mergedOverviewProviderLimit)")
.foregroundStyle(.secondary)
Stepper(
value: self.$settings.mergedOverviewProviderLimit,
in: 1...12)
{
EmptyView()
}
.labelsHidden()
}
} label: {
SettingsRowLabel(
L("overview_provider_limit_title"),
subtitle: L("overview_provider_limit_subtitle"))
}
.disabled(!self.settings.mergeIcons)
} header: {
Text(L("section_combined_icon"))
}
Expand Down Expand Up @@ -135,7 +152,7 @@ struct MenuBarPane: View {

private var overviewProviderPopover: some View {
VStack(alignment: .leading, spacing: 10) {
Text(Self.overviewProviderLimitText())
Text(Self.overviewProviderLimitText(limit: self.settings.mergedOverviewProviderLimit))
.font(.headline)
Text(L("overview_rows_follow_order"))
.font(.footnote)
Expand All @@ -156,7 +173,7 @@ struct MenuBarPane: View {
.toggleStyle(.checkbox)
.disabled(
!self.overviewSelectedProviders.contains(provider) &&
self.overviewSelectedProviders.count >= Self.maxOverviewProviders)
self.overviewSelectedProviders.count >= self.settings.mergedOverviewProviderLimit)
}
}
}
Expand All @@ -173,7 +190,7 @@ struct MenuBarPane: View {
private var overviewSelectedProviders: [UsageProvider] {
self.settings.resolvedMergedOverviewProviders(
activeProviders: self.activeProvidersInOrder,
maxVisibleProviders: Self.maxOverviewProviders)
maxVisibleProviders: self.settings.mergedOverviewProviderLimit)
}

private var showsOverviewConfigureButton: Bool {
Expand All @@ -195,12 +212,12 @@ struct MenuBarPane: View {
provider: provider,
isSelected: isSelected,
activeProviders: self.activeProvidersInOrder,
maxVisibleProviders: Self.maxOverviewProviders)
maxVisibleProviders: self.settings.mergedOverviewProviderLimit)
}

private func reconcileOverviewSelection() {
_ = self.settings.reconcileMergedOverviewSelectedProviders(
activeProviders: self.activeProvidersInOrder,
maxVisibleProviders: Self.maxOverviewProviders)
maxVisibleProviders: self.settings.mergedOverviewProviderLimit)
}
}
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 @@ -583,6 +583,8 @@
"show_most_used_provider_subtitle" = "Menu bar auto-shows the provider closest to its rate limit.";
"display_mode_title" = "Display mode";
"display_mode_subtitle" = "Choose what to show in the menu bar (Pace shows usage vs. expected).";
"overview_provider_limit_title" = "Overview tab limit";
"overview_provider_limit_subtitle" = "How many providers can show at once (default 6).";
"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" = "Work days";
Expand Down
30 changes: 25 additions & 5 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -771,10 +771,10 @@ extension SettingsStore {
get {
Self.decodeProviders(
self.mergedOverviewSelectedProvidersRaw,
maxCount: Self.mergedOverviewProviderLimit)
maxCount: self.mergedOverviewProviderLimit)
}
set {
let normalized = Self.normalizeProviders(newValue, maxCount: Self.mergedOverviewProviderLimit)
let normalized = Self.normalizeProviders(newValue, maxCount: self.mergedOverviewProviderLimit)
self.mergedOverviewSelectedProvidersRaw = normalized.map(\.rawValue)
}
}
Expand Down Expand Up @@ -814,9 +814,29 @@ extension SettingsStore {
self.mergedOverviewSelectionEditedActiveProvidersRaw = nil
}

/// User-configurable cap on how many providers render in the merged-icon Overview tab,
/// persisted through this store's own `userDefaults` (not `.standard` directly) so isolated
/// test stores can observe and reset it independently. Bumps
/// `mergedOverviewProviderLimitRevision` so an already-open Overview menu picks up the change.
var mergedOverviewProviderLimit: Int {
get {
// `userDefaults` is `@ObservationIgnored`, so reading it directly registers no
// dependency — SwiftUI would never re-render a Stepper/Text bound to this property.
// Reading the tracked revision counter first gives the getter an observable
// dependency, matching the technique already used in menuObservationToken.
_ = self.mergedOverviewProviderLimitRevision
let stored = self.userDefaults.integer(forKey: "mergedOverviewProviderLimit")
return stored > 0 ? stored : Self.mergedOverviewProviderLimitDefault
}
set {
self.userDefaults.set(max(1, newValue), forKey: "mergedOverviewProviderLimit")
self.mergedOverviewProviderLimitRevision &+= 1
}
}

func resolvedMergedOverviewProviders(
activeProviders: [UsageProvider],
maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimit) -> [UsageProvider]
maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimitDefault) -> [UsageProvider]
{
guard maxVisibleProviders > 0 else { return [] }
let normalizedActive = Self.normalizeProviders(activeProviders)
Expand All @@ -836,7 +856,7 @@ extension SettingsStore {
@discardableResult
func reconcileMergedOverviewSelectedProviders(
activeProviders: [UsageProvider],
maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimit) -> [UsageProvider]
maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimitDefault) -> [UsageProvider]
{
guard maxVisibleProviders > 0 else {
self.clearMergedOverviewSelectionPreference()
Expand Down Expand Up @@ -873,7 +893,7 @@ extension SettingsStore {
provider: UsageProvider,
isSelected: Bool,
activeProviders: [UsageProvider],
maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimit) -> [UsageProvider]
maxVisibleProviders: Int = SettingsStore.mergedOverviewProviderLimitDefault) -> [UsageProvider]
{
guard maxVisibleProviders > 0 else {
self.clearMergedOverviewSelectionPreference()
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 @@ -84,6 +84,7 @@ extension SettingsStore {
_ = self.mergeIcons
_ = self.switcherShowsIcons
_ = self.mergedOverviewSelectedProviders
_ = self.mergedOverviewProviderLimitRevision
_ = self.zaiAPIToken
_ = self.syntheticAPIToken
_ = self.codexCookieHeader
Expand Down
10 changes: 9 additions & 1 deletion Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,15 @@ enum CodexAccountMenuProjectionRevalidationResult: Equatable {
@Observable
final class SettingsStore {
static let sharedDefaults = AppGroupSupport.sharedDefaults()
static let mergedOverviewProviderLimit = 6
/// Fallback used only by default-parameter expressions below, which Swift requires to be
/// self-independent (no access to `self.userDefaults`). Every real call site instead reads
/// the persisted, per-store value through the `mergedOverviewProviderLimit` instance accessor
/// in SettingsStore+Defaults.swift, so isolated test stores observe and reset it correctly.
static let mergedOverviewProviderLimitDefault = 6
/// Bumped whenever `mergedOverviewProviderLimit` changes through the instance accessor below,
/// so `menuObservationToken` can pick up the change on an already-open Overview menu — the
/// static var itself isn't `@Observable`-tracked.
var mergedOverviewProviderLimitRevision = 0
static let productionCodexAccountReconciliationSnapshotCacheInterval: TimeInterval = 2
static let isRunningTests: Bool = {
let env = ProcessInfo.processInfo.environment
Expand Down
4 changes: 2 additions & 2 deletions Sources/CodexBar/StatusItemController+Animation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1358,7 +1358,7 @@ extension StatusItemController {
let activeProviders = self.store.enabledProvidersForDisplay()
let overviewProviders = self.settings.resolvedMergedOverviewProviders(
activeProviders: activeProviders,
maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit)
maxVisibleProviders: self.settings.mergedOverviewProviderLimit)
if let highest = self.store.providerWithHighestUsage(candidateProviders: overviewProviders) {
return highest.provider
}
Expand All @@ -1372,7 +1372,7 @@ extension StatusItemController {
let enabledProviders = self.store.enabledProvidersForDisplay()
let overviewProviders = self.settings.resolvedMergedOverviewProviders(
activeProviders: enabledProviders,
maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit)
maxVisibleProviders: self.settings.mergedOverviewProviderLimit)
if let provider = overviewProviders.first(where: { self.store.isEnabled($0) }) {
return provider
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ extension StatusItemController {
let activeProviders = self.store.enabledProvidersForDisplay()
return self.settings.resolvedMergedOverviewProviders(
activeProviders: activeProviders,
maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit).contains(.codex)
maxVisibleProviders: self.settings.mergedOverviewProviderLimit).contains(.codex)
}

func observeMenuBarTimeEnvironmentChanges() {
Expand Down
7 changes: 3 additions & 4 deletions Sources/CodexBar/StatusItemController+Menu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import SwiftUI

extension StatusItemController {
static let menuCardBaseWidth: CGFloat = 310
private static let maxOverviewProviders = SettingsStore.mergedOverviewProviderLimit
static let overviewRowIdentifierPrefix = "overviewRow-"
static let persistentRefreshMenuItemID = "persistentRefreshAction"
private static let defaultMenuOpenRefreshDelay: Duration = .seconds(1.2)
Expand Down Expand Up @@ -598,7 +597,7 @@ extension StatusItemController {
private func addOverviewEmptyState(to menu: NSMenu, enabledProviders: [UsageProvider]) {
let resolvedProviders = self.settings.resolvedMergedOverviewProviders(
activeProviders: enabledProviders,
maxVisibleProviders: Self.maxOverviewProviders)
maxVisibleProviders: self.settings.mergedOverviewProviderLimit)
let message = resolvedProviders.isEmpty
? L("No providers selected for Overview.")
: L("No overview data available.")
Expand Down Expand Up @@ -1102,7 +1101,7 @@ extension StatusItemController {
private func includesOverviewTab(enabledProviders: [UsageProvider]) -> Bool {
!self.settings.resolvedMergedOverviewProviders(
activeProviders: enabledProviders,
maxVisibleProviders: Self.maxOverviewProviders).isEmpty
maxVisibleProviders: self.settings.mergedOverviewProviderLimit).isEmpty
}

private func resolvedSwitcherSelection(
Expand Down Expand Up @@ -1240,7 +1239,7 @@ extension StatusItemController {
{
return self.settings.resolvedMergedOverviewProviders(
activeProviders: enabledProviders,
maxVisibleProviders: Self.maxOverviewProviders)
maxVisibleProviders: self.settings.mergedOverviewProviderLimit)
}

if let provider = self.menuProvider(for: menu)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ extension StatusItemController {
private func includesOverviewTab(for providers: [UsageProvider]) -> Bool {
!self.settings.resolvedMergedOverviewProviders(
activeProviders: providers,
maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit).isEmpty
maxVisibleProviders: self.settings.mergedOverviewProviderLimit).isEmpty
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ extension StatusItemController {
if let mergedMenu = self.mergedMenu, menu !== mergedMenu { return false }
let providers = self.settings.resolvedMergedOverviewProviders(
activeProviders: self.store.enabledProvidersForDisplay(),
maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit)
maxVisibleProviders: self.settings.mergedOverviewProviderLimit)
return !providers.isEmpty && self.settings.mergedMenuLastSelectedWasOverview
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ extension StatusItemController {

let includesOverview = !self.settings.resolvedMergedOverviewProviders(
activeProviders: enabledProviders,
maxVisibleProviders: SettingsStore.mergedOverviewProviderLimit).isEmpty
maxVisibleProviders: self.settings.mergedOverviewProviderLimit).isEmpty
var selections = enabledProviders.map(ProviderSwitcherSelection.provider)
if includesOverview {
selections.insert(.overview, at: 0)
Expand Down
2 changes: 1 addition & 1 deletion Tests/CodexBarTests/PreferencesPaneSmokeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ struct PreferencesPaneSmokeTests {

@Test
func `overview provider limit text shows the configured maximum`() {
let text = MenuBarPane.overviewProviderLimitText()
let text = MenuBarPane.overviewProviderLimitText(limit: SettingsStore.mergedOverviewProviderLimitDefault)

#expect(text.contains("6"))
#expect(!text.contains("%@"))
Expand Down