import Foundation import Combine import SwiftUI enum AuthMode: String, Codable, CaseIterable, Identifiable, Sendable { case auto case manualSession case adminAPI var id: String { rawValue } var displayName: String { switch self { case .auto: "Auto" case .manualSession: "Manual token" case .adminAPI: "Admin API key" } } func displayName(for provider: ProviderID) -> String { switch (self, provider) { case (.auto, .claude): "Claude Code CLI" case (.auto, .cursor): "Auto-detect (Cursor IDE)" default: displayName } } } enum AppearanceMode: String, CaseIterable, Identifiable, Codable { case system case light case dark var id: String { rawValue } var displayName: String { switch self { case .system: "System" case .light: "Light" case .dark: "Dark" } } var colorScheme: ColorScheme? { switch self { case .system: nil case .light: .light case .dark: .dark } } } enum RefreshInterval: Int, CaseIterable, Identifiable, Codable { case oneMinute = 60 case fiveMinutes = 300 case fifteenMinutes = 900 var id: Int { rawValue } var displayName: String { switch self { case .oneMinute: "1 minute" case .fiveMinutes: "5 minutes" case .fifteenMinutes: "15 minutes" } } } struct ProviderSettings: Codable, Equatable, Sendable { var enabled: Bool var authMode: AuthMode var menuBarMetrics: [UsageMetric] var popoverMetrics: [UsageMetric] var menuBarLabel: String static func defaults(for provider: ProviderID) -> ProviderSettings { ProviderSettings( enabled: true, authMode: .auto, menuBarMetrics: UsageMetric.defaults(for: provider, location: .menuBar), popoverMetrics: UsageMetric.defaults(for: provider, location: .popover), menuBarLabel: provider.defaultMenuBarLabel ) } } @MainActor final class AppSettings: ObservableObject { static let shared = AppSettings() private enum Keys { static let providerSettings = "providerSettings" static let refreshInterval = "refreshInterval" static let launchAtLogin = "launchAtLogin" static let staleWarningThreshold = "staleWarningThreshold" static let appearanceMode = "appearanceMode" } @Published var providerSettings: [ProviderID: ProviderSettings] { didSet { saveProviderSettings() } } @Published private(set) var revision = 0 @Published var refreshInterval: RefreshInterval { didSet { UserDefaults.standard.set(refreshInterval.rawValue, forKey: Keys.refreshInterval) } } @Published var launchAtLogin: Bool { didSet { UserDefaults.standard.set(launchAtLogin, forKey: Keys.launchAtLogin) LaunchAtLoginService.setEnabled(launchAtLogin) } } @Published var staleWarningThreshold: Int { didSet { UserDefaults.standard.set(staleWarningThreshold, forKey: Keys.staleWarningThreshold) } } @Published var appearanceMode: AppearanceMode { didSet { UserDefaults.standard.set(appearanceMode.rawValue, forKey: Keys.appearanceMode) } } private init() { if let data = UserDefaults.standard.data(forKey: Keys.providerSettings), let decoded = try? JSONDecoder().decode([ProviderID: ProviderSettings].self, from: data) { providerSettings = decoded } else { providerSettings = Dictionary( uniqueKeysWithValues: ProviderID.allCases.map { ($0, ProviderSettings.defaults(for: $0)) } ) } let intervalRaw = UserDefaults.standard.integer(forKey: Keys.refreshInterval) refreshInterval = RefreshInterval(rawValue: intervalRaw == 0 ? RefreshInterval.fiveMinutes.rawValue : intervalRaw) ?? .fiveMinutes let savedLaunchAtLogin = UserDefaults.standard.bool(forKey: Keys.launchAtLogin) launchAtLogin = savedLaunchAtLogin let stale = UserDefaults.standard.integer(forKey: Keys.staleWarningThreshold) staleWarningThreshold = stale == 0 ? 3600 : stale if let savedAppearance = UserDefaults.standard.string(forKey: Keys.appearanceMode), let mode = AppearanceMode(rawValue: savedAppearance) { appearanceMode = mode } else { appearanceMode = .system } if savedLaunchAtLogin { LaunchAtLoginService.setEnabled(true) } ensureAtLeastOneProviderEnabled() } func settings(for provider: ProviderID) -> ProviderSettings { providerSettings[provider] ?? ProviderSettings.defaults(for: provider) } func updateSettings(for providerID: ProviderID, _ update: (inout ProviderSettings) -> Void) { var updated = providerSettings var providerConfig = updated[providerID] ?? ProviderSettings.defaults(for: providerID) let wasEnabled = providerConfig.enabled update(&providerConfig) if wasEnabled, !providerConfig.enabled, wouldDisableLastProvider(disabling: providerID, in: updated) { return } updated[providerID] = providerConfig providerSettings = updated revision += 1 } var enabledProviders: [ProviderID] { ProviderID.allCases.filter { settings(for: $0).enabled } } /// Providers shown in the menu bar — always at least one. var menuBarProviders: [ProviderID] { let enabled = enabledProviders return enabled.isEmpty ? [.cursor] : enabled } var canDisableProvider: (ProviderID) -> Bool { { providerID in guard self.settings(for: providerID).enabled else { return true } return self.enabledProviders.count > 1 } } private func wouldDisableLastProvider(disabling providerID: ProviderID, in settings: [ProviderID: ProviderSettings]) -> Bool { let enabledCount = ProviderID.allCases.filter { id in if id == providerID { return false } return settings[id]?.enabled ?? ProviderSettings.defaults(for: id).enabled }.count return enabledCount == 0 } private func ensureAtLeastOneProviderEnabled() { guard enabledProviders.isEmpty else { return } var updated = providerSettings for providerID in ProviderID.allCases { var config = updated[providerID] ?? ProviderSettings.defaults(for: providerID) config.enabled = true updated[providerID] = config } providerSettings = updated revision += 1 } private func saveProviderSettings() { guard let data = try? JSONEncoder().encode(providerSettings) else { return } UserDefaults.standard.set(data, forKey: Keys.providerSettings) } }