diff --git a/ChillMac/App/AppSettings.swift b/ChillMac/App/AppSettings.swift index 4c90ed6..9917a59 100644 --- a/ChillMac/App/AppSettings.swift +++ b/ChillMac/App/AppSettings.swift @@ -117,7 +117,10 @@ final class AppSettings: ObservableObject { } func syncLaunchAtLogin() { - launchAtLogin = (SMAppService.mainApp.status == .enabled) + let enabled = SMAppService.mainApp.status == .enabled + if launchAtLogin != enabled { + launchAtLogin = enabled + } } func setLaunchAtLogin(_ enabled: Bool) { diff --git a/ChillMac/App/StatusBarController.swift b/ChillMac/App/StatusBarController.swift index f66fa31..2a7d38e 100644 --- a/ChillMac/App/StatusBarController.swift +++ b/ChillMac/App/StatusBarController.swift @@ -148,12 +148,9 @@ final class StatusBarController: NSObject { fpsMonitor.stopMonitoring() fanMonitor.isPopoverVisible = false } else if let button = statusItem.button { - // Resume secondary monitors when popover opens - cpuInfo.startMonitoring() - memoryInfo.startMonitoring() - batteryInfo.startMonitoring() - systemInfo.startMonitoring() - fpsMonitor.startMonitoring() + // Sync settings and show the hosting view first, then start monitors on the + // next turn — otherwise their @Published writes land mid-body and spam + // "Publishing changes from within view updates". fanMonitor.isPopoverVisible = true AppSettings.shared.syncLaunchAtLogin() NotificationCenter.default.post(name: .popoverDidClose, object: nil) @@ -162,6 +159,14 @@ final class StatusBarController: NSObject { NSApp.activate(ignoringOtherApps: true) popover.contentViewController?.view.window?.makeKeyAndOrderFront(nil) NotificationCenter.default.post(name: .popoverDidShow, object: nil) + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.cpuInfo.startMonitoring() + self.memoryInfo.startMonitoring() + self.batteryInfo.startMonitoring() + self.systemInfo.startMonitoring() + self.fpsMonitor.startMonitoring() + } } } diff --git a/ChillMac/Fan/CpuInfo.swift b/ChillMac/Fan/CpuInfo.swift index 9b9ebb9..43b22ae 100644 --- a/ChillMac/Fan/CpuInfo.swift +++ b/ChillMac/Fan/CpuInfo.swift @@ -60,26 +60,47 @@ final class CpuInfo: ObservableObject { previousInfo = current + let newUser: Double + let newSystem: Double + let newIdle: Double + let newTotal: Double if totalDiff > 0 { - userPercent = (userDiff + niceDiff) / totalDiff * 100 - systemPercent = sysDiff / totalDiff * 100 - idlePercent = idleDiff / totalDiff * 100 - totalUsage = 100 - idlePercent + newUser = (userDiff + niceDiff) / totalDiff * 100 + newSystem = sysDiff / totalDiff * 100 + newIdle = idleDiff / totalDiff * 100 + newTotal = 100 - newIdle + } else { + newUser = userPercent + newSystem = systemPercent + newIdle = idlePercent + newTotal = totalUsage } - history.append(totalUsage) - if history.count > maxHistory { - history.removeFirst(history.count - maxHistory) - } + // Defer publishes so Timer callbacks don't write mid-body while the popover is rendering. + DispatchQueue.main.async { [weak self] in + guard let self else { return } + if abs(self.userPercent - newUser) >= 0.1 { self.userPercent = newUser } + if abs(self.systemPercent - newSystem) >= 0.1 { self.systemPercent = newSystem } + if abs(self.idlePercent - newIdle) >= 0.1 { self.idlePercent = newIdle } + if abs(self.totalUsage - newTotal) >= 0.1 { self.totalUsage = newTotal } - userHistory.append(userPercent) - if userHistory.count > maxHistory { - userHistory.removeFirst(userHistory.count - maxHistory) - } + // History charts only matter with the CPU detail panel open. + guard self.isDetailVisible else { return } + + self.history.append(newTotal) + if self.history.count > self.maxHistory { + self.history.removeFirst(self.history.count - self.maxHistory) + } - systemHistory.append(systemPercent) - if systemHistory.count > maxHistory { - systemHistory.removeFirst(systemHistory.count - maxHistory) + self.userHistory.append(newUser) + if self.userHistory.count > self.maxHistory { + self.userHistory.removeFirst(self.userHistory.count - self.maxHistory) + } + + self.systemHistory.append(newSystem) + if self.systemHistory.count > self.maxHistory { + self.systemHistory.removeFirst(self.systemHistory.count - self.maxHistory) + } } // Fetch top processes every 5th poll, only when detail panel is visible.