454 lines · 17.7 KB · Swift
RawDownload
import AppKitimport CodexBarCoreimport Observationimport ServiceManagement enum RefreshFrequency: String, CaseIterable, Identifiable {    case manual    case oneMinute    case twoMinutes    case fiveMinutes    case fifteenMinutes     var id: String { self.rawValue }     var seconds: TimeInterval? {        switch self {        case .manual: nil        case .oneMinute: 60        case .twoMinutes: 120        case .fiveMinutes: 300        case .fifteenMinutes: 900        }    }     var label: String {        switch self {        case .manual: "Manual"        case .oneMinute: "1 min"        case .twoMinutes: "2 min"        case .fiveMinutes: "5 min"        case .fifteenMinutes: "15 min"        }    }} @MainActor@Observablefinal class SettingsStore {    /// Persisted provider display order.    ///    /// Stored as raw `UsageProvider` strings so new providers can be appended automatically without breaking.    private var providerOrderRaw: [String] {        didSet { self.userDefaults.set(self.providerOrderRaw, forKey: "providerOrder") }    }     var refreshFrequency: RefreshFrequency {        didSet { self.userDefaults.set(self.refreshFrequency.rawValue, forKey: "refreshFrequency") }    }     var launchAtLogin: Bool {        didSet {            self.userDefaults.set(self.launchAtLogin, forKey: "launchAtLogin")            LaunchAtLoginManager.setEnabled(self.launchAtLogin)        }    }     /// Hidden toggle to reveal debug-only menu items (enable via defaults write com.steipete.CodexBar debugMenuEnabled    /// -bool YES).    var debugMenuEnabled: Bool {        didSet { self.userDefaults.set(self.debugMenuEnabled, forKey: "debugMenuEnabled") }    }     private var debugLoadingPatternRaw: String? {        didSet {            if let raw = self.debugLoadingPatternRaw {                self.userDefaults.set(raw, forKey: "debugLoadingPattern")            } else {                self.userDefaults.removeObject(forKey: "debugLoadingPattern")            }        }    }     var statusChecksEnabled: Bool {        didSet { self.userDefaults.set(self.statusChecksEnabled, forKey: "statusChecksEnabled") }    }     var sessionQuotaNotificationsEnabled: Bool {        didSet {            self.userDefaults.set(self.sessionQuotaNotificationsEnabled, forKey: "sessionQuotaNotificationsEnabled")        }    }     /// When enabled, progress bars show "percent used" instead of "percent left".    var usageBarsShowUsed: Bool {        didSet { self.userDefaults.set(self.usageBarsShowUsed, forKey: "usageBarsShowUsed") }    }     /// Optional: show provider cost summary from local usage logs (Codex + Claude).    var ccusageCostUsageEnabled: Bool {        didSet { self.userDefaults.set(self.ccusageCostUsageEnabled, forKey: "tokenCostUsageEnabled") }    }     var randomBlinkEnabled: Bool {        didSet { self.userDefaults.set(self.randomBlinkEnabled, forKey: "randomBlinkEnabled") }    }     /// Optional: augment Claude usage with claude.ai web API (via Safari/Chrome/Firefox cookies),    /// incl. "Extra usage" spend.    var claudeWebExtrasEnabled: Bool {        didSet { self.userDefaults.set(self.claudeWebExtrasEnabled, forKey: "claudeWebExtrasEnabled") }    }     /// Optional: show Codex credits + Claude extra usage sections in the menu UI.    var showOptionalCreditsAndExtraUsage: Bool {        didSet {            self.userDefaults.set(self.showOptionalCreditsAndExtraUsage, forKey: "showOptionalCreditsAndExtraUsage")        }    }     private var claudeUsageDataSourceRaw: String? {        didSet {            if let raw = self.claudeUsageDataSourceRaw {                self.userDefaults.set(raw, forKey: "claudeUsageDataSource")            } else {                self.userDefaults.removeObject(forKey: "claudeUsageDataSource")            }        }    }     /// Optional: collapse provider icons into a single menu bar item with an in-menu switcher.    var mergeIcons: Bool {        didSet { self.userDefaults.set(self.mergeIcons, forKey: "mergeIcons") }    }     /// Optional: show provider icons in the in-menu switcher.    var switcherShowsIcons: Bool {        didSet { self.userDefaults.set(self.switcherShowsIcons, forKey: "switcherShowsIcons") }    }     /// z.ai API token (stored in Keychain).    var zaiAPIToken: String {        didSet { self.schedulePersistZaiAPIToken() }    }     private var selectedMenuProviderRaw: String? {        didSet {            if let raw = self.selectedMenuProviderRaw {                self.userDefaults.set(raw, forKey: "selectedMenuProvider")            } else {                self.userDefaults.removeObject(forKey: "selectedMenuProvider")            }        }    }     /// Optional override for the loading animation pattern, exposed via the Debug tab.    var debugLoadingPattern: LoadingPattern? {        get { self.debugLoadingPatternRaw.flatMap(LoadingPattern.init(rawValue:)) }        set {            self.debugLoadingPatternRaw = newValue?.rawValue        }    }     var selectedMenuProvider: UsageProvider? {        get { self.selectedMenuProviderRaw.flatMap(UsageProvider.init(rawValue:)) }        set {            self.selectedMenuProviderRaw = newValue?.rawValue        }    }     var claudeUsageDataSource: ClaudeUsageDataSource {        get { ClaudeUsageDataSource(rawValue: self.claudeUsageDataSourceRaw ?? "") ?? .web }        set {            self.claudeUsageDataSourceRaw = newValue.rawValue            if newValue != .cli {                self.claudeWebExtrasEnabled = false            }        }    }     var menuObservationToken: Int {        _ = self.providerOrderRaw        _ = self.refreshFrequency        _ = self.launchAtLogin        _ = self.debugMenuEnabled        _ = self.statusChecksEnabled        _ = self.sessionQuotaNotificationsEnabled        _ = self.usageBarsShowUsed        _ = self.ccusageCostUsageEnabled        _ = self.randomBlinkEnabled        _ = self.claudeWebExtrasEnabled        _ = self.showOptionalCreditsAndExtraUsage        _ = self.claudeUsageDataSource        _ = self.mergeIcons        _ = self.switcherShowsIcons        _ = self.zaiAPIToken        _ = self.debugLoadingPattern        _ = self.selectedMenuProvider        _ = self.providerToggleRevision        return 0    }     private var providerDetectionCompleted: Bool {        didSet { self.userDefaults.set(self.providerDetectionCompleted, forKey: "providerDetectionCompleted") }    }     @ObservationIgnored private let userDefaults: UserDefaults    @ObservationIgnored private let toggleStore: ProviderToggleStore    @ObservationIgnored private let zaiTokenStore: any ZaiTokenStoring    @ObservationIgnored private var zaiTokenPersistTask: Task<Void, Never>?    private var providerToggleRevision: Int = 0     init(userDefaults: UserDefaults = .standard, zaiTokenStore: any ZaiTokenStoring = KeychainZaiTokenStore()) {        self.userDefaults = userDefaults        self.zaiTokenStore = zaiTokenStore        self.providerOrderRaw = userDefaults.stringArray(forKey: "providerOrder") ?? []        let raw = userDefaults.string(forKey: "refreshFrequency") ?? RefreshFrequency.fiveMinutes.rawValue        self.refreshFrequency = RefreshFrequency(rawValue: raw) ?? .fiveMinutes        self.launchAtLogin = userDefaults.object(forKey: "launchAtLogin") as? Bool ?? false        self.debugMenuEnabled = userDefaults.object(forKey: "debugMenuEnabled") as? Bool ?? false        self.debugLoadingPatternRaw = userDefaults.string(forKey: "debugLoadingPattern")        self.statusChecksEnabled = userDefaults.object(forKey: "statusChecksEnabled") as? Bool ?? true        let sessionQuotaNotificationsDefault = userDefaults.object(            forKey: "sessionQuotaNotificationsEnabled") as? Bool        self.sessionQuotaNotificationsEnabled = sessionQuotaNotificationsDefault ?? true        if sessionQuotaNotificationsDefault == nil {            self.userDefaults.set(true, forKey: "sessionQuotaNotificationsEnabled")        }        self.usageBarsShowUsed = userDefaults.object(forKey: "usageBarsShowUsed") as? Bool ?? false        self.ccusageCostUsageEnabled = userDefaults.object(forKey: "tokenCostUsageEnabled") as? Bool ?? false        self.randomBlinkEnabled = userDefaults.object(forKey: "randomBlinkEnabled") as? Bool ?? false        self.claudeWebExtrasEnabled = userDefaults.object(forKey: "claudeWebExtrasEnabled") as? Bool ?? false        let creditsExtrasDefault = userDefaults.object(forKey: "showOptionalCreditsAndExtraUsage") as? Bool        self.showOptionalCreditsAndExtraUsage = creditsExtrasDefault ?? true        if creditsExtrasDefault == nil {            self.userDefaults.set(true, forKey: "showOptionalCreditsAndExtraUsage")        }        let claudeSourceRaw = userDefaults.string(forKey: "claudeUsageDataSource")        self.claudeUsageDataSourceRaw = claudeSourceRaw ?? ClaudeUsageDataSource.web.rawValue        self.mergeIcons = userDefaults.object(forKey: "mergeIcons") as? Bool ?? true        self.switcherShowsIcons = userDefaults.object(forKey: "switcherShowsIcons") as? Bool ?? true        self.zaiAPIToken = (try? zaiTokenStore.loadToken()) ?? ""        self.selectedMenuProviderRaw = userDefaults.string(forKey: "selectedMenuProvider")        self.providerDetectionCompleted = userDefaults.object(            forKey: "providerDetectionCompleted") as? Bool ?? false        self.toggleStore = ProviderToggleStore(userDefaults: userDefaults)        self.toggleStore.purgeLegacyKeys()        LaunchAtLoginManager.setEnabled(self.launchAtLogin)        self.runInitialProviderDetectionIfNeeded()        self.applyTokenCostDefaultIfNeeded()        if self.claudeUsageDataSource != .cli {            self.claudeWebExtrasEnabled = false        }    }     func orderedProviders() -> [UsageProvider] {        Self.effectiveProviderOrder(raw: self.providerOrderRaw)    }     func moveProvider(fromOffsets: IndexSet, toOffset: Int) {        var order = self.orderedProviders()        order.move(fromOffsets: fromOffsets, toOffset: toOffset)        self.providerOrderRaw = order.map(\.rawValue)    }     func isProviderEnabled(provider: UsageProvider, metadata: ProviderMetadata) -> Bool {        _ = self.providerToggleRevision        return self.toggleStore.isEnabled(metadata: metadata)    }     func setProviderEnabled(provider: UsageProvider, metadata: ProviderMetadata, enabled: Bool) {        self.providerToggleRevision &+= 1        self.toggleStore.setEnabled(enabled, metadata: metadata)    }     func rerunProviderDetection() {        self.runInitialProviderDetectionIfNeeded(force: true)    }     // MARK: - Private     func isCCUsageCostUsageEffectivelyEnabled(for provider: UsageProvider) -> Bool {        self.ccusageCostUsageEnabled && (provider == .codex || provider == .claude)    }     private static func effectiveProviderOrder(raw: [String]) -> [UsageProvider] {        var seen: Set<UsageProvider> = []        var ordered: [UsageProvider] = []         for rawValue in raw {            guard let provider = UsageProvider(rawValue: rawValue) else { continue }            guard !seen.contains(provider) else { continue }            seen.insert(provider)            ordered.append(provider)        }         if ordered.isEmpty {            ordered = UsageProvider.allCases            seen = Set(ordered)        }         if !seen.contains(.factory), let zaiIndex = ordered.firstIndex(of: .zai) {            ordered.insert(.factory, at: zaiIndex)            seen.insert(.factory)        }         for provider in UsageProvider.allCases where !seen.contains(provider) {            ordered.append(provider)        }         return ordered    }     private func runInitialProviderDetectionIfNeeded(force: Bool = false) {        guard force || !self.providerDetectionCompleted else { return }        guard let codexMeta = ProviderRegistry.shared.metadata[.codex],              let claudeMeta = ProviderRegistry.shared.metadata[.claude],              let geminiMeta = ProviderRegistry.shared.metadata[.gemini],              let antigravityMeta = ProviderRegistry.shared.metadata[.antigravity] else { return }         LoginShellPathCache.shared.captureOnce { [weak self] _ in            Task { @MainActor in                await self?.applyProviderDetection(                    codexMeta: codexMeta,                    claudeMeta: claudeMeta,                    geminiMeta: geminiMeta,                    antigravityMeta: antigravityMeta)            }        }    }     private func applyProviderDetection(        codexMeta: ProviderMetadata,        claudeMeta: ProviderMetadata,        geminiMeta: ProviderMetadata,        antigravityMeta: ProviderMetadata) async    {        guard !self.providerDetectionCompleted else { return }        let codexInstalled = BinaryLocator.resolveCodexBinary() != nil        let claudeInstalled = BinaryLocator.resolveClaudeBinary() != nil        let geminiInstalled = BinaryLocator.resolveGeminiBinary() != nil        let antigravityRunning = await AntigravityStatusProbe.isRunning()         // If none installed, keep Codex enabled to match previous behavior.        let noneInstalled = !codexInstalled && !claudeInstalled && !geminiInstalled && !antigravityRunning        let enableCodex = codexInstalled || noneInstalled        let enableClaude = claudeInstalled        let enableGemini = geminiInstalled        let enableAntigravity = antigravityRunning         self.providerToggleRevision &+= 1        self.toggleStore.setEnabled(enableCodex, metadata: codexMeta)        self.toggleStore.setEnabled(enableClaude, metadata: claudeMeta)        self.toggleStore.setEnabled(enableGemini, metadata: geminiMeta)        self.toggleStore.setEnabled(enableAntigravity, metadata: antigravityMeta)        self.providerDetectionCompleted = true    }     private func applyTokenCostDefaultIfNeeded() {        // Settings are persisted in UserDefaults.standard.        guard UserDefaults.standard.object(forKey: "tokenCostUsageEnabled") == nil else { return }         Task { @MainActor [weak self] in            guard let self else { return }            let hasSources = await Task.detached(priority: .utility) {                Self.hasAnyTokenCostUsageSources()            }.value            guard hasSources else { return }            guard UserDefaults.standard.object(forKey: "tokenCostUsageEnabled") == nil else { return }            self.ccusageCostUsageEnabled = true        }    }     nonisolated static func hasAnyTokenCostUsageSources(        env: [String: String] = ProcessInfo.processInfo.environment,        fileManager: FileManager = .default) -> Bool    {        func hasAnyJsonl(in root: URL) -> Bool {            guard fileManager.fileExists(atPath: root.path) else { return false }            guard let enumerator = fileManager.enumerator(                at: root,                includingPropertiesForKeys: [.isRegularFileKey],                options: [.skipsHiddenFiles, .skipsPackageDescendants])            else { return false }             for case let url as URL in enumerator where url.pathExtension.lowercased() == "jsonl" {                return true            }            return false        }         let codexRoot: URL = {            let raw = env["CODEX_HOME"]?.trimmingCharacters(in: .whitespacesAndNewlines)            if let raw, !raw.isEmpty {                return URL(fileURLWithPath: raw).appendingPathComponent("sessions", isDirectory: true)            }            return fileManager.homeDirectoryForCurrentUser                .appendingPathComponent(".codex", isDirectory: true)                .appendingPathComponent("sessions", isDirectory: true)        }()        if hasAnyJsonl(in: codexRoot) { return true }         let claudeRoots: [URL] = {            if let env = env["CLAUDE_CONFIG_DIR"]?.trimmingCharacters(in: .whitespacesAndNewlines),               !env.isEmpty            {                return env.split(separator: ",").map { part in                    let raw = String(part).trimmingCharacters(in: .whitespacesAndNewlines)                    let url = URL(fileURLWithPath: raw)                    if url.lastPathComponent == "projects" {                        return url                    }                    return url.appendingPathComponent("projects", isDirectory: true)                }            }             let home = fileManager.homeDirectoryForCurrentUser            return [                home.appendingPathComponent(".config/claude/projects", isDirectory: true),                home.appendingPathComponent(".claude/projects", isDirectory: true),            ]        }()         return claudeRoots.contains(where: hasAnyJsonl(in:))    }     private func schedulePersistZaiAPIToken() {        self.zaiTokenPersistTask?.cancel()        let token = self.zaiAPIToken        let tokenStore = self.zaiTokenStore        self.zaiTokenPersistTask = Task { @MainActor in            do {                try await Task.sleep(nanoseconds: 350_000_000)            } catch {                return            }            guard !Task.isCancelled else { return }            let error: (any Error)? = await Task.detached(priority: .utility) { () -> (any Error)? in                do {                    try tokenStore.storeToken(token)                    return nil                } catch {                    return error                }            }.value            if let error {                // Keep value in memory; persist best-effort.                CodexBarLog.logger("zai-token-store").error("Failed to persist z.ai token: \(error)")            }        }    }} enum LaunchAtLoginManager {    @MainActor    static func setEnabled(_ enabled: Bool) {        guard #available(macOS 13, *) else { return }        let service = SMAppService.mainApp        if enabled {            try? service.register()        } else {            try? service.unregister()        }    }}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t