Working Networka and Disk usage

This commit is contained in:
Hariharan Mudaliar 2025-07-30 18:04:11 +05:30
commit 8c99122684
6 changed files with 681 additions and 66 deletions

View file

@ -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)

View file

@ -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." : {

View file

@ -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"
)
// Dynamic Stats Grid
LazyVGrid(columns: gridColumns, spacing: 12) {
ForEach(0..<availableGraphs.count, id: \.self) { index in
let graphData = availableGraphs[index]
// 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"
)
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
)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
@ -90,7 +240,7 @@ struct NotchStatsView: View {
}
Button("Clear") {
clearStatsData()
statsManager.clearHistory()
}
.buttonStyle(.bordered)
.disabled(statsManager.isMonitoring)
@ -114,7 +264,7 @@ struct NotchStatsView: View {
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height)
.background(Color(NSColor.windowBackgroundColor).opacity(0.1))
.clipShape(RoundedRectangle(cornerRadius: 12))
.onAppear {
@ -126,12 +276,6 @@ struct NotchStatsView: View {
// Keep monitoring running when tab is not visible
}
}
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)
}
}
struct StatCard: View {
@ -162,6 +306,7 @@ struct StatCard: View {
.font(.title3)
.fontWeight(.bold)
.foregroundColor(.primary)
.frame(height: 22) // Match the height of dual values in DualStatCard
// Mini graph - use more height
MiniGraph(data: data, color: color)
@ -174,6 +319,66 @@ struct StatCard: View {
}
}
struct DualStatCard: View {
let title: String
let positiveValue: String
let negativeValue: String
let positiveData: [Double]
let negativeData: [Double]
let positiveColor: Color
let negativeColor: Color
let icon: String
var body: some View {
VStack(spacing: 6) {
// Header
HStack(spacing: 4) {
Image(systemName: icon)
.foregroundColor(positiveColor)
.font(.caption2)
Text(title)
.font(.caption2)
.fontWeight(.medium)
.foregroundColor(.secondary)
Spacer()
}
// Dual values - horizontal layout to match single value height
HStack(spacing: 8) {
Text(positiveValue)
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(positiveColor)
Text("•")
.font(.caption2)
.foregroundColor(.secondary)
Text(negativeValue)
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(negativeColor)
}
.frame(height: 22) // Match the height of .title3 text in StatCard
// Dual quadrant graph
DualQuadrantGraph(
positiveData: positiveData,
negativeData: negativeData,
positiveColor: positiveColor,
negativeColor: negativeColor
)
.frame(height: 50)
}
.padding(10)
.background(Color(NSColor.controlBackgroundColor).opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 8))
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct MiniGraph: View {
let data: [Double]
let color: Color
@ -229,6 +434,123 @@ struct MiniGraph: View {
}
}
struct DualQuadrantGraph: View {
let positiveData: [Double]
let negativeData: [Double]
let positiveColor: Color
let negativeColor: Color
var body: some View {
GeometryReader { geometry in
let maxPositive = positiveData.max() ?? 1.0
let maxNegative = negativeData.max() ?? 1.0
let maxValue = max(maxPositive, maxNegative)
let normalizedPositive = maxValue > 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)

View file

@ -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 {

View file

@ -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<ifaddrs>?
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<CFMutableDictionary>?
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)
}
}

View file

@ -156,6 +156,11 @@ extension Defaults.Keys {
// MARK: Stats Feature
static let enableStatsFeature = Key<Bool>("enableStatsFeature", default: false)
static let showCpuGraph = Key<Bool>("showCpuGraph", default: true)
static let showMemoryGraph = Key<Bool>("showMemoryGraph", default: true)
static let showGpuGraph = Key<Bool>("showGpuGraph", default: true)
static let showNetworkGraph = Key<Bool>("showNetworkGraph", default: false)
static let showDiskGraph = Key<Bool>("showDiskGraph", default: false)
// MARK: Timer Feature
static let enableTimerFeature = Key<Bool>("enableTimerFeature", default: true)