From 51b698d189140d8f87f45ebcb9e7549979de32e5 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Fri, 14 Nov 2025 23:25:39 +0530 Subject: [PATCH] Frequency and Timeout settings --- DynamicIsland/Localizable.xcstrings | 21 ++++ .../components/Settings/SettingsView.swift | 56 +++++++++++ DynamicIsland/managers/StatsManager.swift | 99 ++++++++++++++++--- DynamicIsland/models/Constants.swift | 2 + 4 files changed, 166 insertions(+), 12 deletions(-) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 6208fff..8d14053 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -5614,6 +5614,9 @@ } } } + }, + "Controls how often system metrics refresh while monitoring is active." : { + }, "Copy event link" : { "comment" : "A tooltip for a button that copies the link of a reminder event to the clipboard.", @@ -9617,6 +9620,9 @@ } } } + }, + "High-frequency updates without a timeout can increase battery usage." : { + }, "History Size" : { "localizations" : { @@ -12581,6 +12587,9 @@ }, "Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history." : { + }, + "Monitoring Behavior" : { + }, "Monitoring Status" : { "localizations" : { @@ -15677,6 +15686,9 @@ } } } + }, + "Sampling can continue while the notch is closed when the timeout is disabled." : { + }, "Save" : { "localizations" : { @@ -17864,6 +17876,9 @@ } } } + }, + "Stats update interval" : { + }, "Status & Actions" : { "localizations" : { @@ -19121,6 +19136,9 @@ } } } + }, + "Update interval" : { + }, "Update Timer" : { "extractionState" : "stale", @@ -19955,6 +19973,9 @@ }, "When disabled, all keyboard shortcuts will be inactive. You can still use the UI controls." : { + }, + "When enabled, stats monitoring stops a few seconds after the notch closes." : { + }, "When enabled, the Dynamic Island will expand horizontally to match the animation size" : { diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 726a0d4..b5de9c3 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -2260,6 +2260,8 @@ private struct TimerPresetComponentControl: View { struct StatsSettings: View { @ObservedObject var statsManager = StatsManager.shared @Default(.enableStatsFeature) var enableStatsFeature + @Default(.statsStopWhenNotchCloses) var statsStopWhenNotchCloses + @Default(.statsUpdateInterval) var statsUpdateInterval @Default(.showCpuGraph) var showCpuGraph @Default(.showMemoryGraph) var showMemoryGraph @Default(.showGpuGraph) var showGpuGraph @@ -2270,6 +2272,21 @@ struct StatsSettings: View { var enabledGraphsCount: Int { [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count } + + private var formattedUpdateInterval: String { + let seconds = Int(statsUpdateInterval.rounded()) + if seconds >= 60 { + return "60 s (1 min)" + } else if seconds == 1 { + return "1 s" + } else { + return "\(seconds) s" + } + } + + private var shouldShowStatsBatteryWarning: Bool { + !statsStopWhenNotchCloses && statsUpdateInterval <= 5 + } var body: some View { Form { @@ -2302,6 +2319,45 @@ struct StatsSettings: View { } if enableStatsFeature { + Section { + Defaults.Toggle("Stop monitoring after closing the notch", key: .statsStopWhenNotchCloses) + .help("When enabled, stats monitoring stops a few seconds after the notch closes.") + + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Update interval") + Spacer() + Text(formattedUpdateInterval) + .foregroundStyle(.secondary) + } + + Slider(value: $statsUpdateInterval, in: 1...60, step: 1) + .accessibilityLabel("Stats update interval") + + Text("Controls how often system metrics refresh while monitoring is active.") + .font(.caption) + .foregroundStyle(.secondary) + } + + if shouldShowStatsBatteryWarning { + Label { + Text("High-frequency updates without a timeout can increase battery usage.") + } icon: { + Image(systemName: "exclamationmark.triangle.fill") + } + .font(.caption) + .foregroundStyle(.orange) + .padding(.top, 4) + } + } header: { + Text("Monitoring Behavior") + } footer: { + Text("Sampling can continue while the notch is closed when the timeout is disabled.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + Section { Defaults.Toggle("CPU Usage", key: .showCpuGraph) Defaults.Toggle("Memory Usage", key: .showMemoryGraph) diff --git a/DynamicIsland/managers/StatsManager.swift b/DynamicIsland/managers/StatsManager.swift index 32b971d..b0c44c8 100644 --- a/DynamicIsland/managers/StatsManager.swift +++ b/DynamicIsland/managers/StatsManager.swift @@ -7,6 +7,7 @@ import Foundation import Combine +import Defaults import SwiftUI import IOKit import IOKit.ps @@ -442,6 +443,11 @@ class StatsManager: ObservableObject { private var isProcessRefreshInFlight = false private let gpuCollector = GPUInfoCollector() private let cpuSensorCollector = CPUSensorCollector() + private var cancellables = Set() + private let minUpdateInterval: TimeInterval = 1.0 + private let maxUpdateInterval: TimeInterval = 60.0 + private let notchCloseStopDelay: TimeInterval = 3.0 + private let tabSwitchStopDelay: TimeInterval = 0.1 // MARK: - Initialization private init() { @@ -462,6 +468,23 @@ class StatsManager: ObservableObject { // Initialize baseline disk stats let initialDiskStats = getDiskStats() previousDiskStats = initialDiskStats + + Defaults.publisher(.statsUpdateInterval, options: []).sink { [weak self] change in + self?.handleUpdateIntervalChange(change.newValue) + }.store(in: &cancellables) + + Defaults.publisher(.statsStopWhenNotchCloses, options: []).sink { [weak self] change in + guard let self else { return } + + if change.newValue { + if self.isMonitoring && self.lastNotchState == "closed" { + self.scheduleDelayedStop(after: self.notchCloseStopDelay) + } + } else { + self.delayedStopTimer?.invalidate() + self.delayedStopTimer = nil + } + }.store(in: &cancellables) } deinit { @@ -495,12 +518,14 @@ class StatsManager: ObservableObject { } } } else { - // Stop monitoring after 3 seconds (when notch is closed or stats tab is not active) - let delay = notchIsOpen ? 0.1 : 3.0 // Stop quickly when switching tabs, slowly when closing notch - delayedStopTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in - DispatchQueue.main.async { - self?.stopMonitoring() + if notchIsOpen && currentView != "stats" { + scheduleDelayedStop(after: tabSwitchStopDelay) + } else if !notchIsOpen { + if Defaults[.statsStopWhenNotchCloses] { + scheduleDelayedStop(after: notchCloseStopDelay) } + } else { + scheduleDelayedStop(after: tabSwitchStopDelay) } } } @@ -525,13 +550,10 @@ class StatsManager: ObservableObject { networkTotals = .zero diskTotals = .zero - // Start periodic monitoring (every 1 second) - monitoringTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in - guard let self = self else { return } - - Task { @MainActor in - self.updateSystemStats() - } + scheduleMonitoringTimer() + + Task { @MainActor in + self.updateSystemStats() } print("StatsManager: Monitoring started") @@ -568,6 +590,59 @@ class StatsManager: ObservableObject { cpuTemperature = CPUTemperatureMetrics(celsius: nil) cpuFrequency = nil } + + private func scheduleMonitoringTimer() { + monitoringTimer?.invalidate() + + let configuredInterval = Defaults[.statsUpdateInterval] + let interval = validatedUpdateInterval(configuredInterval) + + if abs(interval - configuredInterval) > 0.0001 { + Defaults[.statsUpdateInterval] = interval + } + + monitoringTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in + guard let self = self else { return } + + Task { @MainActor in + self.updateSystemStats() + } + } + } + + private func handleUpdateIntervalChange(_ newValue: Double) { + let interval = validatedUpdateInterval(newValue) + + if abs(interval - newValue) > 0.0001 { + Defaults[.statsUpdateInterval] = interval + return + } + + guard isMonitoring else { return } + + DispatchQueue.main.async { [weak self] in + self?.scheduleMonitoringTimer() + } + } + + private func validatedUpdateInterval(_ value: Double) -> TimeInterval { + min(max(value, minUpdateInterval), maxUpdateInterval) + } + + private func scheduleDelayedStop(after delay: TimeInterval) { + guard delay > 0 else { + stopMonitoring() + return + } + + delayedStopTimer?.invalidate() + + delayedStopTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self] _ in + DispatchQueue.main.async { + self?.stopMonitoring() + } + } + } // MARK: - Private Methods @MainActor diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 1564258..d348173 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -446,6 +446,8 @@ extension Defaults.Keys { // MARK: Stats Feature static let enableStatsFeature = Key("enableStatsFeature", default: false) static let autoStartStatsMonitoring = Key("autoStartStatsMonitoring", default: true) + static let statsStopWhenNotchCloses = Key("statsStopWhenNotchCloses", default: false) + static let statsUpdateInterval = Key("statsUpdateInterval", default: 1.0) static let showCpuGraph = Key("showCpuGraph", default: true) static let showMemoryGraph = Key("showMemoryGraph", default: true) static let showGpuGraph = Key("showGpuGraph", default: true)