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
36 changes: 36 additions & 0 deletions Sources/CodexBar/CodexMenuBarVisualizationMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation

/// Controls Codex icon rendering in the menu bar when brand+text mode is disabled.
enum CodexMenuBarVisualizationMode: String, CaseIterable, Identifiable {
case classic
case pieRing
case pieRingSwapped

var id: String {
self.rawValue
}

var label: String {
switch self {
case .classic: "Classic bars"
case .pieRing: "Pie + ring"
case .pieRingSwapped: "Pie + ring (swapped)"
}
}

var description: String {
switch self {
case .classic: "Default Codex bar icon."
case .pieRing: "Inner weekly pie + outer 5-hour ring."
case .pieRingSwapped: "Inner 5-hour pie + outer weekly ring."
}
}

var usesPieRingLayout: Bool {
self != .classic
}

var placesWeeklyInOuterRing: Bool {
self == .pieRingSwapped
}
}
131 changes: 131 additions & 0 deletions Sources/CodexBar/IconRenderer.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import AppKit
import CodexBarCore

struct CodexPieRingInvalidCharts: OptionSet, Sendable {
let rawValue: Int

static let pie = Self(rawValue: 1 << 0)
static let ring = Self(rawValue: 1 << 1)
}

// swiftlint:disable:next type_body_length
enum IconRenderer {
private static let creditsCap: Double = 1000
Expand Down Expand Up @@ -763,6 +770,130 @@ enum IconRenderer {

// swiftlint:enable function_body_length

static func makeCodexPieRingIcon(
weeklyUsed: Double?,
sessionUsed: Double?,
mode: CodexMenuBarVisualizationMode,
invalidCharts: CodexPieRingInvalidCharts = [],
flashInvalidChartsAsUsed: Bool = false,
stale: Bool,
statusIndicator: ProviderStatusIndicator = .none) -> NSImage
{
self.renderImage {
let baseFill = NSColor.labelColor
let trackFillAlpha: CGFloat = stale ? 0.18 : 0.28
let trackStrokeAlpha: CGFloat = stale ? 0.28 : 0.44
let fillColor = baseFill.withAlphaComponent(stale ? 0.55 : 1.0)
let center = CGPoint(x: self.outputSize.width / 2, y: self.outputSize.height / 2)

let outerRadius: CGFloat = 7.2
let outerRingWidth: CGFloat = 2.0
let ringGap: CGFloat = 0.8
let innerPieRadius = outerRadius - outerRingWidth - ringGap

let innerUsed = mode.placesWeeklyInOuterRing ? sessionUsed : weeklyUsed
let outerUsed = mode.placesWeeklyInOuterRing ? weeklyUsed : sessionUsed
let innerProgress = max(0, min((innerUsed ?? 0) / 100, 1))
let outerProgress = max(0, min((outerUsed ?? 0) / 100, 1))
let trackFillColor = baseFill.withAlphaComponent(trackFillAlpha)
let trackStrokeColor = baseFill.withAlphaComponent(trackStrokeAlpha)

let innerTrack = NSBezierPath(
ovalIn: CGRect(
x: center.x - innerPieRadius,
y: center.y - innerPieRadius,
width: innerPieRadius * 2,
height: innerPieRadius * 2))
trackFillColor.setFill()
innerTrack.fill()

if invalidCharts.contains(.pie) {
if flashInvalidChartsAsUsed {
fillColor.setFill()
innerTrack.fill()
} else {
trackFillColor.setFill()
innerTrack.fill()
}
} else if innerUsed != nil, innerProgress > 0 {
fillColor.setFill()
if innerProgress >= 0.999 {
innerTrack.fill()
} else {
let startAngle: CGFloat = 90
let usedEndAngle = startAngle - (innerProgress * 360)
let wedge = NSBezierPath()
wedge.move(to: center)
wedge.appendArc(
withCenter: center,
radius: innerPieRadius,
startAngle: startAngle,
endAngle: usedEndAngle,
clockwise: true)
wedge.close()
wedge.fill()
}
}

let innerStroke = NSBezierPath(
ovalIn: CGRect(
x: center.x - innerPieRadius,
y: center.y - innerPieRadius,
width: innerPieRadius * 2,
height: innerPieRadius * 2))
innerStroke.lineWidth = 1
trackStrokeColor.setStroke()
innerStroke.stroke()

let ringRect = CGRect(
x: center.x - outerRadius,
y: center.y - outerRadius,
width: outerRadius * 2,
height: outerRadius * 2)

let outerTrack = NSBezierPath(ovalIn: ringRect)
outerTrack.lineWidth = outerRingWidth
outerTrack.lineCapStyle = .round
trackStrokeColor.setStroke()
outerTrack.stroke()

if invalidCharts.contains(.ring) {
let fullOuterArc = NSBezierPath(ovalIn: ringRect)
fullOuterArc.lineWidth = outerRingWidth
fullOuterArc.lineCapStyle = .round
if flashInvalidChartsAsUsed {
fillColor.setStroke()
} else {
trackStrokeColor.setStroke()
}
fullOuterArc.stroke()
} else if outerUsed != nil, outerProgress > 0 {
fillColor.setStroke()
if outerProgress >= 0.999 {
let fullOuterArc = NSBezierPath(ovalIn: ringRect)
fullOuterArc.lineWidth = outerRingWidth
fullOuterArc.lineCapStyle = .round
fullOuterArc.stroke()
} else {
let startAngle: CGFloat = 90
let usedEndAngle = startAngle - (outerProgress * 360)
let outerArc = NSBezierPath()
outerArc.lineWidth = outerRingWidth
outerArc.lineCapStyle = .round
outerArc.appendArc(
withCenter: center,
radius: outerRadius,
startAngle: startAngle,
endAngle: usedEndAngle,
clockwise: true)
outerArc.stroke()
}
}

Self.drawStatusOverlay(indicator: statusIndicator)
}
}

/// Morph helper: unbraids a simplified knot into our bar icon.
static func makeMorphIcon(progress: Double, style: IconStyle) -> NSImage {
let clamped = max(0, min(progress, 1))
Expand Down
20 changes: 20 additions & 0 deletions Sources/CodexBar/PreferencesDisplayPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,26 @@ struct DisplayPane: View {
}
.disabled(!self.settings.menuBarShowsBrandIconWithPercent)
.opacity(self.settings.menuBarShowsBrandIconWithPercent ? 1 : 0.5)
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 4) {
Text("Codex icon style")
.font(.body)
Text("Codex-only: Choose whether weekly or 5-hour usage appears in the pie vs ring.")
.font(.footnote)
.foregroundStyle(.tertiary)
}
Spacer()
Picker("Codex icon style", selection: self.$settings.codexMenuBarVisualizationMode) {
ForEach(CodexMenuBarVisualizationMode.allCases) { mode in
Text(mode.label).tag(mode)
}
}
.labelsHidden()
.pickerStyle(.menu)
.frame(maxWidth: 200)
}
.disabled(self.settings.menuBarShowsBrandIconWithPercent)
.opacity(self.settings.menuBarShowsBrandIconWithPercent ? 0.5 : 1)
}

