From 8c99122684cbd582b2e97f57a34d164c059cace4 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 30 Jul 2025 18:04:11 +0530 Subject: [PATCH 1/4] Working Networka and Disk usage --- DynamicIsland/ContentView.swift | 27 +- DynamicIsland/Localizable.xcstrings | 38 +- .../components/Notch/NotchStatsView.swift | 396 ++++++++++++++++-- .../components/Settings/SettingsView.swift | 111 +++-- DynamicIsland/managers/StatsManager.swift | 170 ++++++++ DynamicIsland/models/Constants.swift | 5 + 6 files changed, 681 insertions(+), 66 deletions(-) diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index f93f7f4..9a5565d 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -21,6 +21,31 @@ struct ContentView: View { @ObservedObject var musicManager = MusicManager.shared @ObservedObject var timerManager = TimerManager.shared @ObservedObject var batteryModel = BatteryStatusViewModel.shared + @ObservedObject var statsManager = StatsManager.shared + + @Default(.enableStatsFeature) var enableStatsFeature + @Default(.showCpuGraph) var showCpuGraph + @Default(.showMemoryGraph) var showMemoryGraph + @Default(.showGpuGraph) var showGpuGraph + @Default(.showNetworkGraph) var showNetworkGraph + @Default(.showDiskGraph) var showDiskGraph + + var dynamicNotchSize: CGSize { + // Only apply dynamic sizing when on stats tab and stats are enabled + guard coordinator.currentView == .stats && enableStatsFeature else { + return openNotchSize + } + + let enabledGraphsCount = [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count + + // If 4+ graphs are enabled, increase width + if enabledGraphsCount >= 4 { + let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120 + return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height) + } + + return openNotchSize + } @State private var isHovering: Bool = false @@ -173,7 +198,7 @@ struct ContentView: View { // .keyboardShortcut("E", modifiers: .command) } } - .frame(maxWidth: openNotchSize.width, maxHeight: openNotchSize.height, alignment: .top) + .frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height, alignment: .top) .shadow(color: ((vm.notchState == .open || isHovering) && Defaults[.enableShadow]) ? .black.opacity(0.6) : .clear, radius: Defaults[.cornerRadiusScaling] ? 10 : 5) .background(dragDetector) .environmentObject(vm) diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 2bfc34f..a142c91 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -37,6 +37,9 @@ }, "%lld%%" : { + }, + "•" : { + }, "• Bug fixes" : { @@ -367,6 +370,12 @@ }, "Disabled" : { + }, + "Disk Read" : { + + }, + "Disk Write" : { + }, "Download" : { @@ -412,6 +421,9 @@ }, "dynamic.island" : { + }, + "Each graph can be individually enabled or disabled. Network activity shows download/upload speeds, and disk I/O shows read/write speeds." : { + }, "Easily copy, store, and manage your most-used content. Upgrade now for advanced features like multi-item storage and quick access!" : { @@ -432,6 +444,9 @@ } } } + }, + "Enable graph visibility in Settings → Stats to view performance data." : { + }, "Enable haptics" : { "extractionState" : "stale", @@ -530,6 +545,9 @@ }, "Gradient" : { + }, + "Graph Visibility" : { + }, "Hide DynamicIsland Options" : { @@ -586,6 +604,9 @@ }, "Live" : { + }, + "Live Performance Data" : { + }, "Lottie JSON URL" : { @@ -706,6 +727,12 @@ }, "Need Restart" : { + }, + "Network Download" : { + + }, + "Network Upload" : { + }, "Never hide" : { @@ -738,6 +765,9 @@ }, "No extension installed" : { + }, + "No Graphs Enabled" : { + }, "Non-notch display height" : { @@ -1032,9 +1062,6 @@ }, "System features" : { - }, - "System Performance" : { - }, "The clipboard icon appears in the header next to the settings gear when enabled. Use the global shortcut Cmd+Shift+V or click the icon to access history." : { @@ -1119,7 +1146,10 @@ "What's New" : { }, - "When enabled, the Stats tab will display real-time CPU, Memory, and GPU usage graphs. This feature requires system permissions and may use additional battery." : { + "When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery." : { + + }, + "With %lld graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row." : { }, "You can now enjoy the app. If you want to tweak things further, you can always visit the settings." : { diff --git a/DynamicIsland/components/Notch/NotchStatsView.swift b/DynamicIsland/components/Notch/NotchStatsView.swift index 69eea7f..754eed4 100644 --- a/DynamicIsland/components/Notch/NotchStatsView.swift +++ b/DynamicIsland/components/Notch/NotchStatsView.swift @@ -8,9 +8,142 @@ import SwiftUI import Defaults +// Graph data protocol for unified interface +protocol GraphData { + var title: String { get } + var color: Color { get } + var icon: String { get } + var type: GraphType { get } +} + +enum GraphType { + case single + case dual +} + +// Single value graph data +struct SingleGraphData: GraphData { + let title: String + let value: String + let data: [Double] + let color: Color + let icon: String + let type: GraphType = .single +} + +// Dual value graph data (for network/disk) +struct DualGraphData: GraphData { + let title: String + let positiveValue: String + let negativeValue: String + let positiveData: [Double] + let negativeData: [Double] + let positiveColor: Color + let negativeColor: Color + let color: Color // Primary color for the component + let icon: String + let type: GraphType = .dual +} + struct NotchStatsView: View { @ObservedObject var statsManager = StatsManager.shared @Default(.enableStatsFeature) var enableStatsFeature + @Default(.showCpuGraph) var showCpuGraph + @Default(.showMemoryGraph) var showMemoryGraph + @Default(.showGpuGraph) var showGpuGraph + @Default(.showNetworkGraph) var showNetworkGraph + @Default(.showDiskGraph) var showDiskGraph + + var availableGraphs: [GraphData] { + var graphs: [GraphData] = [] + + if showCpuGraph { + graphs.append(SingleGraphData( + title: "CPU", + value: statsManager.cpuUsageString, + data: statsManager.cpuHistory, + color: .blue, + icon: "cpu" + )) + } + + if showMemoryGraph { + graphs.append(SingleGraphData( + title: "Memory", + value: statsManager.memoryUsageString, + data: statsManager.memoryHistory, + color: .green, + icon: "memorychip" + )) + } + + if showGpuGraph { + graphs.append(SingleGraphData( + title: "GPU", + value: statsManager.gpuUsageString, + data: statsManager.gpuHistory, + color: .purple, + icon: "display" + )) + } + + if showNetworkGraph { + graphs.append(DualGraphData( + title: "Network", + positiveValue: String(format: "↓%.1f MB/s", statsManager.networkDownload), + negativeValue: String(format: "↑%.1f MB/s", statsManager.networkUpload), + positiveData: statsManager.networkDownloadHistory, + negativeData: statsManager.networkUploadHistory, + positiveColor: .orange, + negativeColor: .red, + color: .orange, + icon: "network" + )) + } + + if showDiskGraph { + graphs.append(DualGraphData( + title: "Disk", + positiveValue: String(format: "R %.1f MB/s", statsManager.diskRead), + negativeValue: String(format: "W %.1f MB/s", statsManager.diskWrite), + positiveData: statsManager.diskReadHistory, + negativeData: statsManager.diskWriteHistory, + positiveColor: .cyan, + negativeColor: .yellow, + color: .cyan, + icon: "internaldrive" + )) + } + + return graphs + } + + var dynamicNotchSize: CGSize { + let graphCount = availableGraphs.count + + // Base size for 1-3 graphs + let baseWidth: CGFloat = 610 + + // If 4+ graphs, increase width to accommodate + if graphCount >= 4 { + let extraWidth: CGFloat = CGFloat(graphCount - 3) * 120 + return CGSize(width: baseWidth + extraWidth, height: 200) + } + + return CGSize(width: baseWidth, height: 200) + } + + var gridColumns: [GridItem] { + let graphCount = availableGraphs.count + + if graphCount <= 3 { + // Horizontal layout for 1-3 graphs + return Array(repeating: GridItem(.flexible(), spacing: 12), count: graphCount) + } else { + // Compressed grid layout for 4+ graphs + return Array(repeating: GridItem(.flexible(), spacing: 8), count: graphCount) + } + } var body: some View { VStack(spacing: 0) { @@ -33,38 +166,55 @@ struct NotchStatsView: View { } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() + } else if availableGraphs.isEmpty { + // No graphs enabled state + VStack(spacing: 12) { + Image(systemName: "chart.xyaxis.line") + .font(.system(size: 40)) + .foregroundColor(.secondary) + + Text("No Graphs Enabled") + .font(.headline) + .foregroundColor(.primary) + + Text("Enable graph visibility in Settings → Stats to view performance data.") + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .padding(.horizontal) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding() } else { - // Stats content - use full width and height + // Stats content with dynamic layout ZStack(alignment: .topTrailing) { VStack(spacing: 12) { - // Stats Grid - use more space - HStack(spacing: 12) { - // CPU Usage - StatCard( - title: "CPU", - value: statsManager.cpuUsageString, - data: statsManager.cpuHistory, - color: .blue, - icon: "cpu" - ) - - // Memory Usage - StatCard( - title: "Memory", - value: statsManager.memoryUsageString, - data: statsManager.memoryHistory, - color: .green, - icon: "memorychip" - ) - - // GPU Usage - StatCard( - title: "GPU", - value: statsManager.gpuUsageString, - data: statsManager.gpuHistory, - color: .purple, - icon: "display" - ) + // Dynamic Stats Grid + LazyVGrid(columns: gridColumns, spacing: 12) { + ForEach(0.. 0 ? positiveData.map { $0 / maxValue } : positiveData + let normalizedNegative = maxValue > 0 ? negativeData.map { $0 / maxValue } : negativeData + + let centerY = geometry.size.height / 2 + + ZStack { + // Center dividing line + Path { path in + path.move(to: CGPoint(x: 0, y: centerY)) + path.addLine(to: CGPoint(x: geometry.size.width, y: centerY)) + } + .stroke(Color.secondary.opacity(0.3), lineWidth: 1) + + // Positive quadrant (upper half) + Path { path in + guard !normalizedPositive.isEmpty else { return } + + let stepX = geometry.size.width / CGFloat(normalizedPositive.count - 1) + + for (index, value) in normalizedPositive.enumerated() { + let x = CGFloat(index) * stepX + let y = centerY - (centerY * CGFloat(value)) // Above center + + if index == 0 { + path.move(to: CGPoint(x: x, y: y)) + } else { + path.addLine(to: CGPoint(x: x, y: y)) + } + } + } + .stroke(positiveColor, lineWidth: 2) + + // Positive fill + Path { path in + guard !normalizedPositive.isEmpty else { return } + + let stepX = geometry.size.width / CGFloat(normalizedPositive.count - 1) + + path.move(to: CGPoint(x: 0, y: centerY)) + + for (index, value) in normalizedPositive.enumerated() { + let x = CGFloat(index) * stepX + let y = centerY - (centerY * CGFloat(value)) + path.addLine(to: CGPoint(x: x, y: y)) + } + + path.addLine(to: CGPoint(x: geometry.size.width, y: centerY)) + path.closeSubpath() + } + .fill( + LinearGradient( + gradient: Gradient(colors: [positiveColor.opacity(0.3), positiveColor.opacity(0.1)]), + startPoint: .top, + endPoint: .center + ) + ) + + // Negative quadrant (lower half) + Path { path in + guard !normalizedNegative.isEmpty else { return } + + let stepX = geometry.size.width / CGFloat(normalizedNegative.count - 1) + + for (index, value) in normalizedNegative.enumerated() { + let x = CGFloat(index) * stepX + let y = centerY + (centerY * CGFloat(value)) // Below center + + if index == 0 { + path.move(to: CGPoint(x: x, y: y)) + } else { + path.addLine(to: CGPoint(x: x, y: y)) + } + } + } + .stroke(negativeColor, lineWidth: 2) + + // Negative fill + Path { path in + guard !normalizedNegative.isEmpty else { return } + + let stepX = geometry.size.width / CGFloat(normalizedNegative.count - 1) + + path.move(to: CGPoint(x: 0, y: centerY)) + + for (index, value) in normalizedNegative.enumerated() { + let x = CGFloat(index) * stepX + let y = centerY + (centerY * CGFloat(value)) + path.addLine(to: CGPoint(x: x, y: y)) + } + + path.addLine(to: CGPoint(x: geometry.size.width, y: centerY)) + path.closeSubpath() + } + .fill( + LinearGradient( + gradient: Gradient(colors: [negativeColor.opacity(0.3), negativeColor.opacity(0.1)]), + startPoint: .bottom, + endPoint: .center + ) + ) + } + } + } +} + #Preview { NotchStatsView() .frame(width: 400, height: 300) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 9040fb2..f2e2aed 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -1292,6 +1292,15 @@ struct Shortcuts: View { struct Stats: View { @ObservedObject var statsManager = StatsManager.shared @Default(.enableStatsFeature) var enableStatsFeature + @Default(.showCpuGraph) var showCpuGraph + @Default(.showMemoryGraph) var showMemoryGraph + @Default(.showGpuGraph) var showGpuGraph + @Default(.showNetworkGraph) var showNetworkGraph + @Default(.showDiskGraph) var showDiskGraph + + var enabledGraphsCount: Int { + [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count + } var body: some View { Form { @@ -1307,13 +1316,35 @@ struct Stats: View { } header: { Text("General") } footer: { - Text("When enabled, the Stats tab will display real-time CPU, Memory, and GPU usage graphs. This feature requires system permissions and may use additional battery.") + Text("When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery.") .multilineTextAlignment(.trailing) .foregroundStyle(.secondary) .font(.caption) } if enableStatsFeature { + Section { + Defaults.Toggle("CPU Usage", key: .showCpuGraph) + Defaults.Toggle("Memory Usage", key: .showMemoryGraph) + Defaults.Toggle("GPU Usage", key: .showGpuGraph) + Defaults.Toggle("Network Activity", key: .showNetworkGraph) + Defaults.Toggle("Disk I/O", key: .showDiskGraph) + } header: { + Text("Graph Visibility") + } footer: { + if enabledGraphsCount >= 4 { + Text("With \(enabledGraphsCount) graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } else { + Text("Each graph can be individually enabled or disabled. Network activity shows download/upload speeds, and disk I/O shows read/write speeds.") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + } + Section { HStack { Text("Monitoring Status") @@ -1328,25 +1359,63 @@ struct Stats: View { } if statsManager.isMonitoring { - HStack { - Text("CPU Usage") - Spacer() - Text(statsManager.cpuUsageString) - .foregroundStyle(.secondary) + if showCpuGraph { + HStack { + Text("CPU Usage") + Spacer() + Text(statsManager.cpuUsageString) + .foregroundStyle(.secondary) + } } - HStack { - Text("Memory Usage") - Spacer() - Text(statsManager.memoryUsageString) - .foregroundStyle(.secondary) + if showMemoryGraph { + HStack { + Text("Memory Usage") + Spacer() + Text(statsManager.memoryUsageString) + .foregroundStyle(.secondary) + } } - HStack { - Text("GPU Usage") - Spacer() - Text(statsManager.gpuUsageString) - .foregroundStyle(.secondary) + if showGpuGraph { + HStack { + Text("GPU Usage") + Spacer() + Text(statsManager.gpuUsageString) + .foregroundStyle(.secondary) + } + } + + if showNetworkGraph { + HStack { + Text("Network Download") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.networkDownload)) + .foregroundStyle(.secondary) + } + + HStack { + Text("Network Upload") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.networkUpload)) + .foregroundStyle(.secondary) + } + } + + if showDiskGraph { + HStack { + Text("Disk Read") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.diskRead)) + .foregroundStyle(.secondary) + } + + HStack { + Text("Disk Write") + Spacer() + Text(String(format: "%.1f MB/s", statsManager.diskWrite)) + .foregroundStyle(.secondary) + } } HStack { @@ -1357,7 +1426,7 @@ struct Stats: View { } } } header: { - Text("System Performance") + Text("Live Performance Data") } Section { @@ -1375,7 +1444,7 @@ struct Stats: View { Spacer() Button("Clear Data") { - clearStatsData() + statsManager.clearHistory() } .buttonStyle(.bordered) .disabled(statsManager.isMonitoring) @@ -1387,12 +1456,6 @@ struct Stats: View { } .navigationTitle("Stats") } - - private func clearStatsData() { - statsManager.cpuHistory = Array(repeating: 0.0, count: 30) - statsManager.memoryHistory = Array(repeating: 0.0, count: 30) - statsManager.gpuHistory = Array(repeating: 0.0, count: 30) - } } func proFeatureBadge() -> some View { diff --git a/DynamicIsland/managers/StatsManager.swift b/DynamicIsland/managers/StatsManager.swift index 329142f..72f1779 100644 --- a/DynamicIsland/managers/StatsManager.swift +++ b/DynamicIsland/managers/StatsManager.swift @@ -10,6 +10,8 @@ import Combine import SwiftUI import IOKit import IOKit.ps +import Darwin +import Network class StatsManager: ObservableObject { // MARK: - Properties @@ -19,22 +21,50 @@ class StatsManager: ObservableObject { @Published var cpuUsage: Double = 0.0 @Published var memoryUsage: Double = 0.0 @Published var gpuUsage: Double = 0.0 + @Published var networkDownload: Double = 0.0 // MB/s + @Published var networkUpload: Double = 0.0 // MB/s + @Published var diskRead: Double = 0.0 // MB/s + @Published var diskWrite: Double = 0.0 // MB/s @Published var lastUpdated: Date = .distantPast // Historical data for graphs (last 30 data points) @Published var cpuHistory: [Double] = [] @Published var memoryHistory: [Double] = [] @Published var gpuHistory: [Double] = [] + @Published var networkDownloadHistory: [Double] = [] + @Published var networkUploadHistory: [Double] = [] + @Published var diskReadHistory: [Double] = [] + @Published var diskWriteHistory: [Double] = [] private var monitoringTimer: Timer? private let maxHistoryPoints = 30 + // Network monitoring state + private var previousNetworkStats: (bytesIn: UInt64, bytesOut: UInt64) = (0, 0) + private var previousTimestamp: Date = Date() + + // Disk monitoring state + private var previousDiskStats: (bytesRead: UInt64, bytesWritten: UInt64) = (0, 0) + // MARK: - Initialization private init() { // Initialize with empty history cpuHistory = Array(repeating: 0.0, count: maxHistoryPoints) memoryHistory = Array(repeating: 0.0, count: maxHistoryPoints) gpuHistory = Array(repeating: 0.0, count: maxHistoryPoints) + networkDownloadHistory = Array(repeating: 0.0, count: maxHistoryPoints) + networkUploadHistory = Array(repeating: 0.0, count: maxHistoryPoints) + diskReadHistory = Array(repeating: 0.0, count: maxHistoryPoints) + diskWriteHistory = Array(repeating: 0.0, count: maxHistoryPoints) + + // Initialize baseline network stats + let initialStats = getNetworkStats() + previousNetworkStats = initialStats + previousTimestamp = Date() + + // Initialize baseline disk stats + let initialDiskStats = getDiskStats() + previousDiskStats = initialDiskStats } deinit { @@ -73,16 +103,48 @@ class StatsManager: ObservableObject { let newMemoryUsage = getMemoryUsage() let newGpuUsage = getGPUUsage() + // Calculate network speeds + let currentNetworkStats = getNetworkStats() + let currentTime = Date() + let timeInterval = currentTime.timeIntervalSince(previousTimestamp) + + let bytesDownloaded = currentNetworkStats.bytesIn - previousNetworkStats.bytesIn + let bytesUploaded = currentNetworkStats.bytesOut - previousNetworkStats.bytesOut + + let downloadSpeed = timeInterval > 0 ? Double(bytesDownloaded) / timeInterval / 1_048_576 : 0.0 // Convert to MB/s + let uploadSpeed = timeInterval > 0 ? Double(bytesUploaded) / timeInterval / 1_048_576 : 0.0 // Convert to MB/s + + // Calculate disk speeds + let currentDiskStats = getDiskStats() + let bytesRead = currentDiskStats.bytesRead - previousDiskStats.bytesRead + let bytesWritten = currentDiskStats.bytesWritten - previousDiskStats.bytesWritten + + let readSpeed = timeInterval > 0 ? Double(bytesRead) / timeInterval / 1_048_576 : 0.0 // Convert to MB/s + let writeSpeed = timeInterval > 0 ? Double(bytesWritten) / timeInterval / 1_048_576 : 0.0 // Convert to MB/s + // Update current values cpuUsage = newCpuUsage memoryUsage = newMemoryUsage gpuUsage = newGpuUsage + networkDownload = max(0.0, downloadSpeed) + networkUpload = max(0.0, uploadSpeed) + diskRead = max(0.0, readSpeed) + diskWrite = max(0.0, writeSpeed) lastUpdated = Date() // Update history arrays (sliding window) updateHistory(value: newCpuUsage, history: &cpuHistory) updateHistory(value: newMemoryUsage, history: &memoryHistory) updateHistory(value: newGpuUsage, history: &gpuHistory) + updateHistory(value: networkDownload, history: &networkDownloadHistory) + updateHistory(value: networkUpload, history: &networkUploadHistory) + updateHistory(value: readSpeed, history: &diskReadHistory) + updateHistory(value: writeSpeed, history: &diskWriteHistory) + + // Update previous stats for next calculation + previousNetworkStats = currentNetworkStats + previousDiskStats = currentDiskStats + previousTimestamp = currentTime } private func updateHistory(value: Double, history: inout [Double]) { @@ -159,6 +221,87 @@ class StatsManager: ObservableObject { return min(100.0, max(0.0, simulatedUsage)) } + private func getNetworkStats() -> (bytesIn: UInt64, bytesOut: UInt64) { + // Use BSD sockets to get network interface statistics + var totalBytesIn: UInt64 = 0 + var totalBytesOut: UInt64 = 0 + + var ifaddrs: UnsafeMutablePointer? + guard getifaddrs(&ifaddrs) == 0 else { + return (totalBytesIn, totalBytesOut) + } + + defer { freeifaddrs(ifaddrs) } + + var ptr = ifaddrs + while ptr != nil { + defer { ptr = ptr?.pointee.ifa_next } + + guard let interface = ptr?.pointee, + interface.ifa_addr.pointee.sa_family == UInt8(AF_LINK) else { + continue + } + + let name = String(cString: interface.ifa_name) + // Skip loopback and non-active interfaces + guard !name.hasPrefix("lo") && !name.hasPrefix("gif") && !name.hasPrefix("stf") else { + continue + } + + if let data = interface.ifa_data?.assumingMemoryBound(to: if_data.self) { + totalBytesIn += UInt64(data.pointee.ifi_ibytes) + totalBytesOut += UInt64(data.pointee.ifi_obytes) + } + } + + return (totalBytesIn, totalBytesOut) + } + + private func getDiskStats() -> (bytesRead: UInt64, bytesWritten: UInt64) { + // Use IOKit to get disk I/O statistics + var totalBytesRead: UInt64 = 0 + var totalBytesWritten: UInt64 = 0 + + // Create matching dictionary for IOService + let matchingDict = IOServiceMatching("IOBlockStorageDriver") + + var iterator: io_iterator_t = 0 + let result = IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &iterator) + + guard result == KERN_SUCCESS else { + return (totalBytesRead, totalBytesWritten) + } + + defer { IOObjectRelease(iterator) } + + var service: io_registry_entry_t = IOIteratorNext(iterator) + while service != 0 { + defer { + IOObjectRelease(service) + service = IOIteratorNext(iterator) + } + + var properties: Unmanaged? + let propertiesResult = IORegistryEntryCreateCFProperties(service, &properties, kCFAllocatorDefault, 0) + + guard propertiesResult == KERN_SUCCESS, + let props = properties?.takeRetainedValue() as? [String: Any], + let statistics = props["Statistics"] as? [String: Any] else { + continue + } + + if let bytesRead = statistics["Bytes read"] as? UInt64 { + totalBytesRead += bytesRead + } + + if let bytesWritten = statistics["Bytes written"] as? UInt64 { + totalBytesWritten += bytesWritten + } + } + + return (totalBytesRead, totalBytesWritten) + } + // MARK: - Computed Properties for UI var cpuUsageString: String { return String(format: "%.1f%%", cpuUsage) @@ -172,6 +315,22 @@ class StatsManager: ObservableObject { return String(format: "%.1f%%", gpuUsage) } + var networkDownloadString: String { + return String(format: "%.1f MB/s", networkDownload) + } + + var networkUploadString: String { + return String(format: "%.1f MB/s", networkUpload) + } + + var diskReadString: String { + return String(format: "%.1f MB/s", diskRead) + } + + var diskWriteString: String { + return String(format: "%.1f MB/s", diskWrite) + } + var maxCpuUsage: Double { return cpuHistory.max() ?? 0.0 } @@ -201,4 +360,15 @@ class StatsManager: ObservableObject { guard !nonZeroValues.isEmpty else { return 0.0 } return nonZeroValues.reduce(0, +) / Double(nonZeroValues.count) } + + // MARK: - Clear History Method + func clearHistory() { + cpuHistory = Array(repeating: 0.0, count: maxHistoryPoints) + memoryHistory = Array(repeating: 0.0, count: maxHistoryPoints) + gpuHistory = Array(repeating: 0.0, count: maxHistoryPoints) + networkDownloadHistory = Array(repeating: 0.0, count: maxHistoryPoints) + networkUploadHistory = Array(repeating: 0.0, count: maxHistoryPoints) + diskReadHistory = Array(repeating: 0.0, count: maxHistoryPoints) + diskWriteHistory = Array(repeating: 0.0, count: maxHistoryPoints) + } } diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index e2ac275..7a67952 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -156,6 +156,11 @@ extension Defaults.Keys { // MARK: Stats Feature static let enableStatsFeature = Key("enableStatsFeature", default: false) + static let showCpuGraph = Key("showCpuGraph", default: true) + static let showMemoryGraph = Key("showMemoryGraph", default: true) + static let showGpuGraph = Key("showGpuGraph", default: true) + static let showNetworkGraph = Key("showNetworkGraph", default: false) + static let showDiskGraph = Key("showDiskGraph", default: false) // MARK: Timer Feature static let enableTimerFeature = Key("enableTimerFeature", default: true) From 0cc6f369a757a47c52b89b82e6caf7d42b6275d3 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 30 Jul 2025 18:16:07 +0530 Subject: [PATCH 2/4] Working with resizing --- DynamicIsland/DynamicIslandApp.swift | 93 +++++++++++++++++++ .../components/Notch/NotchStatsView.swift | 17 +--- .../models/DynamicIslandViewModel.swift | 25 ++++- 3 files changed, 118 insertions(+), 17 deletions(-) diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index f4c7ebd..b4970d4 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -85,6 +85,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { var closeNotchWorkItem: DispatchWorkItem? private var previousScreens: [NSScreen]? private var onboardingWindowController: NSWindowController? + private var cancellables = Set() func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return false @@ -162,9 +163,101 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + private func updateWindowSizeIfNeeded() { + // Calculate required size based on current state + let requiredSize = calculateRequiredNotchSize() + + // Update all windows if size has changed + for (screen, window) in windows { + if window.frame.size != requiredSize { + let screenFrame = screen.frame + let newFrame = NSRect( + x: screenFrame.origin.x + (screenFrame.width / 2) - requiredSize.width / 2, + y: screenFrame.origin.y + screenFrame.height - requiredSize.height, + width: requiredSize.width, + height: requiredSize.height + ) + + // Animate the frame change for smooth transition + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.3 + context.allowsImplicitAnimation = true + window.setFrame(newFrame, display: true, animate: true) + } + } + } + } + + private func calculateRequiredNotchSize() -> CGSize { + // Only apply dynamic sizing when on stats tab and stats are enabled + guard coordinator.currentView == .stats && Defaults[.enableStatsFeature] else { + return openNotchSize + } + + let enabledGraphsCount = [ + Defaults[.showCpuGraph], + Defaults[.showMemoryGraph], + Defaults[.showGpuGraph], + Defaults[.showNetworkGraph], + Defaults[.showDiskGraph] + ].filter { $0 }.count + + // If 4+ graphs are enabled, increase width + if enabledGraphsCount >= 4 { + let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120 + return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height) + } + + return openNotchSize + } + func applicationDidFinishLaunching(_ notification: Notification) { coordinator.setupWorkersNotificationObservers() + + // Observe tab changes to update window size dynamically + coordinator.objectWillChange.sink { [weak self] in + DispatchQueue.main.async { + self?.updateWindowSizeIfNeeded() + } + }.store(in: &cancellables) + + // Observe stats settings changes + Defaults.publisher(.enableStatsFeature, options: []).sink { [weak self] _ in + DispatchQueue.main.async { + self?.updateWindowSizeIfNeeded() + } + }.store(in: &cancellables) + + Defaults.publisher(.showCpuGraph, options: []).sink { [weak self] _ in + DispatchQueue.main.async { + self?.updateWindowSizeIfNeeded() + } + }.store(in: &cancellables) + + Defaults.publisher(.showMemoryGraph, options: []).sink { [weak self] _ in + DispatchQueue.main.async { + self?.updateWindowSizeIfNeeded() + } + }.store(in: &cancellables) + + Defaults.publisher(.showGpuGraph, options: []).sink { [weak self] _ in + DispatchQueue.main.async { + self?.updateWindowSizeIfNeeded() + } + }.store(in: &cancellables) + + Defaults.publisher(.showNetworkGraph, options: []).sink { [weak self] _ in + DispatchQueue.main.async { + self?.updateWindowSizeIfNeeded() + } + }.store(in: &cancellables) + + Defaults.publisher(.showDiskGraph, options: []).sink { [weak self] _ in + DispatchQueue.main.async { + self?.updateWindowSizeIfNeeded() + } + }.store(in: &cancellables) NotificationCenter.default.addObserver( self, diff --git a/DynamicIsland/components/Notch/NotchStatsView.swift b/DynamicIsland/components/Notch/NotchStatsView.swift index 754eed4..556a96f 100644 --- a/DynamicIsland/components/Notch/NotchStatsView.swift +++ b/DynamicIsland/components/Notch/NotchStatsView.swift @@ -118,21 +118,6 @@ struct NotchStatsView: View { return graphs } - var dynamicNotchSize: CGSize { - let graphCount = availableGraphs.count - - // Base size for 1-3 graphs - let baseWidth: CGFloat = 610 - - // If 4+ graphs, increase width to accommodate - if graphCount >= 4 { - let extraWidth: CGFloat = CGFloat(graphCount - 3) * 120 - return CGSize(width: baseWidth + extraWidth, height: 200) - } - - return CGSize(width: baseWidth, height: 200) - } - var gridColumns: [GridItem] { let graphCount = availableGraphs.count @@ -264,7 +249,7 @@ struct NotchStatsView: View { } } } - .frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height) + .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(NSColor.windowBackgroundColor).opacity(0.1)) .clipShape(RoundedRectangle(cornerRadius: 12)) .onAppear { diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index d73371b..a55cf2c 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -116,13 +116,36 @@ class DynamicIslandViewModel: NSObject, ObservableObject { func open() { withAnimation(.bouncy) { - self.notchSize = openNotchSize + self.notchSize = calculateDynamicNotchSize() self.notchState = .open } // Force music information update when notch is opened MusicManager.shared.forceUpdate() } + + private func calculateDynamicNotchSize() -> CGSize { + // Only apply dynamic sizing when on stats tab and stats are enabled + guard DynamicIslandViewCoordinator.shared.currentView == .stats && Defaults[.enableStatsFeature] else { + return openNotchSize + } + + let enabledGraphsCount = [ + Defaults[.showCpuGraph], + Defaults[.showMemoryGraph], + Defaults[.showGpuGraph], + Defaults[.showNetworkGraph], + Defaults[.showDiskGraph] + ].filter { $0 }.count + + // If 4+ graphs are enabled, increase width + if enabledGraphsCount >= 4 { + let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120 + return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height) + } + + return openNotchSize + } func close() { withAnimation(.smooth) { [weak self] in From 64b5815c4a8730c83ab03704fdaeddb41faae147 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 30 Jul 2025 18:45:24 +0530 Subject: [PATCH 3/4] Working Disk Read/Write --- DynamicIsland/managers/StatsManager.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/DynamicIsland/managers/StatsManager.swift b/DynamicIsland/managers/StatsManager.swift index 72f1779..db8fdc7 100644 --- a/DynamicIsland/managers/StatsManager.swift +++ b/DynamicIsland/managers/StatsManager.swift @@ -258,13 +258,11 @@ class StatsManager: ObservableObject { } private func getDiskStats() -> (bytesRead: UInt64, bytesWritten: UInt64) { - // Use IOKit to get disk I/O statistics + // Use IOKit to get disk I/O statistics from IOStorage service var totalBytesRead: UInt64 = 0 var totalBytesWritten: UInt64 = 0 - // Create matching dictionary for IOService - let matchingDict = IOServiceMatching("IOBlockStorageDriver") - + let matchingDict = IOServiceMatching("IOStorage") var iterator: io_iterator_t = 0 let result = IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &iterator) @@ -290,11 +288,12 @@ class StatsManager: ObservableObject { continue } - if let bytesRead = statistics["Bytes read"] as? UInt64 { + // Use the correct property names for APFS/modern filesystems + if let bytesRead = statistics["Bytes read from block device"] as? UInt64 { totalBytesRead += bytesRead } - if let bytesWritten = statistics["Bytes written"] as? UInt64 { + if let bytesWritten = statistics["Bytes written to block device"] as? UInt64 { totalBytesWritten += bytesWritten } } From 2e19ff1ed9757a0d9a9ab4630f496315dd7a1bce Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Wed, 30 Jul 2025 20:04:03 +0530 Subject: [PATCH 4/4] Debouncing but working --- DynamicIsland/ContentView.swift | 12 +- DynamicIsland/DynamicIslandApp.swift | 161 +++++++++++----- .../components/Notch/NotchStatsView.swift | 177 ++++++++++++++---- logic.md | 124 ++++++++++++ 4 files changed, 384 insertions(+), 90 deletions(-) create mode 100644 logic.md diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 9a5565d..708d01e 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -38,13 +38,17 @@ struct ContentView: View { let enabledGraphsCount = [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count - // If 4+ graphs are enabled, increase width + // Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows + var requiredHeight = openNotchSize.height + if enabledGraphsCount >= 4 { - let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120 - return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height) + // Two rows needed - add height for second row plus spacing + let extraHeight: CGFloat = 120 + 12 // Graph height + spacing + requiredHeight = openNotchSize.height + extraHeight } - return openNotchSize + // Width stays constant - no horizontal expansion + return CGSize(width: openNotchSize.width, height: requiredHeight) } diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index b4970d4..102ff7a 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -86,12 +86,31 @@ class AppDelegate: NSObject, NSApplicationDelegate { private var previousScreens: [NSScreen]? private var onboardingWindowController: NSWindowController? private var cancellables = Set() + + // Debouncing mechanism for window size updates + private var windowSizeUpdateWorkItem: DispatchWorkItem? + + private func debouncedUpdateWindowSize() { + // Cancel any existing work item + windowSizeUpdateWorkItem?.cancel() + + // Create new work item with delay + let workItem = DispatchWorkItem { [weak self] in + self?.updateWindowSizeIfNeeded() + } + + // Store reference and schedule + windowSizeUpdateWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: workItem) + } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return false } func applicationWillTerminate(_ notification: Notification) { + // Cancel any pending window size updates + windowSizeUpdateWorkItem?.cancel() NotificationCenter.default.removeObserver(self) } @@ -126,9 +145,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { private func createDynamicIslandWindow(for screen: NSScreen, with viewModel: DynamicIslandViewModel) -> NSWindow { + // Use the current required size instead of always using openNotchSize + let requiredSize = calculateRequiredNotchSize() + let window = DynamicIslandWindow( contentRect: NSRect( - x: 0, y: 0, width: openNotchSize.width, height: openNotchSize.height), + x: 0, y: 0, width: requiredSize.width, height: requiredSize.height), styleMask: [.borderless, .nonactivatingPanel, .utilityWindow, .hudWindow], backing: .buffered, defer: false @@ -151,14 +173,20 @@ class AppDelegate: NSObject, NSApplicationDelegate { window.alphaValue = 0 } - DispatchQueue.main.async { [weak window] in - guard let window = window else { return } - let screenFrame = screen.frame - window.setFrameOrigin( - NSPoint( - x: screenFrame.origin.x + (screenFrame.width / 2) - window.frame.width / 2, - y: screenFrame.origin.y + screenFrame.height - window.frame.height - )) + // Use the same centering logic as updateWindowSizeIfNeeded() + let screenFrame = screen.frame + let centerX = screenFrame.origin.x + (screenFrame.width / 2) + let newX = centerX - (window.frame.width / 2) + let newY = screenFrame.origin.y + screenFrame.height - window.frame.height + + window.setFrame(NSRect( + x: newX, + y: newY, + width: window.frame.width, + height: window.frame.height + ), display: false) + + if changeAlpha { window.alphaValue = 1 } } @@ -167,22 +195,65 @@ class AppDelegate: NSObject, NSApplicationDelegate { // Calculate required size based on current state let requiredSize = calculateRequiredNotchSize() - // Update all windows if size has changed - for (screen, window) in windows { - if window.frame.size != requiredSize { - let screenFrame = screen.frame - let newFrame = NSRect( - x: screenFrame.origin.x + (screenFrame.width / 2) - requiredSize.width / 2, - y: screenFrame.origin.y + screenFrame.height - requiredSize.height, - width: requiredSize.width, - height: requiredSize.height - ) + if Defaults[.showOnAllDisplays] { + // Update all windows if size has changed (multi-display mode) + for (screen, window) in windows { + if window.frame.size != requiredSize { + // Calculate center position BEFORE any changes + let screenFrame = screen.frame + let centerX = screenFrame.origin.x + (screenFrame.width / 2) + let newX = centerX - (requiredSize.width / 2) + let newY = screenFrame.origin.y + screenFrame.height - requiredSize.height + + // Stop any existing animations first + NSAnimationContext.runAnimationGroup { _ in + window.animator().setFrame(window.frame, display: false) + } + + // Animate window resize smoothly + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.25 + context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) + context.allowsImplicitAnimation = true + window.animator().setFrame(NSRect( + x: newX, + y: newY, + width: requiredSize.width, + height: requiredSize.height + ), display: true) + } + } + } + } else { + // Update single window if size has changed (single display mode) + if let window = window, window.frame.size != requiredSize { + // Find the screen this window is on + let currentScreen = NSScreen.screens.first { screen in + screen.frame.intersects(window.frame) + } ?? NSScreen.main ?? NSScreen.screens.first! - // Animate the frame change for smooth transition + // Calculate center position BEFORE any changes + let screenFrame = currentScreen.frame + let centerX = screenFrame.origin.x + (screenFrame.width / 2) + let newX = centerX - (requiredSize.width / 2) + let newY = screenFrame.origin.y + screenFrame.height - requiredSize.height + + // Stop any existing animations first + NSAnimationContext.runAnimationGroup { _ in + window.animator().setFrame(window.frame, display: false) + } + + // Animate window resize smoothly NSAnimationContext.runAnimationGroup { context in - context.duration = 0.3 + context.duration = 0.25 + context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut) context.allowsImplicitAnimation = true - window.setFrame(newFrame, display: true, animate: true) + window.animator().setFrame(NSRect( + x: newX, + y: newY, + width: requiredSize.width, + height: requiredSize.height + ), display: true) } } } @@ -202,61 +273,51 @@ class AppDelegate: NSObject, NSApplicationDelegate { Defaults[.showDiskGraph] ].filter { $0 }.count - // If 4+ graphs are enabled, increase width + // Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows + var requiredHeight = openNotchSize.height + if enabledGraphsCount >= 4 { - let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120 - return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height) + // Two rows needed - add height for second row plus spacing + let extraHeight: CGFloat = 120 + 12 // Graph height + spacing + requiredHeight = openNotchSize.height + extraHeight } - return openNotchSize + // Width stays constant - no horizontal expansion + return CGSize(width: openNotchSize.width, height: requiredHeight) } func applicationDidFinishLaunching(_ notification: Notification) { coordinator.setupWorkersNotificationObservers() - // Observe tab changes to update window size dynamically - coordinator.objectWillChange.sink { [weak self] in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + // Observe tab changes - use debounced updates + coordinator.$currentView.sink { [weak self] newView in + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) - // Observe stats settings changes + // Observe stats settings changes - use debounced updates Defaults.publisher(.enableStatsFeature, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) Defaults.publisher(.showCpuGraph, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) Defaults.publisher(.showMemoryGraph, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) Defaults.publisher(.showGpuGraph, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) Defaults.publisher(.showNetworkGraph, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) Defaults.publisher(.showDiskGraph, options: []).sink { [weak self] _ in - DispatchQueue.main.async { - self?.updateWindowSizeIfNeeded() - } + self?.debouncedUpdateWindowSize() }.store(in: &cancellables) NotificationCenter.default.addObserver( diff --git a/DynamicIsland/components/Notch/NotchStatsView.swift b/DynamicIsland/components/Notch/NotchStatsView.swift index 556a96f..8c2ec38 100644 --- a/DynamicIsland/components/Notch/NotchStatsView.swift +++ b/DynamicIsland/components/Notch/NotchStatsView.swift @@ -118,18 +118,144 @@ struct NotchStatsView: View { return graphs } - var gridColumns: [GridItem] { + // New vertical expansion layout system + @ViewBuilder + var statsGridLayout: some View { let graphCount = availableGraphs.count - if graphCount <= 3 { - // Horizontal layout for 1-3 graphs - return Array(repeating: GridItem(.flexible(), spacing: 12), count: graphCount) - } else { - // Compressed grid layout for 4+ graphs - return Array(repeating: GridItem(.flexible(), spacing: 8), count: graphCount) + switch graphCount { + case 1...3: + // Single row for 1-3 graphs + HStack(spacing: 12) { + ForEach(0.. some View { + if let singleData = graphData as? SingleGraphData { + StatCard( + title: singleData.title, + value: singleData.value, + data: singleData.data, + color: singleData.color, + icon: singleData.icon + ) + } else if let dualData = graphData as? DualGraphData { + DualStatCard( + title: dualData.title, + positiveValue: dualData.positiveValue, + negativeValue: dualData.negativeValue, + positiveData: dualData.positiveData, + negativeData: dualData.negativeData, + positiveColor: dualData.positiveColor, + negativeColor: dualData.negativeColor, + icon: dualData.icon + ) + } + } + var body: some View { VStack(spacing: 0) { if !enableStatsFeature { @@ -171,39 +297,18 @@ struct NotchStatsView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } else { - // Stats content with dynamic layout + // Stats content with vertical expansion layout ZStack(alignment: .topTrailing) { VStack(spacing: 12) { - // Dynamic Stats Grid - LazyVGrid(columns: gridColumns, spacing: 12) { - ForEach(0..