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. Load more
import AppKit import CodexBarCore import Combine import OSLog import QuartzCore import Security import SwiftUI @preconcurrency import UserNotifications @main struct 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 @MainActor protocol 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_SPARKLE import 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 } #else private func makeUpdaterController() -> UpdaterProviding { DisabledUpdaterController() } #endif @MainActor final 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 {} @MainActor final 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 } }