From d4bc25edd655ccd32646d51e527b4ea23550cb32 Mon Sep 17 00:00:00 2001 From: nsd97 Date: Sun, 19 Jul 2026 11:50:51 -0400 Subject: [PATCH] Show compact menu bar temperature only when Warm or Hot. Keep the template fan quiet while Good; optionally append an integer degree readout when system peak crosses the same thresholds as the popover header. Co-authored-by: Cursor --- ChillMac/App/AppSettings.swift | 2 ++ ChillMac/App/StatusBarController.swift | 35 ++++++++++++++++++++++- ChillMac/Fan/ThermalStatus.swift | 39 ++++++++++++++++++++++++++ ChillMac/Views/PopoverView.swift | 24 +++++++++------- ChillMac/Views/SettingsView.swift | 28 ++++++++++++++++++ 5 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 ChillMac/Fan/ThermalStatus.swift diff --git a/ChillMac/App/AppSettings.swift b/ChillMac/App/AppSettings.swift index 4c90ed6..edb1514 100644 --- a/ChillMac/App/AppSettings.swift +++ b/ChillMac/App/AppSettings.swift @@ -77,6 +77,8 @@ final class AppSettings: ObservableObject { @AppStorage("forcePerformanceOnBattery") var forcePerformanceOnBattery = false @AppStorage("keepFansOnScreenSleep") var keepFansOnScreenSleep = false @AppStorage("showFPS") var showFPS = false + /// When on, the menu bar shows a compact temp next to the fan only while Warm/Hot. + @AppStorage("showMenuBarTemp") var showMenuBarTemp = true static let popoverMinHeight: CGFloat = 400 static let popoverMaxHeight: CGFloat = 900 diff --git a/ChillMac/App/StatusBarController.swift b/ChillMac/App/StatusBarController.swift index f66fa31..7c12e3e 100644 --- a/ChillMac/App/StatusBarController.swift +++ b/ChillMac/App/StatusBarController.swift @@ -7,10 +7,12 @@ final class StatusBarController: NSObject { private let popover: NSPopover private var eventMonitor: Any? private var settingsSub: AnyCancellable? + private var statusItemSubs = Set() private var heightObserver: Any? private var detailResetObserver: Any? private var detailPanelObserver: Any? private var lastPopoverHeight: CGFloat = 0 + private var lastStatusItemSignature: String? private let detailPanel = DetailPanelController() private let memoryInfo: MemoryInfo @@ -46,10 +48,11 @@ final class StatusBarController: NSObject { popover.contentSize = NSSize(width: 420, height: CGFloat(AppSettings.shared.popoverHeight)) if let button = statusItem.button { - button.image = NSImage(systemSymbolName: "fan.fill", accessibilityDescription: "ChillMac") + button.imagePosition = .imageLeft button.action = #selector(togglePopover(_:)) button.target = self } + updateStatusItem() // Close popover when clicking outside both the popover and detail panel eventMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { [weak self] _ in @@ -83,6 +86,7 @@ final class StatusBarController: NSObject { guard let self else { return } self.popover.appearance = AppSettings.shared.nsAppearance self.popover.contentViewController?.view.appearance = AppSettings.shared.nsAppearance + self.updateStatusItem() // Handle height changes from settings (e.g. Reset button), not during live drag let newHeight = CGFloat(AppSettings.shared.popoverHeight) @@ -94,6 +98,11 @@ final class StatusBarController: NSObject { } } + fanMonitor.$peakTemperature + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in self?.updateStatusItem() } + .store(in: &statusItemSubs) + // Live resize during drag — bypasses AppSettings for smooth performance heightObserver = NotificationCenter.default.addObserver(forName: .popoverHeightChanged, object: nil, queue: .main) { [weak self] notification in guard let self, let height = notification.userInfo?["height"] as? CGFloat else { return } @@ -116,6 +125,30 @@ final class StatusBarController: NSObject { } } + /// Template fan always; compact temp title only when setting is on and status is Warm/Hot. + private func updateStatusItem() { + guard let button = statusItem.button else { return } + + let peak = fanMonitor.peakTemperature + let thermal = ThermalStatus.from(peakCelsius: peak) + let settings = AppSettings.shared + let showTemp = settings.showMenuBarTemp && thermal.showsMenuBarTemperature + let title = showTemp + ? ThermalStatus.menuBarTemperatureText(celsius: peak, useFahrenheit: settings.useFahrenheit) + : "" + let signature = "\(showTemp)|\(title)|\(settings.useFahrenheit)|\(thermal.label)" + guard signature != lastStatusItemSignature else { return } + lastStatusItemSignature = signature + + let image = NSImage(systemSymbolName: "fan.fill", accessibilityDescription: nil) + image?.isTemplate = true + button.image = image + button.title = title + button.imagePosition = showTemp ? .imageLeft : .imageOnly + button.toolTip = showTemp ? "ChillMac — \(thermal.label) \(title)" : "ChillMac" + button.setAccessibilityLabel(showTemp ? "ChillMac, \(thermal.label), \(title)" : "ChillMac") + } + deinit { if let monitor = eventMonitor { NSEvent.removeMonitor(monitor) diff --git a/ChillMac/Fan/ThermalStatus.swift b/ChillMac/Fan/ThermalStatus.swift new file mode 100644 index 0000000..50edc60 --- /dev/null +++ b/ChillMac/Fan/ThermalStatus.swift @@ -0,0 +1,39 @@ +import Foundation + +/// Shared Good / Warm / Hot vocabulary for popover + menu bar. +enum ThermalStatus: Equatable { + case unknown + case good + case warm + case hot + + /// Same thresholds as the popover header: Warm ≥ 75°C, Hot ≥ 90°C. + static func from(peakCelsius: Double) -> ThermalStatus { + guard peakCelsius > 0 else { return .unknown } + if peakCelsius >= 90 { return .hot } + if peakCelsius >= 75 { return .warm } + return .good + } + + var label: String { + switch self { + case .unknown, .good: return "Good" + case .warm: return "Warm" + case .hot: return "Hot" + } + } + + /// Menu bar stays quiet unless the machine is Warm or Hot. + var showsMenuBarTemperature: Bool { + self == .warm || self == .hot + } + + /// Compact monochrome readout for the status item (integer degrees). + static func menuBarTemperatureText(celsius: Double, useFahrenheit: Bool) -> String { + if useFahrenheit { + let f = celsius * 9.0 / 5.0 + 32.0 + return "\(Int(f.rounded()))°" + } + return "\(Int(celsius.rounded()))°" + } +} diff --git a/ChillMac/Views/PopoverView.swift b/ChillMac/Views/PopoverView.swift index 1566bdb..e3d30c5 100644 --- a/ChillMac/Views/PopoverView.swift +++ b/ChillMac/Views/PopoverView.swift @@ -138,21 +138,25 @@ struct PopoverView: View { .animation(.easeOut(duration: 0.3), value: appeared) } + private var currentThermalStatus: ThermalStatus { + guard !monitor.sensors.isEmpty, + let maxTemp = monitor.sensors.map(\.temperature).max() else { + return .unknown + } + return ThermalStatus.from(peakCelsius: maxTemp) + } + private var thermalStatus: String { - guard !monitor.sensors.isEmpty else { return "Good" } - let maxTemp = monitor.sensors.map(\.temperature).max() ?? 0 - if maxTemp >= 90 { return "Hot" } - if maxTemp >= 75 { return "Warm" } - return "Good" + currentThermalStatus.label } private var thermalStatusColor: Color { - guard !monitor.sensors.isEmpty else { return .green } let isLight = (settings.preferredColorScheme ?? colorScheme) == .light - let maxTemp = monitor.sensors.map(\.temperature).max() ?? 0 - if maxTemp >= 90 { return .red } - if maxTemp >= 75 { return isLight ? Color(red: 0.80, green: 0.45, blue: 0.0) : .orange } - return .green + switch currentThermalStatus { + case .hot: return .red + case .warm: return isLight ? Color(red: 0.80, green: 0.45, blue: 0.0) : .orange + case .good, .unknown: return .green + } } private var maxTempDisplay: String { diff --git a/ChillMac/Views/SettingsView.swift b/ChillMac/Views/SettingsView.swift index ab1747a..e629370 100644 --- a/ChillMac/Views/SettingsView.swift +++ b/ChillMac/Views/SettingsView.swift @@ -345,6 +345,34 @@ struct SettingsView: View { .padding(.horizontal, 14) .padding(.vertical, 10) + Divider() + .background(theme.dividerSubtle) + + // Menu bar temperature (Warm/Hot only) + HStack { + Image(systemName: "menubar.arrow.up.rectangle") + .font(.system(size: 16)) + .foregroundColor(theme.textTertiary) + .frame(width: 24) + VStack(alignment: .leading, spacing: 2) { + Text("Show Temp in Menu Bar") + .font(.system(size: 13, weight: .medium)) + .foregroundColor(theme.textPrimary) + Text("Only when Warm or Hot — stays quiet when Good") + .font(.system(size: 11)) + .foregroundColor(theme.textQuaternary) + } + Spacer() + Toggle(isOn: $settings.showMenuBarTemp) { + EmptyView() + } + .toggleStyle(.switch) + .controlSize(.small) + .tint(.teal) + } + .padding(.horizontal, 14) + .padding(.vertical, 10) + Divider() .background(theme.dividerSubtle)