import CodexBarCore import Foundation enum PercentWindow: String, CaseIterable, Codable, Hashable, Sendable { case session case weekly case automatic } enum MenuBarLayoutToken: Codable, Hashable, Sendable { case icon case providerName case accountLabel case percent(window: PercentWindow) case usageBar case resetCountdown case resetAbsolute case runsOut case costToday case cost30d case separatorDot case space } enum MenuBarLayoutSemanticWindowResolver { static func windows( provider: UsageProvider, snapshot: UsageSnapshot?) -> (session: RateWindow?, weekly: RateWindow?) { guard let snapshot else { return (nil, nil) } let candidates = [ snapshot.primary, snapshot.secondary, snapshot.tertiary, ] + (snapshot.extraRateWindows ?? []).map(\.window) let usable = candidates.compactMap { window -> RateWindow? in guard let window, !window.isSyntheticPlaceholder else { return nil } return window } let session = usable.first { window in guard let minutes = window.windowMinutes else { return false } return (60...(12 * 60)).contains(minutes) } let cadenceWeekly = usable.first { $0.windowMinutes == 7 * 24 * 60 } let kimiWeekly = snapshot.primary.flatMap { $0.isSyntheticPlaceholder ? nil : $0 } let weekly = provider == .kimi ? kimiWeekly ?? cadenceWeekly : cadenceWeekly return (session, weekly) } } enum MenuBarLayoutCostResolver { static func todayCostUSD( snapshot: CostUsageTokenSnapshot?, now: Date, calendar: Calendar = .current) -> Double? { guard let snapshot else { return nil } return CostUsageTokenSnapshot.entry( in: snapshot.daily, forLocalDayContaining: now, calendar: calendar)?.costUSD } } struct MenuBarLayout: Codable, Hashable, Sendable { static let defaultLayout = MenuBarLayout(lines: [[.icon, .percent(window: .automatic)]]) let lines: [[MenuBarLayoutToken]] init(lines: [[MenuBarLayoutToken]]) { self.lines = Self.normalizedLines(lines) } private enum CodingKeys: String, CodingKey { case lines } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) try self.init(lines: container.decode([[MenuBarLayoutToken]].self, forKey: .lines)) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.lines, forKey: .lines) } private static func normalizedLines(_ lines: [[MenuBarLayoutToken]]) -> [[MenuBarLayoutToken]] { guard let firstContentLine = lines.firstIndex(where: { !$0.isEmpty }) else { return self.defaultLayout.lines } return Array(lines[firstContentLine...].prefix(2)) } } enum MenuBarLayoutPreset: String, CaseIterable, Identifiable, Sendable { case iconAndPercent case iconOnly case percentAndReset case compactStacked case custom var id: String { self.rawValue } var layout: MenuBarLayout? { switch self { case .iconAndPercent: MenuBarLayout(lines: [[.icon, .percent(window: .automatic)]]) case .iconOnly: MenuBarLayout(lines: [[.icon]]) case .percentAndReset: MenuBarLayout(lines: [[ .icon, .percent(window: .automatic), .separatorDot, .resetCountdown, ]]) case .compactStacked: MenuBarLayout(lines: [ [.percent(window: .session)], [.percent(window: .weekly)], ]) case .custom: nil } } static func matching(_ layout: MenuBarLayout) -> Self { allCases.first { $0.layout == layout } ?? .custom } } enum MenuBarLayoutSize: String, CaseIterable, Identifiable, Sendable { case small case regular var id: String { self.rawValue } } enum MenuBarLayoutGap: String, CaseIterable, Identifiable, Sendable { case tight case regular var id: String { self.rawValue } } struct MenuBarLayoutResolution: Equatable { struct LegacySettings: Equatable { let iconStyle: MenuBarIconStyle let displayMode: MenuBarDisplayMode let metricPreference: MenuBarMetricPreference let resetTimeDisplayStyle: ResetTimeDisplayStyle } let layout: MenuBarLayout let legacySettings: LegacySettings? var usesLegacyRendering: Bool { self.legacySettings != nil } static func stored(_ layout: MenuBarLayout) -> Self { Self(layout: layout, legacySettings: nil) } static func legacy( iconStyle: MenuBarIconStyle, displayMode: MenuBarDisplayMode, metricPreference: MenuBarMetricPreference, resetTimeDisplayStyle: ResetTimeDisplayStyle, provider: UsageProvider? = nil) -> Self { Self( layout: MenuBarLayout.migrated( iconStyle: iconStyle, displayMode: displayMode, metricPreference: metricPreference, resetTimeDisplayStyle: resetTimeDisplayStyle, provider: provider), legacySettings: LegacySettings( iconStyle: iconStyle, displayMode: displayMode, metricPreference: metricPreference, resetTimeDisplayStyle: resetTimeDisplayStyle)) } } extension MenuBarLayout { static func migrated( iconStyle: MenuBarIconStyle, displayMode: MenuBarDisplayMode, metricPreference: MenuBarMetricPreference, resetTimeDisplayStyle: ResetTimeDisplayStyle, provider: UsageProvider? = nil) -> MenuBarLayout { _ = iconStyle // Critters and bars keep rendering through their unchanged legacy path. let icon: MenuBarLayoutToken = .icon switch displayMode { case .percent: if metricPreference == .primaryAndSecondary { return MenuBarLayout(lines: [[ icon, .percent(window: Self.percentWindow(for: .primary, provider: provider)), .separatorDot, .percent(window: Self.percentWindow(for: .secondary, provider: provider)), ]]) } return MenuBarLayout(lines: [[ icon, .percent(window: Self.percentWindow(for: metricPreference, provider: provider)), ]]) case .pace: return MenuBarLayout(lines: [[icon, .runsOut]]) case .both: return MenuBarLayout(lines: [[ icon, .percent(window: Self.percentWindow(for: metricPreference, provider: provider)), .separatorDot, .runsOut, ]]) case .resetTime: let resetItem = resetTimeDisplayStyle == .absolute ? MenuBarLayoutToken.resetAbsolute : MenuBarLayoutToken.resetCountdown return MenuBarLayout(lines: [[icon, resetItem]]) } } private static func percentWindow( for preference: MenuBarMetricPreference, provider: UsageProvider?) -> PercentWindow { switch preference { case .primary: provider == .kimi ? .weekly : .session case .secondary: provider == .kimi ? .session : .weekly case .automatic, .primaryAndSecondary, .tertiary, .extraUsage, .average, .monthlyPlan: .automatic } } }