182 lines · 7.7 KB · Swift
RawDownload
import CodexBarCoreimport Foundation enum MenuBarDisplayText {    static func percentText(window: RateWindow?, showUsed: Bool) -> String? {        guard let window else { return nil }        let percent = showUsed ? window.usedPercent : window.remainingPercent        return UsageFormatter.percentString(percent)    }     static func paceText(pace: UsagePace?) -> String? {        guard let pace else { return nil }        let deltaValue = Int(abs(pace.deltaPercent).rounded())        if deltaValue == 0 { return "0%" }        let sign = pace.deltaPercent >= 0 ? "+" : "-"        return "\(sign)\(deltaValue)%"    }     /// Combined "session · weekly" menu-bar text shared by providers that expose both a    /// session (5h) and weekly (7d) lane, e.g. Codex and Claude.    static func combinedSessionWeeklyPercentText(        sessionWindow: RateWindow?,        weeklyWindow: RateWindow?,        showUsed: Bool,        resetTimeDisplayStyle: ResetTimeDisplayStyle = .countdown,        showsResetTimeWhenExhausted: Bool = false,        now: Date = .init())        -> String?    {        var parts: [String] = []        if let sessionWindow,           let session = self.laneValueText(               window: sessionWindow,               showUsed: showUsed,               resetTimeDisplayStyle: resetTimeDisplayStyle,               showsResetTimeWhenExhausted: showsResetTimeWhenExhausted,               now: now)        {            parts.append("\(self.sessionWindowLabel(window: sessionWindow)) \(session)")        }        if let weeklyWindow,           let weekly = self.laneValueText(               window: weeklyWindow,               showUsed: showUsed,               resetTimeDisplayStyle: resetTimeDisplayStyle,               showsResetTimeWhenExhausted: showsResetTimeWhenExhausted,               now: now)        {            parts.append("W \(weekly)")        }        return parts.isEmpty ? nil : parts.joined(separator: " · ")    }     private static func laneValueText(        window: RateWindow,        showUsed: Bool,        resetTimeDisplayStyle: ResetTimeDisplayStyle,        showsResetTimeWhenExhausted: Bool,        now: Date) -> String?    {        if let resetText = self.exhaustedResetText(            window: window,            enabled: showsResetTimeWhenExhausted,            style: resetTimeDisplayStyle,            now: now)        {            return resetText        }        return self.percentText(window: window, showUsed: showUsed)    }     private static func sessionWindowLabel(window: RateWindow) -> String {        guard let minutes = window.windowMinutes, minutes > 0 else { return "S" }        guard minutes.isMultiple(of: 60) else { return "\(minutes)m" }        return "\(minutes / 60)h"    }     static func displayText(        mode: MenuBarDisplayMode,        percentWindow: RateWindow?,        pace: UsagePace? = nil,        showUsed: Bool,        resetTimeDisplayStyle: ResetTimeDisplayStyle = .countdown,        showsResetTimeWhenExhausted: Bool = false,        now: Date = .init()) -> String?    {        if mode != .resetTime,           showsResetTimeWhenExhausted,           let percentWindow,           percentWindow.remainingPercent <= 0        {            if let resetText = self.exhaustedResetText(                window: percentWindow,                enabled: true,                style: resetTimeDisplayStyle,                now: now)            {                return resetText            }            // Smart mode cannot replace an exhausted percentage unless the reset is concrete, future,            // and schedulable. Preserve the quota signal in pace/both modes too; a pace from another            // combined lane must not hide that this displayed lane is already exhausted.            return self.percentText(window: percentWindow, showUsed: showUsed)        }        switch mode {        case .percent:            return self.percentText(window: percentWindow, showUsed: showUsed)        case .pace:            // Pace can be temporarily unavailable near a reset or when a provider omits window metadata.            // Keep the selected quota visible instead of collapsing the status item to an icon-only state.            return self.paceText(pace: pace)                ?? self.percentText(window: percentWindow, showUsed: showUsed)        case .both:            guard let percent = percentText(window: percentWindow, showUsed: showUsed) else { return nil }            // Fall back to percent-only when pace is unavailable (e.g. Copilot)            guard let paceText = Self.paceText(pace: pace) else { return percent }            return "\(percent) · \(paceText)"        case .resetTime:            guard let percentWindow else { return nil }            return self.resetTimeText(window: percentWindow, style: resetTimeDisplayStyle, now: now)                ?? self.percentText(window: percentWindow, showUsed: showUsed)        }    }     /// "↻ …" reset text for a window, or nil when it carries no usable reset metadata.    static func resetTimeText(        window: RateWindow,        style: ResetTimeDisplayStyle,        now: Date) -> String?    {        if let resetsAt = window.resetsAt {            let description = switch style {            case .countdown:                UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)            case .absolute:                UsageFormatter.resetDescription(from: resetsAt, now: now)            }            return "↻ \(description)"        }        if let resetDescription = self.resetMetadataText(window.resetDescription) {            return "↻ \(resetDescription)"        }        return nil    }     /// Smart-mode replacement: when enabled and the quota is exhausted (0% remaining, regardless of    /// whether the display shows used or remaining), surface the reset time instead of a dead percent.    ///    /// Requires a concrete, still-future `resetsAt`. The smart option only replaces the percent when it    /// has a reset time it can both render as a live countdown/clock AND hand to the refresh scheduler,    /// so the lane keeps ticking and flips back to the percentage once the reset passes. Windows with    /// only textual reset metadata (`resetDescription`, no `resetsAt`) or an already-elapsed reset can't    /// be scheduled, so they keep showing the percent instead of freezing on stale reset text.    private static func exhaustedResetText(        window: RateWindow?,        enabled: Bool,        style: ResetTimeDisplayStyle,        now: Date) -> String?    {        guard enabled, let window, window.remainingPercent <= 0 else { return nil }        guard let resetsAt = window.resetsAt, resetsAt > now else { return nil }        return self.resetTimeText(window: window, style: style, now: now)    }     private static func resetMetadataText(_ description: String?) -> String? {        guard let description else { return nil }        let trimmed = description.trimmingCharacters(in: .whitespacesAndNewlines)        guard !trimmed.isEmpty else { return nil }         // RateWindow.resetDescription predates provider-specific detail fields and is also used for        // request/token summaries. Only trust phrases that explicitly describe reset timing.        let normalized = trimmed.lowercased()        let resetPrefixes = [            "reset ", "resets ", "in ", "today ", "today,", "tomorrow ", "tomorrow,", "next ",            "expire ", "expires ", "refill ", "refills ",        ]        let exactResetDescriptions = ["today", "tomorrow", "expired", "now", "soon"]        return exactResetDescriptions.contains(normalized) || resetPrefixes.contains(where: normalized.hasPrefix)            ? trimmed            : nil    }}