1059 lines · 39.8 KB · Swift
RawDownload
import AppKitimport CodexBarCoreimport Combineimport OSLogimport QuartzCoreimport Securityimport SwiftUI@preconcurrency import UserNotifications @mainstruct CodexBarApp: App {    @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate    @StateObject private var settings: SettingsStore    @StateObject private var store: UsageStore    private let preferencesSelection: PreferencesSelection    private let account: AccountInfo     init() {        let preferencesSelection = PreferencesSelection()        let settings = SettingsStore()        let fetcher = UsageFetcher()        let account = fetcher.loadAccountInfo()        let store = UsageStore(fetcher: fetcher, settings: settings)        self.preferencesSelection = preferencesSelection        _settings = StateObject(wrappedValue: settings)        _store = StateObject(wrappedValue: store)        self.account = account        self.appDelegate.configure(            store: store,            settings: settings,            account: account,            selection: preferencesSelection)    }     @SceneBuilder    var body: some Scene {        // Hidden 1×1 window to keep SwiftUI's lifecycle alive so `Settings` scene        // shows the native toolbar tabs even though the UI is AppKit-based.        WindowGroup("CodexBarLifecycleKeepalive") {            HiddenWindowView()        }        .defaultSize(width: 20, height: 20)        .windowStyle(.hiddenTitleBar)         Settings {            PreferencesView(                settings: self.settings,                store: self.store,                updater: self.appDelegate.updaterController,                selection: self.preferencesSelection)        }        .defaultSize(width: PreferencesTab.windowWidth, height: PreferencesTab.general.preferredHeight)        .windowResizability(.contentSize)    }     private func openSettings(tab: PreferencesTab) {        self.preferencesSelection.tab = tab        NSApp.activate(ignoringOtherApps: true)        _ = NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil)    }} // MARK: - Updater abstraction @MainActorprotocol UpdaterProviding: AnyObject {    var automaticallyChecksForUpdates: Bool { get set }    var isAvailable: Bool { get }    func checkForUpdates(_ sender: Any?)} // No-op updater used for debug builds and non-bundled runs to suppress Sparkle dialogs.final class DisabledUpdaterController: UpdaterProviding {    var automaticallyChecksForUpdates: Bool = false    let isAvailable: Bool = false    func checkForUpdates(_ sender: Any?) {}} #if canImport(Sparkle) && ENABLE_SPARKLEimport Sparkle extension SPUStandardUpdaterController: UpdaterProviding {    var automaticallyChecksForUpdates: Bool {        get { self.updater.automaticallyChecksForUpdates }        set { self.updater.automaticallyChecksForUpdates = newValue }    }     var isAvailable: Bool { true }} private func isDeveloperIDSigned(bundleURL: URL) -> Bool {    var staticCode: SecStaticCode?    guard SecStaticCodeCreateWithPath(bundleURL as CFURL, SecCSFlags(), &staticCode) == errSecSuccess,          let code = staticCode else { return false }     var infoCF: CFDictionary?    guard SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &infoCF) == errSecSuccess,          let info = infoCF as? [String: Any],          let certs = info[kSecCodeInfoCertificates as String] as? [SecCertificate],          let leaf = certs.first else { return false }     if let summary = SecCertificateCopySubjectSummary(leaf) as String? {        return summary.hasPrefix("Developer ID Application:")    }    return false} private func makeUpdaterController() -> UpdaterProviding {    let bundleURL = Bundle.main.bundleURL    let isBundledApp = bundleURL.pathExtension == "app"    guard isBundledApp, isDeveloperIDSigned(bundleURL: bundleURL) else { return DisabledUpdaterController() }     let defaults = UserDefaults.standard    let autoUpdateKey = "autoUpdateEnabled"    // Default to true for first launch; fall back to saved preference thereafter.    let savedAutoUpdate = (defaults.object(forKey: autoUpdateKey) as? Bool) ?? true     let controller = SPUStandardUpdaterController(        startingUpdater: false,        updaterDelegate: nil,        userDriverDelegate: nil)    controller.updater.automaticallyChecksForUpdates = savedAutoUpdate    controller.startUpdater()    return controller}#elseprivate func makeUpdaterController() -> UpdaterProviding {    DisabledUpdaterController()}#endif @MainActorfinal class AppDelegate: NSObject, NSApplicationDelegate {    let updaterController: UpdaterProviding = makeUpdaterController()    private var statusController: StatusItemControlling?    private var store: UsageStore?    private var settings: SettingsStore?    private var account: AccountInfo?    private var preferencesSelection: PreferencesSelection?     func configure(store: UsageStore, settings: SettingsStore, account: AccountInfo, selection: PreferencesSelection) {        self.store = store        self.settings = settings        self.account = account        self.preferencesSelection = selection    }     func applicationDidFinishLaunching(_ notification: Notification) {        self.ensureStatusController()    }     private func ensureStatusController() {        if self.statusController != nil { return }         if let store, let settings, let account, let selection = self.preferencesSelection {            self.statusController = StatusItemController.factory(                store,                settings,                account,                self.updaterController,                selection)            return        }         // Defensive fallback: this should not be hit in normal app lifecycle.        let fallbackSettings = SettingsStore()        let fetcher = UsageFetcher()        let fallbackAccount = fetcher.loadAccountInfo()        let fallbackStore = UsageStore(fetcher: fetcher, settings: fallbackSettings)        self.statusController = StatusItemController.factory(            fallbackStore,            fallbackSettings,            fallbackAccount,            self.updaterController,            PreferencesSelection())    }} extension CodexBarApp {    private var codexSnapshot: UsageSnapshot? { self.store.snapshot(for: .codex) }    private var claudeSnapshot: UsageSnapshot? { self.store.snapshot(for: .claude) }    private var codexShouldAnimate: Bool {        self.store.isEnabled(.codex) && self.codexSnapshot == nil && !self.store.isStale(provider: .codex)    }     private var claudeShouldAnimate: Bool {        self.store.isEnabled(.claude) && self.claudeSnapshot == nil && !self.store.isStale(provider: .claude)    }} // MARK: - Status item controller (AppKit-hosted icons, SwiftUI popovers) protocol StatusItemControlling: AnyObject {} @MainActorfinal class StatusItemController: NSObject, NSMenuDelegate, StatusItemControlling {    typealias Factory = (UsageStore, SettingsStore, AccountInfo, UpdaterProviding, PreferencesSelection)        -> StatusItemControlling    static let defaultFactory: Factory = { store, settings, account, updater, selection in        StatusItemController(            store: store,            settings: settings,            account: account,            updater: updater,            preferencesSelection: selection)    }     static var factory: Factory = StatusItemController.defaultFactory     private let store: UsageStore    private let settings: SettingsStore    private let account: AccountInfo    private let updater: UpdaterProviding    private var statusItems: [UsageProvider: NSStatusItem] = [:]    private(set) var lastMenuProvider: UsageProvider?    private var menuProviders: [ObjectIdentifier: UsageProvider] = [:]    private var blinkTask: Task<Void, Never>?    private var loginTask: Task<Void, Never>? {        didSet { self.refreshMenusForLoginStateChange() }    }     private var activeLoginProvider: UsageProvider? {        didSet {            if oldValue != self.activeLoginProvider {                self.refreshMenusForLoginStateChange()            }        }    }     private var blinkStates: [UsageProvider: BlinkState] = [:]    private var blinkAmounts: [UsageProvider: CGFloat] = [:]    private var wiggleAmounts: [UsageProvider: CGFloat] = [:]    private var tiltAmounts: [UsageProvider: CGFloat] = [:]    private var blinkForceUntil: Date?    private var cancellables = Set<AnyCancellable>()    private var loginPhase: LoginPhase = .idle {        didSet {            if oldValue != self.loginPhase {                self.refreshMenusForLoginStateChange()            }        }    }     private let preferencesSelection: PreferencesSelection    private var animationDisplayLink: CADisplayLink?    private var animationPhase: Double = 0    private var animationPattern: LoadingPattern = .knightRider    private let loginLogger = Logger(subsystem: "com.steipete.codexbar", category: "login")     private struct BlinkState {        var nextBlink: Date        var blinkStart: Date?        var pendingSecondStart: Date?        var effect: MotionEffect = .blink         static func randomDelay() -> TimeInterval {            Double.random(in: 3...12)        }    }     private enum MotionEffect {        case blink        case wiggle        case tilt    }     private enum LoginPhase {        case idle        case requesting        case waitingBrowser    }     init(        store: UsageStore,        settings: SettingsStore,        account: AccountInfo,        updater: UpdaterProviding,        preferencesSelection: PreferencesSelection)    {        self.store = store        self.settings = settings        self.account = account        self.updater = updater        self.preferencesSelection = preferencesSelection        let bar = NSStatusBar.system        for provider in UsageProvider.allCases {            self.statusItems[provider] = bar.statusItem(withLength: NSStatusItem.variableLength)        }        super.init()        self.wireBindings()        self.updateIcons()        self.updateVisibility()        NotificationCenter.default.addObserver(            self,            selector: #selector(self.handleDebugReplayNotification(_:)),            name: .codexbarDebugReplayAllAnimations,            object: nil)        NotificationCenter.default.addObserver(            self,            selector: #selector(self.handleDebugBlinkNotification),            name: .codexbarDebugBlinkNow,            object: nil)    }     private func wireBindings() {        self.store.objectWillChange            .receive(on: RunLoop.main)            .sink { [weak self] _ in                self?.updateIcons()                self?.updateBlinkingState()            }            .store(in: &self.cancellables)         self.store.$debugForceAnimation            .receive(on: RunLoop.main)            .sink { [weak self] _ in                self?.updateVisibility()                self?.updateBlinkingState()            }            .store(in: &self.cancellables)         self.settings.objectWillChange            .receive(on: RunLoop.main)            .sink { [weak self] _ in                self?.updateVisibility()                self?.updateBlinkingState()            }            .store(in: &self.cancellables)    }     private func installButtonsIfNeeded() {        // No button actions needed when menus are attached directly.    }     private func updateIcons() {        UsageProvider.allCases.forEach { self.applyIcon(for: $0, phase: nil) }        self.attachMenus(fallback: self.fallbackProvider)        self.updateAnimationState()        self.updateBlinkingState()    }     private func updateVisibility() {        let fallback = self.fallbackProvider        for provider in UsageProvider.allCases {            let item = self.statusItems[provider]            let isEnabled = self.isEnabled(provider)            let force = self.store.debugForceAnimation            item?.isVisible = isEnabled || fallback == provider || force        }        self.attachMenus(fallback: fallback)        self.updateAnimationState()        self.updateBlinkingState()    }     private var fallbackProvider: UsageProvider? {        self.store.enabledProviders().isEmpty ? .codex : nil    }     private func isEnabled(_ provider: UsageProvider) -> Bool {        self.store.isEnabled(provider)    }     private func refreshMenusForLoginStateChange() {        self.attachMenus(fallback: self.fallbackProvider)    }     private func attachMenus(fallback: UsageProvider? = nil) {        self.menuProviders.removeAll()        for provider in UsageProvider.allCases {            guard let item = self.statusItems[provider] else { continue }            if self.isEnabled(provider) {                item.menu = self.makeMenu(for: provider)            } else if fallback == provider {                item.menu = self.makeMenu(for: nil)            } else {                item.menu = nil            }        }    }     private func updateBlinkingState() {        let blinkingEnabled = self.isBlinkingAllowed()        let anyVisible = UsageProvider.allCases.contains { self.isVisible($0) }        if blinkingEnabled, anyVisible {            if self.blinkTask == nil {                self.seedBlinkStatesIfNeeded()                self.blinkTask = Task { [weak self] in                    while !Task.isCancelled {                        try? await Task.sleep(for: .milliseconds(75))                        await MainActor.run { self?.tickBlink() }                    }                }            }        } else {            self.stopBlinking()        }    }     private func seedBlinkStatesIfNeeded() {        let now = Date()        for provider in UsageProvider.allCases where self.blinkStates[provider] == nil {            self.blinkStates[provider] = BlinkState(nextBlink: now.addingTimeInterval(BlinkState.randomDelay()))        }    }     private func stopBlinking() {        self.blinkTask?.cancel()        self.blinkTask = nil        self.blinkAmounts.removeAll()        UsageProvider.allCases.forEach { self.applyIcon(for: $0, phase: nil) }    }     private func tickBlink(now: Date = .init()) {        guard self.isBlinkingAllowed(at: now) else {            self.stopBlinking()            return        }         let blinkDuration: TimeInterval = 0.36        let doubleBlinkChance = 0.18        let doubleDelayRange: ClosedRange<TimeInterval> = 0.22...0.34         for provider in UsageProvider.allCases {            guard self.isVisible(provider), !self.shouldAnimate(provider: provider) else {                self.clearMotion(for: provider)                continue            }             var state = self                .blinkStates[provider] ?? BlinkState(nextBlink: now.addingTimeInterval(BlinkState.randomDelay()))             if let pendingSecond = state.pendingSecondStart, now >= pendingSecond {                state.blinkStart = now                state.pendingSecondStart = nil            }             if let start = state.blinkStart {                let elapsed = now.timeIntervalSince(start)                if elapsed >= blinkDuration {                    state.blinkStart = nil                    if let pending = state.pendingSecondStart, now < pending {                        // Wait for the planned double-blink.                    } else {                        state.pendingSecondStart = nil                        state.nextBlink = now.addingTimeInterval(BlinkState.randomDelay())                    }                    self.clearMotion(for: provider)                } else {                    let progress = max(0, min(elapsed / blinkDuration, 1))                    let symmetric = progress < 0.5 ? progress * 2 : (1 - progress) * 2                    let eased = pow(symmetric, 2.2) // slightly punchier than smoothstep                    self.assignMotion(amount: CGFloat(eased), for: provider, effect: state.effect)                }            } else if now >= state.nextBlink {                state.blinkStart = now                state.effect = self.randomEffect(for: provider)                if state.effect == .blink, Double.random(in: 0...1) < doubleBlinkChance {                    state.pendingSecondStart = now.addingTimeInterval(Double.random(in: doubleDelayRange))                }                self.clearMotion(for: provider)            } else {                self.clearMotion(for: provider)            }             self.blinkStates[provider] = state            self.applyIcon(for: provider, phase: nil)        }    }     private func blinkAmount(for provider: UsageProvider) -> CGFloat {        guard self.isBlinkingAllowed() else { return 0 }        return self.blinkAmounts[provider] ?? 0    }     private func wiggleAmount(for provider: UsageProvider) -> CGFloat {        guard self.isBlinkingAllowed() else { return 0 }        return self.wiggleAmounts[provider] ?? 0    }     private func tiltAmount(for provider: UsageProvider) -> CGFloat {        guard self.isBlinkingAllowed() else { return 0 }        return self.tiltAmounts[provider] ?? 0    }     private func assignMotion(amount: CGFloat, for provider: UsageProvider, effect: MotionEffect) {        switch effect {        case .blink:            self.blinkAmounts[provider] = amount            self.wiggleAmounts[provider] = 0            self.tiltAmounts[provider] = 0        case .wiggle:            self.wiggleAmounts[provider] = amount            self.blinkAmounts[provider] = 0            self.tiltAmounts[provider] = 0        case .tilt:            self.tiltAmounts[provider] = amount            self.blinkAmounts[provider] = 0            self.wiggleAmounts[provider] = 0        }    }

Lines 1–500 of 1059.