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 @@ -39,6 +39,7 @@
- Preferences: add per-provider menu bar metric picker (#185) — thanks @HaukeSchnau
- Preferences: tighten provider rows (inline pickers, compact layout, inline refresh + auto-source status).
- Preferences: remove the “experimental” label from Antigravity.
- Menu bar: add display mode picker for percent/pace/both in the menu bar icon (#169). Thanks @PhilETaylor!
- Menu bar: fix combined loading indicator flicker during loading animation (incl. debug replay).
- Menu bar: prevent blink updates from clobbering the loading animation.

Expand Down
26 changes: 26 additions & 0 deletions Sources/CodexBar/MenuBarDisplayMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Foundation

/// Controls what the menu bar displays when brand icon mode is enabled.
enum MenuBarDisplayMode: String, CaseIterable, Identifiable {
case percent
case pace
case both

var id: String { self.rawValue }

var label: String {
switch self {
case .percent: "Percent"
case .pace: "Pace"
case .both: "Both"
}
}

var description: String {
switch self {
case .percent: "Show remaining/used percentage (e.g. 45%)"
case .pace: "Show pace indicator (e.g. +5%)"
case .both: "Show both percentage and pace (e.g. 45% · +5%)"
}
}
}
20 changes: 20 additions & 0 deletions Sources/CodexBar/PreferencesAdvancedPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,26 @@ struct AdvancedPane: View {
title: "Menu bar shows percent",
subtitle: "Replace critter bars with provider branding icons and a percentage.",
binding: self.$settings.menuBarShowsBrandIconWithPercent)
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("Display mode")
.font(.body)
Text("Choose what to show in the menu bar (Pace shows usage vs. expected).")
.font(.footnote)
.foregroundStyle(.tertiary)
}
Spacer()
Picker("Display mode", selection: self.$settings.menuBarDisplayMode) {
ForEach(MenuBarDisplayMode.allCases) { mode in
Text(mode.label).tag(mode)
}
}
.labelsHidden()
.pickerStyle(.menu)
.frame(maxWidth: 200)
}
.disabled(!self.settings.menuBarShowsBrandIconWithPercent)
.opacity(self.settings.menuBarShowsBrandIconWithPercent ? 1 : 0.5)
PreferenceToggleRow(
title: "Show all token accounts",
subtitle: "Stack token accounts in the menu (otherwise show an account switcher bar).",
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 @@ -12,6 +12,7 @@ extension SettingsStore {
_ = self.usageBarsShowUsed
_ = self.resetTimesShowAbsolute
_ = self.menuBarShowsBrandIconWithPercent
_ = self.menuBarDisplayMode
_ = self.showAllTokenAccountsInMenu
_ = self.menuBarMetricPreferencesRaw
_ = self.costUsageEnabled
Expand Down
20 changes: 20 additions & 0 deletions Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ final class SettingsStore {
}
}

/// Controls what the menu bar displays when brand icon mode is enabled.
private var menuBarDisplayModeRaw: String? {
didSet {
if let raw = self.menuBarDisplayModeRaw {
self.userDefaults.set(raw, forKey: "menuBarDisplayMode")
} else {
self.userDefaults.removeObject(forKey: "menuBarDisplayMode")
}
}
}

/// Optional: show all token accounts stacked in the menu (otherwise show a switcher bar).
var showAllTokenAccountsInMenu: Bool {
didSet { self.userDefaults.set(self.showAllTokenAccountsInMenu, forKey: "showAllTokenAccountsInMenu") }
Expand Down Expand Up @@ -669,6 +680,8 @@ final class SettingsStore {
self.resetTimesShowAbsolute = userDefaults.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false
self.menuBarShowsBrandIconWithPercent = userDefaults.object(
forKey: "menuBarShowsBrandIconWithPercent") as? Bool ?? false
self.menuBarDisplayModeRaw = userDefaults.string(forKey: "menuBarDisplayMode")
?? MenuBarDisplayMode.percent.rawValue
self.showAllTokenAccountsInMenu = userDefaults.object(
forKey: "showAllTokenAccountsInMenu") as? Bool ?? false
let storedMenuBarMetricPreferences = userDefaults.dictionary(
Expand Down Expand Up @@ -1653,6 +1666,13 @@ extension SettingsStore {
}
}

extension SettingsStore {
var menuBarDisplayMode: MenuBarDisplayMode {
get { MenuBarDisplayMode(rawValue: self.menuBarDisplayModeRaw ?? "") ?? .percent }
set { self.menuBarDisplayModeRaw = newValue.rawValue }
}
}

enum LaunchAtLoginManager {
@MainActor
static func setEnabled(_ enabled: Bool) {
Expand Down
36 changes: 32 additions & 4 deletions Sources/CodexBar/StatusItemController+Animation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,9 @@ extension StatusItemController {
if showBrandPercent,
let brand = ProviderBrandIcon.image(for: primaryProvider)
{
let percentText = self.menuBarPercentText(for: primaryProvider, snapshot: snapshot)
let displayText = self.menuBarDisplayText(for: primaryProvider, snapshot: snapshot)
self.setButtonImage(brand, for: button)
self.setButtonTitle(percentText, for: button)
self.setButtonTitle(displayText, for: button)
return
}

Expand Down Expand Up @@ -272,9 +272,9 @@ extension StatusItemController {
if showBrandPercent,
let brand = ProviderBrandIcon.image(for: provider)
{
let percentText = self.menuBarPercentText(for: provider, snapshot: snapshot)
let displayText = self.menuBarDisplayText(for: provider, snapshot: snapshot)
self.setButtonImage(brand, for: button)
self.setButtonTitle(percentText, for: button)
self.setButtonTitle(displayText, for: button)
return
}
var primary = showUsed ? snapshot?.primary?.usedPercent : snapshot?.primary?.remainingPercent
Expand Down Expand Up @@ -349,6 +349,34 @@ extension StatusItemController {
return String(format: "%.0f%%", clamped)
}

private func menuBarPaceText(for provider: UsageProvider, snapshot: UsageSnapshot?) -> String? {
// PACE is calculated from the weekly (secondary) window, not the session (primary) window.
guard let window = snapshot?.secondary else { return nil }
guard let pace = UsagePaceText.weeklyPace(provider: provider, window: window, now: Date()) else { return nil }

let deltaValue = Int(abs(pace.deltaPercent).rounded())
let sign = pace.deltaPercent >= 0 ? "+" : "-"
return "\(sign)\(deltaValue)%"
}

func menuBarDisplayText(for provider: UsageProvider, snapshot: UsageSnapshot?) -> String? {
let mode = self.settings.menuBarDisplayMode

switch mode {
case .percent:
return self.menuBarPercentText(for: provider, snapshot: snapshot)
case .pace:
return self.menuBarPaceText(for: provider, snapshot: snapshot)
case .both:
let percentText = self.menuBarPercentText(for: provider, snapshot: snapshot)
let paceText = self.menuBarPaceText(for: provider, snapshot: snapshot)
if let percent = percentText, let pace = paceText {
return "\(percent) · \(pace)"
}
return nil
}
}

private func menuBarPercentWindow(for provider: UsageProvider, snapshot: UsageSnapshot?) -> RateWindow? {
self.menuBarMetricWindow(for: provider, snapshot: snapshot)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodexBar/UsagePaceText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ enum UsagePaceText {
return countdown
}

private static func weeklyPace(provider: UsageProvider, window: RateWindow, now: Date) -> UsagePace? {
static func weeklyPace(provider: UsageProvider, window: RateWindow, now: Date) -> UsagePace? {
guard provider == .codex || provider == .claude else { return nil }
guard window.remainingPercent > 0 else { return nil }
guard let pace = UsagePace.weekly(window: window, now: now, defaultWindowMinutes: 10080) else { return nil }
Expand Down