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
14 changes: 14 additions & 0 deletions Sources/CodexBar/MenuBarDisplayText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@ import CodexBarCore
import Foundation

enum MenuBarDisplayText {
/// Returns a compact countdown string (e.g. "28m" or "2h 5m") until the next quota reset.
/// Returns nil if `resetsAt` is nil or already in the past.
static func timeUntilResetText(resetsAt: Date?, now: Date = .init()) -> String? {
guard let resetsAt, resetsAt > now else { return nil }
let totalSeconds = Int(resetsAt.timeIntervalSince(now))
let hours = totalSeconds / 3600
let minutes = (totalSeconds % 3600) / 60
if hours > 0 {
return "\(hours)h \(minutes)m"
} else {
return minutes > 0 ? "\(minutes)m" : "<1m"
}
}

static func percentText(window: RateWindow?, showUsed: Bool) -> String? {
guard let window else { return nil }
let percent = showUsed ? window.usedPercent : window.remainingPercent
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBar/PreferencesDisplayPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ struct DisplayPane: View {
title: "Menu bar shows percent",
subtitle: "Replace critter bars with provider branding icons and a percentage.",
binding: self.$settings.menuBarShowsBrandIconWithPercent)
PreferenceToggleRow(
title: "Show time until reset",
subtitle: "Display a countdown (e.g. \"28m\") next to the percentage in the menu bar.",
binding: self.$settings.menuBarShowsTimeUntilReset)
.disabled(!self.settings.menuBarShowsBrandIconWithPercent)
.opacity(self.settings.menuBarShowsBrandIconWithPercent ? 1 : 0.5)
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("Display mode")
Expand Down
8 changes: 8 additions & 0 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ extension SettingsStore {
}
}

var menuBarShowsTimeUntilReset: Bool {
get { self.defaultsState.menuBarShowsTimeUntilReset }
set {
self.defaultsState.menuBarShowsTimeUntilReset = newValue
self.userDefaults.set(newValue, forKey: "menuBarShowsTimeUntilReset")
}
}

var menuBarShowsBrandIconWithPercent: Bool {
get { self.defaultsState.menuBarShowsBrandIconWithPercent }
set {
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ extension SettingsStore {
}
let usageBarsShowUsed = userDefaults.object(forKey: "usageBarsShowUsed") as? Bool ?? false
let resetTimesShowAbsolute = userDefaults.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false
let menuBarShowsTimeUntilReset = userDefaults.object(forKey: "menuBarShowsTimeUntilReset") as? Bool ?? false
let menuBarShowsBrandIconWithPercent = userDefaults.object(
forKey: "menuBarShowsBrandIconWithPercent") as? Bool ?? false
let menuBarDisplayModeRaw = userDefaults.string(forKey: "menuBarDisplayMode")
Expand Down Expand Up @@ -236,6 +237,7 @@ extension SettingsStore {
sessionQuotaNotificationsEnabled: sessionQuotaNotificationsEnabled,
usageBarsShowUsed: usageBarsShowUsed,
resetTimesShowAbsolute: resetTimesShowAbsolute,
menuBarShowsTimeUntilReset: menuBarShowsTimeUntilReset,
menuBarShowsBrandIconWithPercent: menuBarShowsBrandIconWithPercent,
menuBarDisplayModeRaw: menuBarDisplayModeRaw,
showAllTokenAccountsInMenu: showAllTokenAccountsInMenu,
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStoreState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ struct SettingsDefaultsState: Sendable {
var sessionQuotaNotificationsEnabled: Bool
var usageBarsShowUsed: Bool
var resetTimesShowAbsolute: Bool
var menuBarShowsTimeUntilReset: Bool
var menuBarShowsBrandIconWithPercent: Bool
var menuBarDisplayModeRaw: String?
var showAllTokenAccountsInMenu: Bool
Expand Down
14 changes: 13 additions & 1 deletion Sources/CodexBar/StatusItemController+Animation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,19 @@ extension StatusItemController {
.replacingOccurrences(of: " left", with: "")
}

return displayText
guard let base = displayText else { return displayText }

if self.settings.menuBarShowsTimeUntilReset {
// Pick the nearest upcoming reset across primary and secondary windows.
let now = Date()
let dates = [snapshot?.primary?.resetsAt, snapshot?.secondary?.resetsAt].compactMap { $0 }
let nearest = dates.filter { $0 > now }.min()
if let countdown = MenuBarDisplayText.timeUntilResetText(resetsAt: nearest, now: now) {
return "\(base) · \(countdown)"
}
}

return base
}

private func menuBarPercentWindow(for provider: UsageProvider, snapshot: UsageSnapshot?) -> RateWindow? {
Expand Down
20 changes: 20 additions & 0 deletions Sources/CodexBar/StatusItemController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
var lastSwitcherProviders: [UsageProvider] = []
/// Tracks which switcher tab state was used for the current merged-menu switcher instance.
var lastMergedSwitcherSelection: ProviderSwitcherSelection?
/// Fires every 60 seconds to keep the time-until-reset countdown current in the menu bar.
private var countdownRefreshTimer: Timer?

let loginLogger = CodexBarLog.logger(LogCategories.login)
var selectedMenuProvider: UsageProvider? {
get { self.settings.selectedMenuProvider }
Expand Down Expand Up @@ -195,6 +198,7 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
self.observeDebugForceAnimation()
self.observeSettingsChanges()
self.observeUpdaterChanges()
self.updateCountdownTimer()
}

private func observeStoreChanges() {
Expand Down Expand Up @@ -320,11 +324,27 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
}
self.updateVisibility()
self.updateIcons()
self.updateCountdownTimer()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Track countdown toggle in settings observation

This call only runs when observeSettingsChanges() is triggered, but the new menuBarShowsTimeUntilReset flag is not read in SettingsStore.menuObservationToken (Sources/CodexBar/SettingsStore+MenuObservation.swift), so toggling “Show time until reset” at runtime does not immediately execute this path. In that scenario the menu bar text and countdown timer won’t start/stop until some unrelated observed setting changes (or the app restarts), which makes the new toggle appear broken.

Useful? React with 👍 / 👎.

if shouldRefreshOpenMenus {
self.refreshOpenMenusIfNeeded()
}
}

/// Starts a 60-second repeating timer when the countdown feature is active, stops it otherwise.
private func updateCountdownTimer() {
let needsTimer = self.settings.menuBarShowsTimeUntilReset &&
self.settings.menuBarShowsBrandIconWithPercent
if needsTimer, self.countdownRefreshTimer == nil {
self.countdownRefreshTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) {
[weak self] _ in
Task { @MainActor [weak self] in self?.updateIcons() }
}
} else if !needsTimer, self.countdownRefreshTimer != nil {
self.countdownRefreshTimer?.invalidate()
self.countdownRefreshTimer = nil
}
}

private func updateIcons() {
// Avoid flicker: when an animation driver is active, store updates can call `updateIcons()` and
// briefly overwrite the animated frame with the static (phase=nil) icon.
Expand Down