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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ debug_*.swift
.vscode/
.codex/environments/
.swiftpm-cache/
output.log

# Debug/analysis docs
docs/*-analysis.md
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodexBar/PreferencesDisplayPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ struct DisplayPane: View {
title: "Merge Icons",
subtitle: "Use a single menu bar icon with a provider switcher.",
binding: self.$settings.mergeIcons)
PreferenceToggleRow(
title: "Show separate progress bars",
subtitle: "Display an additional status item showing only progress bars (no icon or text).",
binding: self.$settings.menuBarShowsSeparateBars)
.disabled(!self.settings.mergeIcons)
.opacity(self.settings.mergeIcons ? 1 : 0.5)
Comment on lines +24 to +29
Comment on lines +24 to +29
PreferenceToggleRow(
title: "Switcher shows icons",
subtitle: "Show provider icons in the switcher (otherwise show a weekly progress line).",
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 @@ -125,6 +125,14 @@ extension SettingsStore {
}
}

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

private var menuBarDisplayModeRaw: String? {
get { self.defaultsState.menuBarDisplayModeRaw }
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 @@ -188,6 +188,7 @@ extension SettingsStore {
let resetTimesShowAbsolute = userDefaults.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false
let menuBarShowsBrandIconWithPercent = userDefaults.object(
forKey: "menuBarShowsBrandIconWithPercent") as? Bool ?? false
let menuBarShowsSeparateBars = userDefaults.object(forKey: "menuBarShowsSeparateBars") as? Bool ?? false
let menuBarDisplayModeRaw = userDefaults.string(forKey: "menuBarDisplayMode")
?? MenuBarDisplayMode.percent.rawValue
let historicalTrackingEnabled = userDefaults.object(forKey: "historicalTrackingEnabled") as? Bool ?? false
Expand Down Expand Up @@ -238,6 +239,7 @@ extension SettingsStore {
usageBarsShowUsed: usageBarsShowUsed,
resetTimesShowAbsolute: resetTimesShowAbsolute,
menuBarShowsBrandIconWithPercent: menuBarShowsBrandIconWithPercent,
menuBarShowsSeparateBars: menuBarShowsSeparateBars,
menuBarDisplayModeRaw: menuBarDisplayModeRaw,
historicalTrackingEnabled: historicalTrackingEnabled,
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 @@ -14,6 +14,7 @@ struct SettingsDefaultsState: Sendable {
var usageBarsShowUsed: Bool
var resetTimesShowAbsolute: Bool
var menuBarShowsBrandIconWithPercent: Bool
var menuBarShowsSeparateBars: Bool
var menuBarDisplayModeRaw: String?
var historicalTrackingEnabled: Bool
var showAllTokenAccountsInMenu: Bool
Expand Down
96 changes: 96 additions & 0 deletions Sources/CodexBar/StatusItemController+Animation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ extension StatusItemController {
if mergeIcons {
let phase: Double? = self.needsMenuBarIconAnimation() ? self.animationPhase : nil
self.applyIcon(phase: phase)
self.applySeparateBarsIcon(phase: phase)
}
}

Expand Down Expand Up @@ -322,6 +323,99 @@ extension StatusItemController {
self.setButtonImage(image, for: button)
}
}

/// Applies an icon showing only dual progress bars (no brand icon, no text) to the separate bars status item.
func applySeparateBarsIcon(phase: Double?) {
guard self.settings.menuBarShowsSeparateBars,
let button = self.separateBarsStatusItem?.button
else { return }
Comment on lines +327 to +331

let primaryProvider = self.primaryProviderForUnifiedIcon()

// Force a refresh when the provider changes to avoid rendering artifacts
let providerChanged = self.lastSeparateBarsProvider != primaryProvider
if providerChanged {
// Clear the old image to force a clean render
button.image = nil
self.lastSeparateBarsProvider = primaryProvider
}
Comment on lines +327 to +341

let showUsed = self.settings.usageBarsShowUsed
let snapshot = self.store.snapshot(for: primaryProvider)

var primary = showUsed ? snapshot?.primary?.usedPercent : snapshot?.primary?.remainingPercent
var weekly = showUsed ? snapshot?.secondary?.usedPercent : snapshot?.secondary?.remainingPercent
if showUsed,
Comment on lines +327 to +348
primaryProvider == .warp,
let remaining = snapshot?.secondary?.remainingPercent,
remaining <= 0
{
weekly = 0
}
if showUsed,
primaryProvider == .warp,
let remaining = snapshot?.secondary?.remainingPercent,
remaining > 0,
weekly == 0
{
weekly = Self.loadingPercentEpsilon
}
var stale = self.store.isStale(provider: primaryProvider)
var morphProgress: Double?

let needsAnimation = self.needsMenuBarIconAnimation()
if let phase, needsAnimation {
var pattern = self.animationPattern
if pattern == .unbraid {
morphProgress = pattern.value(phase: phase) / 100
primary = nil
weekly = nil
stale = false
} else {
primary = max(pattern.value(phase: phase), Self.loadingPercentEpsilon)
weekly = max(pattern.value(phase: phase + pattern.secondaryOffset), Self.loadingPercentEpsilon)
stale = false
}
}

let style: IconStyle = self.store.style(for: primaryProvider)
let isLoading = phase != nil && self.shouldAnimate(provider: primaryProvider)
let blink: CGFloat = {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we also review the code path that clears blink and motion state for this new item? Since this renderer now depends on blink, wiggle, and tilt, I'm wondering whether the separate bars icon could remain on its last animated frame if blinking stops without an immediate redraw.

guard isLoading, style == .warp, let phase else {
return self.blinkAmount(for: primaryProvider)
}
let normalized = (sin(phase * 3) + 1) / 2
return CGFloat(max(0, min(normalized, 1)))
}()
let wiggle = self.wiggleAmount(for: primaryProvider)
let tilt = self.tiltAmount(for: primaryProvider) * .pi / 28

let statusIndicator: ProviderStatusIndicator = {
for provider in self.store.enabledProviders() {
let indicator = self.store.statusIndicator(for: provider)
if indicator.hasIssue { return indicator }
}
return .none
}()

self.setButtonTitle(nil, for: button)
if let morphProgress {
let image = IconRenderer.makeMorphIcon(progress: morphProgress, style: style)
self.setButtonImage(image, for: button)
} else {
let image = IconRenderer.makeIcon(
primaryRemaining: primary,
weeklyRemaining: weekly,
creditsRemaining: nil,
stale: stale,
style: style,
blink: blink,
wiggle: wiggle,
tilt: tilt,
statusIndicator: statusIndicator)
self.setButtonImage(image, for: button)
}
}

func applyIcon(for provider: UsageProvider, phase: Double?) {
guard let button = self.statusItems[provider]?.button else { return }
Expand Down Expand Up @@ -581,6 +675,7 @@ extension StatusItemController {
self.animationPhase = 0
if self.shouldMergeIcons {
self.applyIcon(phase: nil)
self.applySeparateBarsIcon(phase: nil)
} else {
UsageProvider.allCases.forEach { self.applyIcon(for: $0, phase: nil) }
}
Expand All @@ -591,6 +686,7 @@ extension StatusItemController {
self.animationPhase += 0.045 // half-speed animation
if self.shouldMergeIcons {
self.applyIcon(phase: self.animationPhase)
self.applySeparateBarsIcon(phase: self.animationPhase)
} else {
UsageProvider.allCases.forEach { self.applyIcon(for: $0, phase: self.animationPhase) }
}
Expand Down
17 changes: 17 additions & 0 deletions Sources/CodexBar/StatusItemController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
private let statusBar: NSStatusBar
var statusItem: NSStatusItem
var statusItems: [UsageProvider: NSStatusItem] = [:]
var separateBarsStatusItem: NSStatusItem? // Optional separate status item showing only progress bars
var lastMenuProvider: UsageProvider?
var menuProviders: [ObjectIdentifier: UsageProvider] = [:]
var menuContentVersion: Int = 0
Expand Down Expand Up @@ -85,6 +86,8 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
/// Tracks which `usageBarsShowUsed` mode the provider switcher was built with.
/// Used to decide whether we can "smart update" menu content without rebuilding the switcher.
var lastSwitcherUsageBarsShowUsed: Bool
/// Tracks the last provider used for the separate bars status item, to detect when it changes.
var lastSeparateBarsProvider: UsageProvider?
/// Tracks whether the merged-menu switcher was built with the Overview tab visible.
/// Used to force switcher rebuilds when Overview availability toggles.
var lastSwitcherIncludesOverview: Bool = false
Expand Down Expand Up @@ -338,6 +341,7 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
let phase: Double? = self.needsMenuBarIconAnimation() ? self.animationPhase : nil
if self.shouldMergeIcons {
self.applyIcon(phase: phase)
self.applySeparateBarsIcon(phase: phase)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh separate bars on every icon render path

applySeparateBarsIcon is wired only in updateIcons, but animation/blink render paths (updateAnimationFrame, updateAnimationState, and tickBlink in StatusItemController+Animation.swift) still call only applyIcon. As a result, when loading animation or blink effects are active, the separate bars item does not advance frames and can also stay blank on startup until another settings/store update triggers updateIcons after updateVisibility creates the item.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we double-check the startup path for this item when the setting is already enabled? updateIcons() appears to run before updateVisibility() in init, and applySeparateBarsIcon() returns early until the item exists, so I'm wondering whether this status item could be visible but initially blank until a later refresh occurs.

self.attachMenus()
} else {
UsageProvider.allCases.forEach { self.applyIcon(for: $0, phase: phase) }
Expand All @@ -362,6 +366,19 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
let anyEnabled = !self.store.enabledProviders().isEmpty
let force = self.store.debugForceAnimation
let mergeIcons = self.shouldMergeIcons
let showsSeparateBars = self.settings.menuBarShowsSeparateBars && mergeIcons

Comment on lines 368 to +370
// Update separate bars status item visibility
if showsSeparateBars {
if self.separateBarsStatusItem == nil {
self.separateBarsStatusItem = self.statusBar.statusItem(withLength: NSStatusItem.variableLength)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we verify how interactions are wired for this new status item? I'm not seeing a menu or action assigned to separateBarsStatusItem, so I'm wondering whether it may appear in the menu bar but not respond when clicked.

self.separateBarsStatusItem?.button?.imageScaling = .scaleNone
}
self.separateBarsStatusItem?.isVisible = anyEnabled || force
Comment on lines +369 to +377
} else {
self.separateBarsStatusItem?.isVisible = false
}

if mergeIcons {
self.statusItem.isVisible = anyEnabled || force
for item in self.statusItems.values {
Expand Down