101 lines · 4.4 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)        -> String?    {        var parts: [String] = []        if let sessionWindow,           let session = self.percentText(window: sessionWindow, showUsed: showUsed)        {            parts.append("\(self.sessionWindowLabel(window: sessionWindow)) \(session)")        }        if let weekly = self.percentText(window: weeklyWindow, showUsed: showUsed) {            parts.append("W \(weekly)")        }        return parts.isEmpty ? nil : parts.joined(separator: " · ")    }     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,        now: Date = .init()) -> String?    {        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 }            if let resetsAt = percentWindow.resetsAt {                let description = switch resetTimeDisplayStyle {                case .countdown:                    UsageFormatter.resetCountdownDescription(from: resetsAt, now: now)                case .absolute:                    UsageFormatter.resetDescription(from: resetsAt, now: now)                }                return "↻ \(description)"            }            if let resetDescription = self.resetMetadataText(percentWindow.resetDescription) {                return "↻ \(resetDescription)"            }            return self.percentText(window: percentWindow, showUsed: showUsed)        }    }     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    }}