Divider()
Expand Down
17 changes: 17 additions & 0 deletions Sources/CodexBar/SettingsStore+Defaults.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,28 @@ extension SettingsStore {
}
}

private var codexMenuBarVisualizationModeRaw: String? {
get { self.defaultsState.codexMenuBarVisualizationModeRaw }
set {
self.defaultsState.codexMenuBarVisualizationModeRaw = newValue
if let raw = newValue {
self.userDefaults.set(raw, forKey: "codexMenuBarVisualizationMode")
} else {
self.userDefaults.removeObject(forKey: "codexMenuBarVisualizationMode")
}
}
}

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

var codexMenuBarVisualizationMode: CodexMenuBarVisualizationMode {
get { CodexMenuBarVisualizationMode(rawValue: self.codexMenuBarVisualizationModeRaw ?? "") ?? .classic }
set { self.codexMenuBarVisualizationModeRaw = newValue.rawValue }
}

var showAllTokenAccountsInMenu: Bool {
get { self.defaultsState.showAllTokenAccountsInMenu }
set {
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 @@ -16,6 +16,7 @@ extension SettingsStore {
_ = self.menuBarShowsBrandIconWithPercent
_ = self.menuBarShowsHighestUsage
_ = self.menuBarDisplayMode
_ = self.codexMenuBarVisualizationMode
_ = self.showAllTokenAccountsInMenu
_ = self.menuBarMetricPreferencesRaw
_ = self.costUsageEnabled
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBar/SettingsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ extension SettingsStore {
forKey: "menuBarShowsBrandIconWithPercent") as? Bool ?? false
let menuBarDisplayModeRaw = userDefaults.string(forKey: "menuBarDisplayMode")
?? MenuBarDisplayMode.percent.rawValue
let codexMenuBarVisualizationModeRaw = userDefaults.string(forKey: "codexMenuBarVisualizationMode")
?? CodexMenuBarVisualizationMode.classic.rawValue
let showAllTokenAccountsInMenu = userDefaults.object(forKey: "showAllTokenAccountsInMenu") as? Bool ?? false
let storedPreferences = userDefaults.dictionary(forKey: "menuBarMetricPreferences") as? [String: String] ?? [:]
var resolvedPreferences = storedPreferences
Expand Down Expand Up @@ -238,6 +240,7 @@ extension SettingsStore {
resetTimesShowAbsolute: resetTimesShowAbsolute,
menuBarShowsBrandIconWithPercent: menuBarShowsBrandIconWithPercent,
menuBarDisplayModeRaw: menuBarDisplayModeRaw,
codexMenuBarVisualizationModeRaw: codexMenuBarVisualizationModeRaw,
showAllTokenAccountsInMenu: showAllTokenAccountsInMenu,
menuBarMetricPreferencesRaw: resolvedPreferences,
costUsageEnabled: costUsageEnabled,
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/SettingsStoreState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ struct SettingsDefaultsState: Sendable {
var resetTimesShowAbsolute: Bool
var menuBarShowsBrandIconWithPercent: Bool
var menuBarDisplayModeRaw: String?
var codexMenuBarVisualizationModeRaw: String?
var showAllTokenAccountsInMenu: Bool
var menuBarMetricPreferencesRaw: [String: String]
var costUsageEnabled: Bool
Expand Down
Loading