285 lines · 9.9 KB · Swift
RawDownload
import AppKitimport CodexBarCoreimport KeyboardShortcutsimport Observationimport QuartzCoreimport Securityimport SwiftUI @mainstruct CodexBarApp: App {    @NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate    @State private var settings: SettingsStore    @State private var store: UsageStore    private let preferencesSelection: PreferencesSelection    private let account: AccountInfo     init() {        let env = ProcessInfo.processInfo.environment        let level = CodexBarLog.parseLevel(env["CODEXBAR_LOG_LEVEL"]) ?? .info        CodexBarLog.bootstrapIfNeeded(.init(            destination: .oslog(subsystem: "com.steipete.codexbar"),            level: level,            json: false))         KeychainPromptCoordinator.install()         // Migrate keychain items to reduce permission prompts during development        KeychainMigration.migrateIfNeeded()         let preferencesSelection = PreferencesSelection()        let settings = SettingsStore()        let fetcher = UsageFetcher()        let browserDetection = BrowserDetection(cacheTTL: BrowserDetection.defaultCacheTTL)        let account = fetcher.loadAccountInfo()        let store = UsageStore(fetcher: fetcher, browserDetection: browserDetection, settings: settings)        self.preferencesSelection = preferencesSelection        _settings = State(wrappedValue: settings)        _store = State(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 automaticallyDownloadsUpdates: Bool { get set }    var isAvailable: Bool { get }    var unavailableReason: String? { get }    var updateStatus: UpdateStatus { 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    var automaticallyDownloadsUpdates: Bool = false    let isAvailable: Bool = false    let unavailableReason: String?    let updateStatus = UpdateStatus()     init(unavailableReason: String? = nil) {        self.unavailableReason = unavailableReason    }     func checkForUpdates(_ sender: Any?) {}} @MainActor@Observablefinal class UpdateStatus {    static let disabled = UpdateStatus()    var isUpdateReady: Bool     init(isUpdateReady: Bool = false) {        self.isUpdateReady = isUpdateReady    }} #if canImport(Sparkle) && ENABLE_SPARKLEimport Sparkle @MainActorfinal class SparkleUpdaterController: NSObject, UpdaterProviding, SPUUpdaterDelegate {    private lazy var controller = SPUStandardUpdaterController(        startingUpdater: false,        updaterDelegate: self,        userDriverDelegate: nil)    let updateStatus = UpdateStatus()    let unavailableReason: String? = nil     init(savedAutoUpdate: Bool) {        super.init()        let updater = self.controller.updater        updater.automaticallyChecksForUpdates = savedAutoUpdate        updater.automaticallyDownloadsUpdates = savedAutoUpdate        self.controller.startUpdater()    }     var automaticallyChecksForUpdates: Bool {        get { self.controller.updater.automaticallyChecksForUpdates }        set { self.controller.updater.automaticallyChecksForUpdates = newValue }    }     var automaticallyDownloadsUpdates: Bool {        get { self.controller.updater.automaticallyDownloadsUpdates }        set { self.controller.updater.automaticallyDownloadsUpdates = newValue }    }     var isAvailable: Bool { true }     func checkForUpdates(_ sender: Any?) {        self.controller.checkForUpdates(sender)    }     nonisolated func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {        Task { @MainActor in            self.updateStatus.isUpdateReady = true        }    }     nonisolated func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {        Task { @MainActor in            self.updateStatus.isUpdateReady = false        }    }     nonisolated func userDidCancelDownload(_ updater: SPUUpdater) {        Task { @MainActor in            self.updateStatus.isUpdateReady = false        }    }     nonisolated func updater(        _ updater: SPUUpdater,        userDidMake choice: SPUUserUpdateChoice,        forUpdate updateItem: SUAppcastItem,        state: SPUUserUpdateState)    {        let downloaded = state.stage == .downloaded        Task { @MainActor in            switch choice {            case .install, .skip:                self.updateStatus.isUpdateReady = false            case .dismiss:                self.updateStatus.isUpdateReady = downloaded            @unknown default:                self.updateStatus.isUpdateReady = false            }        }    }} 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} @MainActorprivate func makeUpdaterController() -> UpdaterProviding {    let bundleURL = Bundle.main.bundleURL    let isBundledApp = bundleURL.pathExtension == "app"    guard isBundledApp else {        return DisabledUpdaterController(unavailableReason: "Updates unavailable in this build.")    }     if InstallOrigin.isHomebrewCask(appBundleURL: bundleURL) {        return DisabledUpdaterController(            unavailableReason: "Updates managed by Homebrew. Run: brew upgrade --cask steipete/tap/codexbar")    }     guard isDeveloperIDSigned(bundleURL: bundleURL) else {        return DisabledUpdaterController(unavailableReason: "Updates unavailable in this build.")    }     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    return SparkleUpdaterController(savedAutoUpdate: savedAutoUpdate)}#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) {        AppNotifications.shared.requestAuthorizationOnStartup()        self.ensureStatusController()        KeyboardShortcuts.onKeyUp(for: .openMenu) { [weak self] in            Task { @MainActor [weak self] in                self?.statusController?.openMenuFromShortcut()            }        }    }     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 browserDetection = BrowserDetection(cacheTTL: BrowserDetection.defaultCacheTTL)        let fallbackAccount = fetcher.loadAccountInfo()        let fallbackStore = UsageStore(fetcher: fetcher, browserDetection: browserDetection, settings: fallbackSettings)        self.statusController = StatusItemController.factory(            fallbackStore,            fallbackSettings,            fallbackAccount,            self.updaterController,            PreferencesSelection())    }}