mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Working stuffgit add .
This commit is contained in:
parent
acf669ca9c
commit
630431a008
15 changed files with 2577 additions and 308 deletions
|
|
@ -817,10 +817,6 @@
|
|||
},
|
||||
"%lld colors" : {
|
||||
|
||||
},
|
||||
"%lld processes" : {
|
||||
"comment" : "A label at the bottom of the ranked processes popover showing the total number of processes being monitored.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"%lld%%" : {
|
||||
"localizations" : {
|
||||
|
|
@ -3053,6 +3049,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Averages" : {
|
||||
"comment" : "A label displayed above the average usage and last update information for a GPU.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Battery" : {
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
|
|
@ -3572,6 +3572,10 @@
|
|||
},
|
||||
"Bottom Pad:" : {
|
||||
|
||||
},
|
||||
"Breakdown" : {
|
||||
"comment" : "A label displayed above the breakdown of memory usage.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Brightness HUD" : {
|
||||
|
||||
|
|
@ -4828,6 +4832,10 @@
|
|||
},
|
||||
"Copy something to start" : {
|
||||
|
||||
},
|
||||
"Core %lld" : {
|
||||
"comment" : "A label showing the ID of a CPU core. The argument is the ID of the CPU core.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"CPU Usage" : {
|
||||
|
||||
|
|
@ -7312,6 +7320,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"External" : {
|
||||
"comment" : "A label indicating an external storage device.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Fill Notch" : {
|
||||
|
||||
},
|
||||
|
|
@ -7417,6 +7429,10 @@
|
|||
},
|
||||
"Fit to Notch" : {
|
||||
|
||||
},
|
||||
"Free · %@" : {
|
||||
"comment" : "A label displaying the amount of free storage on a disk device. The argument is the number of free bytes on the disk.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Gemini API Key" : {
|
||||
|
||||
|
|
@ -8749,6 +8765,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Idle" : {
|
||||
"comment" : "A label displayed alongside the status of a GPU device.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Idle Animation Style" : {
|
||||
"comment" : "The header label for the section that lets users choose the style of idle animation.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
|
@ -9078,6 +9098,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"IPv4 · %@" : {
|
||||
"comment" : "A label displaying an IP address for a network interface. The text inside the parentheses is the IP address of the interface.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"IPv6 · %@" : {
|
||||
"comment" : "A label displaying an IPv6 address associated with a network interface. The text inside the parentheses is replaced with the actual IPv6 address.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Keyboard Backlight HUD" : {
|
||||
|
||||
},
|
||||
|
|
@ -9201,6 +9229,10 @@
|
|||
},
|
||||
"Live Performance Data" : {
|
||||
|
||||
},
|
||||
"Load Average" : {
|
||||
"comment" : "A label displayed above the load average values.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Lock Screen" : {
|
||||
|
||||
|
|
@ -11765,6 +11797,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"No active interfaces detected yet." : {
|
||||
"comment" : "A message displayed when there are no active network interfaces to display.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"No clipboard history" : {
|
||||
|
||||
},
|
||||
|
|
@ -12198,11 +12234,19 @@
|
|||
"comment" : "A label displayed when no graphs are enabled in the Stats tab.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"No process data yet. Keep stats open for a moment." : {
|
||||
"comment" : "A message displayed when there is no process data available to display in the CPU stats view. It encourages the user to keep the stats open for a moment to gather more data.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"No results" : {
|
||||
|
||||
},
|
||||
"No results found" : {
|
||||
|
||||
},
|
||||
"No storage devices detected yet." : {
|
||||
"comment" : "A message indicating that no storage devices have been detected yet.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Non-notch display height" : {
|
||||
"localizations" : {
|
||||
|
|
@ -12997,10 +13041,6 @@
|
|||
},
|
||||
"Picking Status" : {
|
||||
|
||||
},
|
||||
"PID: %d" : {
|
||||
"comment" : "A small label displaying the process ID of a particular app. The argument is the process ID of the app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Pin items from history to save them here" : {
|
||||
|
||||
|
|
@ -13128,6 +13168,10 @@
|
|||
},
|
||||
"Position" : {
|
||||
|
||||
},
|
||||
"Pressure" : {
|
||||
"comment" : "A label describing the memory pressure status.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Preview" : {
|
||||
|
||||
|
|
@ -13470,6 +13514,10 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Read" : {
|
||||
"comment" : "A heading displayed for the user's disk read speed.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Ready" : {
|
||||
|
||||
},
|
||||
|
|
@ -13488,10 +13536,6 @@
|
|||
},
|
||||
"Recording Status" : {
|
||||
|
||||
},
|
||||
"Refresh" : {
|
||||
"comment" : "A button to refresh the displayed data.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Release name" : {
|
||||
"localizations" : {
|
||||
|
|
@ -14298,6 +14342,18 @@
|
|||
},
|
||||
"Sensitivity" : {
|
||||
|
||||
},
|
||||
"Session · ↓ %@ · ↑ %@" : {
|
||||
"comment" : "A row displaying the total downloaded and uploaded bytes for the network session.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Session · ↓ %1$@ · ↑ %2$@"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Set" : {
|
||||
|
||||
|
|
@ -16018,6 +16074,18 @@
|
|||
},
|
||||
"Supports reasoning mode" : {
|
||||
|
||||
},
|
||||
"Swap" : {
|
||||
"comment" : "A label for the section of the memory dashboard related to swap usage.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Swap is disabled on this system." : {
|
||||
"comment" : "A message indicating that swap memory is disabled on the user's system.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"System" : {
|
||||
"comment" : "A label displayed above the system, load average, and uptime information.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"System features" : {
|
||||
"localizations" : {
|
||||
|
|
@ -16452,8 +16520,8 @@
|
|||
"Toggle the Dynamic Island open or closed from anywhere." : {
|
||||
|
||||
},
|
||||
"Top Processes by %@" : {
|
||||
"comment" : "A heading that announces the top processes by a specific metric. The text inside the parentheses is replaced with the actual metric name.",
|
||||
"Total · %@" : {
|
||||
"comment" : "A label displaying the total storage capacity of a disk device. The argument is the total storage capacity of the disk device.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Transform" : {
|
||||
|
|
@ -16976,6 +17044,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Upload" : {
|
||||
"comment" : "A label displayed above the user's upload statistics.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Usage" : {
|
||||
"comment" : "A label displayed above the CPU usage statistics.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Usage Breakdown" : {
|
||||
"comment" : "A heading for the breakdown of CPU usage.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Use music visualizer spectrogram" : {
|
||||
"localizations" : {
|
||||
"ar" : {
|
||||
|
|
@ -17076,8 +17156,20 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"Used" : {
|
||||
"comment" : "A label describing the percentage of memory that is currently in use.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Used · %@" : {
|
||||
"comment" : "A label showing the used space on a storage device, followed by the size of that space. The argument is the number of bytes used on the device.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Uses event-driven private API for real-time screen recording detection" : {
|
||||
|
||||
},
|
||||
"Utilization" : {
|
||||
"comment" : "A label displayed alongside the utilization percentage in the GPU detail view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Version" : {
|
||||
"localizations" : {
|
||||
|
|
@ -17599,6 +17691,10 @@
|
|||
},
|
||||
"With %lld graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row." : {
|
||||
|
||||
},
|
||||
"Write" : {
|
||||
"comment" : "A label for the write section of the disk overview.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"You" : {
|
||||
|
||||
|
|
|
|||
|
|
@ -56,15 +56,19 @@ struct NotchStatsView: View {
|
|||
@State private var showingCPUPopover = false
|
||||
@State private var showingMemoryPopover = false
|
||||
@State private var showingGPUPopover = false
|
||||
@State private var showingNetworkPopover = false
|
||||
@State private var showingDiskPopover = false
|
||||
@State private var isHoveringCPUPopover = false
|
||||
@State private var isHoveringMemoryPopover = false
|
||||
@State private var isHoveringGPUPopover = false
|
||||
@State private var isHoveringNetworkPopover = false
|
||||
@State private var isHoveringDiskPopover = false
|
||||
@State private var isResizingForStats = false
|
||||
@EnvironmentObject var vm: DynamicIslandViewModel
|
||||
|
||||
|
||||
var availableGraphs: [GraphData] {
|
||||
var graphs: [GraphData] = []
|
||||
|
||||
|
||||
if showCpuGraph {
|
||||
graphs.append(SingleGraphData(
|
||||
title: "CPU",
|
||||
|
|
@ -74,7 +78,7 @@ struct NotchStatsView: View {
|
|||
icon: "cpu"
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
if showMemoryGraph {
|
||||
graphs.append(SingleGraphData(
|
||||
title: "Memory",
|
||||
|
|
@ -84,7 +88,7 @@ struct NotchStatsView: View {
|
|||
icon: "memorychip"
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
if showGpuGraph {
|
||||
graphs.append(SingleGraphData(
|
||||
title: "GPU",
|
||||
|
|
@ -94,7 +98,7 @@ struct NotchStatsView: View {
|
|||
icon: "display"
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
if showNetworkGraph {
|
||||
graphs.append(DualGraphData(
|
||||
title: "Network",
|
||||
|
|
@ -108,7 +112,7 @@ struct NotchStatsView: View {
|
|||
icon: "network"
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
if showDiskGraph {
|
||||
graphs.append(DualGraphData(
|
||||
title: "Disk",
|
||||
|
|
@ -122,15 +126,15 @@ struct NotchStatsView: View {
|
|||
icon: "internaldrive"
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
return graphs
|
||||
}
|
||||
|
||||
|
||||
// Smart grid layout system for different graph counts
|
||||
@ViewBuilder
|
||||
var statsGridLayout: some View {
|
||||
let graphCount = availableGraphs.count
|
||||
|
||||
|
||||
if graphCount <= 3 {
|
||||
// 1-3 graphs: Single row with equal spacing
|
||||
LazyVGrid(
|
||||
|
|
@ -163,7 +167,7 @@ struct NotchStatsView: View {
|
|||
graphViewForIndex(index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Second row: 2 graphs (half-width each)
|
||||
LazyVGrid(
|
||||
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
|
||||
|
|
@ -176,13 +180,12 @@ struct NotchStatsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private func graphViewForIndex(_ index: Int) -> some View {
|
||||
let graphData = availableGraphs[index]
|
||||
|
||||
// Only make CPU, Memory, GPU clickable for process popovers
|
||||
if graphData.title == "CPU" || graphData.title == "Memory" || graphData.title == "GPU" {
|
||||
|
||||
if let rankingType = rankingTypeForGraph(graphData) {
|
||||
Button(action: {
|
||||
handleGraphClick(for: graphData)
|
||||
}) {
|
||||
|
|
@ -191,7 +194,7 @@ struct NotchStatsView: View {
|
|||
.buttonStyle(PlainButtonStyle())
|
||||
.popover(isPresented: bindingForGraph(graphData)) {
|
||||
RankedProcessPopover(
|
||||
rankingType: rankingTypeForGraph(graphData),
|
||||
rankingType: rankingType,
|
||||
onHoverChange: { hovering in
|
||||
switch graphData.title {
|
||||
case "CPU":
|
||||
|
|
@ -200,13 +203,16 @@ struct NotchStatsView: View {
|
|||
isHoveringMemoryPopover = hovering
|
||||
case "GPU":
|
||||
isHoveringGPUPopover = hovering
|
||||
case "Network":
|
||||
isHoveringNetworkPopover = hovering
|
||||
case "Disk":
|
||||
isHoveringDiskPopover = hovering
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
)
|
||||
.onDisappear {
|
||||
// Reset hover states when popover disappears
|
||||
switch graphData.title {
|
||||
case "CPU":
|
||||
isHoveringCPUPopover = false
|
||||
|
|
@ -214,10 +220,13 @@ struct NotchStatsView: View {
|
|||
isHoveringMemoryPopover = false
|
||||
case "GPU":
|
||||
isHoveringGPUPopover = false
|
||||
case "Network":
|
||||
isHoveringNetworkPopover = false
|
||||
case "Disk":
|
||||
isHoveringDiskPopover = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
// Ensure popover state is updated when popover disappears
|
||||
DispatchQueue.main.async {
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
|
|
@ -228,7 +237,6 @@ struct NotchStatsView: View {
|
|||
removal: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4))
|
||||
))
|
||||
} else {
|
||||
// Network and Disk graphs are not clickable
|
||||
UnifiedStatsCard(graphData: graphData)
|
||||
.transition(.asymmetric(
|
||||
insertion: .scale.combined(with: .opacity).animation(.easeInOut(duration: 0.4)),
|
||||
|
|
@ -236,7 +244,7 @@ struct NotchStatsView: View {
|
|||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func handleGraphClick(for graphData: GraphData) {
|
||||
switch graphData.title {
|
||||
case "CPU":
|
||||
|
|
@ -245,11 +253,15 @@ struct NotchStatsView: View {
|
|||
showingMemoryPopover = true
|
||||
case "GPU":
|
||||
showingGPUPopover = true
|
||||
case "Network":
|
||||
showingNetworkPopover = true
|
||||
case "Disk":
|
||||
showingDiskPopover = true
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func bindingForGraph(_ graphData: GraphData) -> Binding<Bool> {
|
||||
switch graphData.title {
|
||||
case "CPU":
|
||||
|
|
@ -258,12 +270,16 @@ struct NotchStatsView: View {
|
|||
return $showingMemoryPopover
|
||||
case "GPU":
|
||||
return $showingGPUPopover
|
||||
case "Network":
|
||||
return $showingNetworkPopover
|
||||
case "Disk":
|
||||
return $showingDiskPopover
|
||||
default:
|
||||
return .constant(false)
|
||||
}
|
||||
}
|
||||
|
||||
private func rankingTypeForGraph(_ graphData: GraphData) -> ProcessRankingType {
|
||||
|
||||
private func rankingTypeForGraph(_ graphData: GraphData) -> ProcessRankingType? {
|
||||
switch graphData.title {
|
||||
case "CPU":
|
||||
return .cpu
|
||||
|
|
@ -271,8 +287,12 @@ struct NotchStatsView: View {
|
|||
return .memory
|
||||
case "GPU":
|
||||
return .gpu
|
||||
case "Network":
|
||||
return .network
|
||||
case "Disk":
|
||||
return .disk
|
||||
default:
|
||||
return .cpu
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -323,7 +343,6 @@ struct NotchStatsView: View {
|
|||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding()
|
||||
} else {
|
||||
// Stats content with smart grid layout
|
||||
VStack(spacing: 8) {
|
||||
statsGridLayout
|
||||
}
|
||||
|
|
@ -349,7 +368,7 @@ struct NotchStatsView: View {
|
|||
}
|
||||
.animation(.easeInOut(duration: 0.4), value: enableStatsFeature)
|
||||
.animation(.easeInOut(duration: 0.4), value: availableGraphs.count)
|
||||
.onChange(of: availableGraphs.count) { _, newCount in
|
||||
.onChange(of: availableGraphs.count) { _, _ in
|
||||
// Protect against hover interference during dynamic sizing
|
||||
isResizingForStats = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
|
||||
|
|
@ -365,6 +384,12 @@ struct NotchStatsView: View {
|
|||
.onChange(of: showingGPUPopover) { _, _ in
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.onChange(of: showingNetworkPopover) { _, _ in
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.onChange(of: showingDiskPopover) { _, _ in
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.onChange(of: isHoveringCPUPopover) { _, _ in
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
|
|
@ -374,15 +399,23 @@ struct NotchStatsView: View {
|
|||
.onChange(of: isHoveringGPUPopover) { _, _ in
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.onChange(of: isHoveringNetworkPopover) { _, _ in
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
.onChange(of: isHoveringDiskPopover) { _, _ in
|
||||
updateStatsPopoverState()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStatsPopoverState() {
|
||||
// Use the same logic as battery popover: active only when shown AND hovered
|
||||
// Also consider resize protection to prevent closing during layout changes
|
||||
let newState = (showingCPUPopover && isHoveringCPUPopover) ||
|
||||
(showingMemoryPopover && isHoveringMemoryPopover) ||
|
||||
(showingGPUPopover && isHoveringGPUPopover) ||
|
||||
isResizingForStats
|
||||
let newState = (showingCPUPopover && isHoveringCPUPopover) ||
|
||||
(showingMemoryPopover && isHoveringMemoryPopover) ||
|
||||
(showingGPUPopover && isHoveringGPUPopover) ||
|
||||
(showingNetworkPopover && isHoveringNetworkPopover) ||
|
||||
(showingDiskPopover && isHoveringDiskPopover) ||
|
||||
isResizingForStats
|
||||
if vm.isStatsPopoverActive != newState {
|
||||
vm.isStatsPopoverActive = newState
|
||||
#if DEBUG
|
||||
|
|
@ -390,6 +423,8 @@ struct NotchStatsView: View {
|
|||
print(" CPU: shown=\(showingCPUPopover), hovering=\(isHoveringCPUPopover)")
|
||||
print(" Memory: shown=\(showingMemoryPopover), hovering=\(isHoveringMemoryPopover)")
|
||||
print(" GPU: shown=\(showingGPUPopover), hovering=\(isHoveringGPUPopover)")
|
||||
print(" Network: shown=\(showingNetworkPopover), hovering=\(isHoveringNetworkPopover)")
|
||||
print(" Disk: shown=\(showingDiskPopover), hovering=\(isHoveringDiskPopover)")
|
||||
print(" Resizing: \(isResizingForStats)")
|
||||
#endif
|
||||
}
|
||||
|
|
@ -460,8 +495,8 @@ struct UnifiedStatsCard: View {
|
|||
}
|
||||
.frame(height: 36) // Match boring.notch exactly - reduced from 50
|
||||
|
||||
// Click hint - only for CPU, Memory, GPU
|
||||
if graphData.title == "CPU" || graphData.title == "Memory" || graphData.title == "GPU" {
|
||||
// Click hint - shown for graphs that open popovers
|
||||
if ["CPU", "Memory", "GPU", "Network", "Disk"].contains(graphData.title) {
|
||||
Text("Click for details")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
|
|
|
|||
66
DynamicIsland/components/Stats/CPUStatsDetailView.swift
Normal file
66
DynamicIsland/components/Stats/CPUStatsDetailView.swift
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// CPUStatsDetailView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// SwiftUI port of the stats app CPU popup layout.
|
||||
// Mirrors dashboard, history, detail rows, and top processes.
|
||||
//
|
||||
// Created by GitHub Copilot on 18/10/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CPUStatsDetailView: View {
|
||||
@ObservedObject private var statsManager = StatsManager.shared
|
||||
@State private var topProcesses: [ProcessStats] = []
|
||||
|
||||
private let systemColor = Color(red: 0.94, green: 0.32, blue: 0.28)
|
||||
private let userColor = Color(red: 0.27, green: 0.52, blue: 0.97)
|
||||
private let idleColor = Color.gray.opacity(0.28)
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
StatsCard(title: "CPU Overview") {
|
||||
CPUUsageDashboard(
|
||||
breakdown: statsManager.cpuBreakdown,
|
||||
loadAverage: statsManager.cpuLoadAverage,
|
||||
uptime: statsManager.cpuUptime,
|
||||
coreCount: statsManager.cpuCoreUsage.count,
|
||||
systemColor: systemColor,
|
||||
userColor: userColor,
|
||||
idleColor: idleColor
|
||||
)
|
||||
}
|
||||
|
||||
if !statsManager.cpuCoreUsage.isEmpty {
|
||||
StatsCard(title: "Per-Core Usage") {
|
||||
CPUCoreUsageGrid(cores: statsManager.cpuCoreUsage, accentColor: userColor)
|
||||
}
|
||||
}
|
||||
|
||||
StatsCard(title: "Details") {
|
||||
CPUDetailsGrid(
|
||||
breakdown: statsManager.cpuBreakdown,
|
||||
loadAverage: statsManager.cpuLoadAverage,
|
||||
uptime: statsManager.cpuUptime,
|
||||
coreCount: statsManager.cpuCoreUsage.count,
|
||||
systemColor: systemColor,
|
||||
userColor: userColor
|
||||
)
|
||||
}
|
||||
|
||||
StatsCard(title: "Top Processes") {
|
||||
CPUProcessList(processes: topProcesses, accentColor: userColor)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.onReceive(statsManager.$topCPUProcesses) { processes in
|
||||
topProcesses = Array(processes.prefix(8))
|
||||
}
|
||||
.onAppear {
|
||||
topProcesses = Array(statsManager.topCPUProcesses.prefix(8))
|
||||
}
|
||||
}
|
||||
}
|
||||
410
DynamicIsland/components/Stats/CPUStatsSharedViews.swift
Normal file
410
DynamicIsland/components/Stats/CPUStatsSharedViews.swift
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
//
|
||||
// CPUStatsSharedViews.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Shared CPU stats UI components reused across the notch stats tab
|
||||
// and the legacy stats panel.
|
||||
//
|
||||
// Created by GitHub Copilot on 19/10/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct StatsCard<Content: View>: View {
|
||||
let title: String
|
||||
let padding: CGFloat
|
||||
let background: Color
|
||||
let cornerRadius: CGFloat
|
||||
let content: Content
|
||||
|
||||
init(
|
||||
title: String,
|
||||
padding: CGFloat = 16,
|
||||
background: Color = Color(nsColor: .windowBackgroundColor).opacity(0.65),
|
||||
cornerRadius: CGFloat = 12,
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
self.title = title
|
||||
self.padding = padding
|
||||
self.background = background
|
||||
self.cornerRadius = cornerRadius
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
|
||||
content
|
||||
}
|
||||
.padding(padding)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
||||
.fill(background)
|
||||
)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
||||
.stroke(Color.white.opacity(0.05), lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct CPUUsageDashboard: View {
|
||||
let breakdown: CPULoadBreakdown
|
||||
let loadAverage: LoadAverage
|
||||
let uptime: TimeInterval
|
||||
let coreCount: Int
|
||||
let systemColor: Color
|
||||
let userColor: Color
|
||||
let idleColor: Color
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 28) {
|
||||
CPUSegmentDonut(
|
||||
breakdown: breakdown,
|
||||
systemColor: systemColor,
|
||||
userColor: userColor,
|
||||
idleColor: idleColor
|
||||
)
|
||||
.frame(width: 132, height: 132)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Usage")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
DetailRow(color: systemColor.opacity(0.95), label: "System", value: StatsFormatting.percentage(breakdown.system))
|
||||
DetailRow(color: userColor.opacity(0.95), label: "User", value: StatsFormatting.percentage(breakdown.user))
|
||||
DetailRow(color: idleColor.opacity(0.7), label: "Idle", value: StatsFormatting.percentage(breakdown.idle))
|
||||
DetailRow(color: nil, label: "Active", value: StatsFormatting.percentage(breakdown.activeUsage))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("System")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
DetailRow(color: nil, label: "Logical cores", value: "\(max(coreCount, 1))")
|
||||
DetailRow(color: nil, label: "Uptime", value: StatsFormatting.abbreviatedDuration(uptime))
|
||||
Divider().padding(.vertical, 4)
|
||||
DetailRow(color: nil, label: "Load 1m", value: String(format: "%.2f", loadAverage.oneMinute))
|
||||
DetailRow(color: nil, label: "Load 5m", value: String(format: "%.2f", loadAverage.fiveMinutes))
|
||||
DetailRow(color: nil, label: "Load 15m", value: String(format: "%.2f", loadAverage.fifteenMinutes))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CPUSegmentDonut: View {
|
||||
let breakdown: CPULoadBreakdown
|
||||
let systemColor: Color
|
||||
let userColor: Color
|
||||
let idleColor: Color
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(idleColor.opacity(0.3), lineWidth: 14)
|
||||
CPUUsageRing(
|
||||
breakdown: breakdown,
|
||||
systemColor: systemColor,
|
||||
userColor: userColor,
|
||||
lineWidth: 14
|
||||
)
|
||||
VStack(spacing: 4) {
|
||||
Text(StatsFormatting.percentage(breakdown.activeUsage))
|
||||
.font(.system(size: 30, weight: .bold, design: .rounded))
|
||||
Text("Active")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CPUUsageRing: View {
|
||||
let breakdown: CPULoadBreakdown
|
||||
let systemColor: Color
|
||||
let userColor: Color
|
||||
var lineWidth: CGFloat = 14
|
||||
|
||||
var body: some View {
|
||||
let segments = breakdown.normalizedSegments
|
||||
ZStack {
|
||||
if segments.system > 0 {
|
||||
RingArc(start: 0, end: segments.system, color: systemColor, lineWidth: lineWidth)
|
||||
}
|
||||
if segments.user > 0 {
|
||||
RingArc(start: segments.system, end: segments.system + segments.user, color: userColor, lineWidth: lineWidth)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RingArc: View {
|
||||
let start: Double
|
||||
let end: Double
|
||||
let color: Color
|
||||
var lineWidth: CGFloat = 14
|
||||
|
||||
var body: some View {
|
||||
Circle()
|
||||
.trim(from: CGFloat(start), to: CGFloat(end))
|
||||
.stroke(color, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
}
|
||||
}
|
||||
|
||||
struct LegendRow: View {
|
||||
let color: Color
|
||||
let title: String
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 10, height: 10)
|
||||
Text(title)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadAverageRow: View {
|
||||
let loadAverage: LoadAverage
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Load Average")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
HStack(spacing: 12) {
|
||||
LoadAverageValue(label: "1m", value: loadAverage.oneMinute)
|
||||
LoadAverageValue(label: "5m", value: loadAverage.fiveMinutes)
|
||||
LoadAverageValue(label: "15m", value: loadAverage.fifteenMinutes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadAverageValue: View {
|
||||
let label: String
|
||||
let value: Double
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 2) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
Text(String(format: "%.2f", value))
|
||||
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DetailRow: View {
|
||||
let color: Color?
|
||||
let label: String
|
||||
let value: String
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
if let color {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 8, height: 8)
|
||||
}
|
||||
Text(label)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CPUCoreUsageGrid: View {
|
||||
let cores: [CPUCoreUsage]
|
||||
let accentColor: Color
|
||||
|
||||
private var columns: [GridItem] {
|
||||
[GridItem(.adaptive(minimum: 90), spacing: 14, alignment: .top)]
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
LazyVGrid(columns: columns, spacing: 14) {
|
||||
ForEach(cores) { core in
|
||||
CPUCoreUsageCell(core: core, accentColor: accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CPUCoreUsageCell: View {
|
||||
let core: CPUCoreUsage
|
||||
let accentColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Text("Core \(core.id + 1)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(StatsFormatting.percentage(core.usage))
|
||||
.font(.system(size: 12, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(accentColor)
|
||||
}
|
||||
GeometryReader { proxy in
|
||||
let width = proxy.size.width
|
||||
let progress = max(0, min(core.usage / 100, 1))
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule()
|
||||
.fill(Color.primary.opacity(0.08))
|
||||
Capsule()
|
||||
.fill(accentColor)
|
||||
.frame(width: width * progress)
|
||||
}
|
||||
}
|
||||
.frame(height: 8)
|
||||
}
|
||||
.padding(10)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.fill(Color(nsColor: .controlBackgroundColor).opacity(0.6))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct CPUDetailsGrid: View {
|
||||
let breakdown: CPULoadBreakdown
|
||||
let loadAverage: LoadAverage
|
||||
let uptime: TimeInterval
|
||||
let coreCount: Int
|
||||
let systemColor: Color
|
||||
let userColor: Color
|
||||
var showLoadAverage: Bool = true
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Usage Breakdown")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
DetailRow(color: nil, label: "Total", value: StatsFormatting.percentage(breakdown.activeUsage))
|
||||
DetailRow(color: systemColor, label: "System", value: StatsFormatting.percentage(breakdown.system))
|
||||
DetailRow(color: userColor, label: "User", value: StatsFormatting.percentage(breakdown.user))
|
||||
DetailRow(color: Color.gray.opacity(0.4), label: "Idle", value: StatsFormatting.percentage(breakdown.idle))
|
||||
}
|
||||
|
||||
Divider().padding(.vertical, 4)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("System")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
DetailRow(color: nil, label: "Logical cores", value: "\(max(coreCount, 1))")
|
||||
DetailRow(color: nil, label: "Uptime", value: StatsFormatting.abbreviatedDuration(uptime))
|
||||
}
|
||||
|
||||
if showLoadAverage {
|
||||
Divider().padding(.vertical, 4)
|
||||
LoadAverageRow(loadAverage: loadAverage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct CPUProcessList: View {
|
||||
let processes: [ProcessStats]
|
||||
let accentColor: Color
|
||||
var displayLimit: Int? = nil
|
||||
|
||||
private var visibleProcesses: [ProcessStats] {
|
||||
if let limit = displayLimit {
|
||||
return Array(processes.prefix(limit))
|
||||
}
|
||||
return processes
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
if visibleProcesses.isEmpty {
|
||||
Text("No process data yet. Keep stats open for a moment.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {
|
||||
ForEach(visibleProcesses) { process in
|
||||
CPUProcessRow(process: process, accentColor: accentColor)
|
||||
if process.id != visibleProcesses.last?.id {
|
||||
Divider().padding(.leading, 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CPUProcessRow: View {
|
||||
let process: ProcessStats
|
||||
let accentColor: Color
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
iconView
|
||||
.frame(width: 24, height: 24)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 6, style: .continuous)
|
||||
.stroke(Color.white.opacity(0.08), lineWidth: 0.5)
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(process.name)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
Text(process.memoryUsageString)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(process.cpuUsageString)
|
||||
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
private var iconView: some View {
|
||||
Group {
|
||||
if let icon = process.icon {
|
||||
Image(nsImage: icon)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
} else {
|
||||
ZStack {
|
||||
accentColor.opacity(0.15)
|
||||
Image(systemName: "app")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
220
DynamicIsland/components/Stats/DiskStatsDetailView.swift
Normal file
220
DynamicIsland/components/Stats/DiskStatsDetailView.swift
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
//
|
||||
// DiskStatsDetailView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Disk detail dashboard mirroring Stats app cards.
|
||||
//
|
||||
// Created by GitHub Copilot on 19/10/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DiskStatsDetailView: View {
|
||||
@ObservedObject private var statsManager = StatsManager.shared
|
||||
|
||||
private let readColor = Color.cyan
|
||||
private let writeColor = Color.yellow
|
||||
private let cardBackground = Color(nsColor: .windowBackgroundColor).opacity(0.65)
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
StatsCard(title: "Disk Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
DiskOverview(
|
||||
readSpeed: statsManager.diskRead,
|
||||
writeSpeed: statsManager.diskWrite,
|
||||
peakRead: statsManager.diskReadHistory.max() ?? 0,
|
||||
peakWrite: statsManager.diskWriteHistory.max() ?? 0,
|
||||
readColor: readColor,
|
||||
writeColor: writeColor
|
||||
)
|
||||
}
|
||||
|
||||
StatsCard(title: "Storage Devices", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
DiskDevicesCard(devices: statsManager.diskDevices, accentColor: readColor)
|
||||
}
|
||||
|
||||
StatsCard(title: "Session Totals", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
DiskTotalsView(totals: statsManager.diskTotals, readColor: readColor, writeColor: writeColor)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.frame(minWidth: 360, minHeight: 340)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DiskOverview: View {
|
||||
let readSpeed: Double
|
||||
let writeSpeed: Double
|
||||
let peakRead: Double
|
||||
let peakWrite: Double
|
||||
let readColor: Color
|
||||
let writeColor: Color
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 24) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Read")
|
||||
.font(.headline)
|
||||
.foregroundColor(readColor)
|
||||
Text(StatsFormatting.mbPerSecond(readSpeed))
|
||||
.font(.system(size: 26, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
DetailRow(color: readColor.opacity(0.6), label: "Peak", value: StatsFormatting.mbPerSecond(peakRead))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Divider()
|
||||
.frame(height: 110)
|
||||
.overlay(Color.white.opacity(0.06))
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Write")
|
||||
.font(.headline)
|
||||
.foregroundColor(writeColor)
|
||||
Text(StatsFormatting.mbPerSecond(writeSpeed))
|
||||
.font(.system(size: 26, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
DetailRow(color: writeColor.opacity(0.6), label: "Peak", value: StatsFormatting.mbPerSecond(peakWrite))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DiskTotalsView: View {
|
||||
let totals: DiskTotals
|
||||
let readColor: Color
|
||||
let writeColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
DetailRow(color: readColor.opacity(0.75), label: "Read", value: formattedBytes(totals.readMB))
|
||||
DetailRow(color: writeColor.opacity(0.75), label: "Written", value: formattedBytes(totals.writtenMB))
|
||||
}
|
||||
}
|
||||
|
||||
private func formattedBytes(_ megabytes: Double) -> String {
|
||||
let bytes = UInt64(megabytes * 1_048_576)
|
||||
return StatsFormatting.bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
private struct DiskDevicesCard: View {
|
||||
let devices: [DiskDeviceMetrics]
|
||||
let accentColor: Color
|
||||
|
||||
private var sortedDevices: [DiskDeviceMetrics] {
|
||||
devices.sorted { lhs, rhs in
|
||||
if lhs.isRoot != rhs.isRoot {
|
||||
return lhs.isRoot && !rhs.isRoot
|
||||
}
|
||||
if lhs.isRemovable != rhs.isRemovable {
|
||||
return !lhs.isRemovable && rhs.isRemovable
|
||||
}
|
||||
return lhs.name.localizedCaseInsensitiveCompare(rhs.name) == .orderedAscending
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if sortedDevices.isEmpty {
|
||||
Text("No storage devices detected yet.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {
|
||||
VStack(spacing: 12) {
|
||||
ForEach(sortedDevices) { device in
|
||||
DiskDeviceRow(device: device, accentColor: accentColor)
|
||||
if device.id != sortedDevices.last?.id {
|
||||
Divider().padding(.leading, 36)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DiskDeviceRow: View {
|
||||
let device: DiskDeviceMetrics
|
||||
let accentColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(spacing: 10) {
|
||||
RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
.fill(accentColor.opacity(0.16))
|
||||
.overlay(
|
||||
Image(systemName: device.isRemovable ? "externaldrive" : "internaldrive")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(accentColor)
|
||||
)
|
||||
.frame(width: 32, height: 32)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(device.name)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
Text(device.path.path)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer()
|
||||
if device.isRoot {
|
||||
Text("System")
|
||||
.font(.caption2)
|
||||
.foregroundColor(accentColor)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(accentColor.opacity(0.16))
|
||||
.clipShape(Capsule())
|
||||
} else if device.isRemovable {
|
||||
Text("External")
|
||||
.font(.caption2)
|
||||
.foregroundColor(accentColor)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(accentColor.opacity(0.16))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
DiskUsageBar(usedBytes: device.usedBytes, totalBytes: device.totalBytes, tint: accentColor)
|
||||
.frame(height: 8)
|
||||
|
||||
HStack {
|
||||
Text("Used · \(StatsFormatting.bytes(device.usedBytes))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text("Free · \(StatsFormatting.bytes(device.freeBytes))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("Total · \(StatsFormatting.bytes(device.totalBytes))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct DiskUsageBar: View {
|
||||
let usedBytes: UInt64
|
||||
let totalBytes: UInt64
|
||||
let tint: Color
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
let width = proxy.size.width
|
||||
let progress = totalBytes > 0 ? min(max(Double(usedBytes) / Double(totalBytes), 0), 1) : 0
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule()
|
||||
.fill(Color.primary.opacity(0.08))
|
||||
Capsule()
|
||||
.fill(tint.opacity(0.8))
|
||||
.frame(width: width * progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
190
DynamicIsland/components/Stats/GPUStatsDetailView.swift
Normal file
190
DynamicIsland/components/Stats/GPUStatsDetailView.swift
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
//
|
||||
// GPUStatsDetailView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// GPU detail dashboard inspired by the Stats app layout.
|
||||
//
|
||||
// Created by GitHub Copilot on 19/10/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct GPUStatsDetailView: View {
|
||||
@ObservedObject private var statsManager = StatsManager.shared
|
||||
@State private var topProcesses: [ProcessStats] = []
|
||||
|
||||
private let accentColor = Color.purple
|
||||
private let cardBackground = Color(nsColor: .windowBackgroundColor).opacity(0.65)
|
||||
private let processDisplayLimit = 8
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
StatsCard(title: "GPU Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
GPUUsageDashboard(
|
||||
usage: statsManager.gpuUsage,
|
||||
breakdown: statsManager.gpuBreakdown,
|
||||
averageUsage: statsManager.avgGpuUsage,
|
||||
lastUpdated: statsManager.lastUpdated,
|
||||
accentColor: accentColor,
|
||||
primaryDevice: statsManager.gpuDevices.first
|
||||
)
|
||||
}
|
||||
|
||||
if !statsManager.gpuDevices.isEmpty {
|
||||
StatsCard(title: "Devices", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
GPUDeviceList(devices: statsManager.gpuDevices, accentColor: accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
StatsCard(title: "Top Processes", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
CPUProcessList(processes: topProcesses, accentColor: accentColor, displayLimit: processDisplayLimit)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.frame(minWidth: 360, minHeight: 380)
|
||||
.onAppear(perform: refreshProcesses)
|
||||
.onReceive(statsManager.$lastUpdated) { _ in
|
||||
refreshProcesses()
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshProcesses() {
|
||||
let processes = statsManager.getProcessesRankedByGPU()
|
||||
topProcesses = Array(processes.prefix(processDisplayLimit))
|
||||
}
|
||||
}
|
||||
|
||||
private struct GPUUsageDashboard: View {
|
||||
let usage: Double
|
||||
let breakdown: GPUBreakdown
|
||||
let averageUsage: Double
|
||||
let lastUpdated: Date
|
||||
let accentColor: Color
|
||||
let primaryDevice: GPUDeviceMetrics?
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 24) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(accentColor.opacity(0.25), lineWidth: 14)
|
||||
.frame(width: 120, height: 120)
|
||||
|
||||
RingArc(start: 0, end: CGFloat(min(max(usage / 100, 0), 1)), color: accentColor, lineWidth: 14)
|
||||
.frame(width: 120, height: 120)
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text(StatsFormatting.percentage(usage))
|
||||
.font(.system(size: 28, weight: .bold, design: .rounded))
|
||||
Text("Active")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
DetailRow(color: accentColor.opacity(0.9), label: "Render", value: StatsFormatting.percentage(breakdown.render))
|
||||
DetailRow(color: accentColor.opacity(0.7), label: "Compute", value: StatsFormatting.percentage(breakdown.compute))
|
||||
DetailRow(color: accentColor.opacity(0.55), label: "Video", value: StatsFormatting.percentage(breakdown.video))
|
||||
DetailRow(color: accentColor.opacity(0.45), label: "Other", value: StatsFormatting.percentage(breakdown.other))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Averages")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
DetailRow(color: nil, label: "Session Avg", value: StatsFormatting.percentage(averageUsage))
|
||||
DetailRow(color: nil, label: "Last Update", value: formattedTimestamp(lastUpdated))
|
||||
if let device = primaryDevice {
|
||||
Divider().padding(.vertical, 4)
|
||||
DetailRow(color: nil, label: "Active GPU", value: device.formattedVendorModel)
|
||||
DetailRow(color: nil, label: "Status", value: device.isActive ? "Active" : "Idle")
|
||||
DetailRow(color: nil, label: "Temperature", value: device.temperatureText)
|
||||
if let cores = device.cores {
|
||||
DetailRow(color: nil, label: "Cores", value: "\(cores)")
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
private func formattedTimestamp(_ date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .none
|
||||
formatter.timeStyle = .medium
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
private struct GPUDeviceList: View {
|
||||
let devices: [GPUDeviceMetrics]
|
||||
let accentColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
ForEach(Array(devices.enumerated()), id: \.element.id) { index, device in
|
||||
GPUDeviceRow(device: device, accentColor: accentColor)
|
||||
if index < devices.count - 1 {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct GPUDeviceRow: View {
|
||||
let device: GPUDeviceMetrics
|
||||
let accentColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack(spacing: 12) {
|
||||
Circle()
|
||||
.fill(device.isActive ? Color.green.opacity(0.85) : Color.red.opacity(0.7))
|
||||
.frame(width: 8, height: 8)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(device.formattedVendorModel)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
Text(device.isActive ? "Active" : "Idle")
|
||||
.font(.caption2)
|
||||
.foregroundColor(device.isActive ? .green : .secondary)
|
||||
}
|
||||
Spacer()
|
||||
VStack(alignment: .trailing, spacing: 2) {
|
||||
Text(device.utilizationText)
|
||||
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(accentColor)
|
||||
Text("Utilization")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
VStack(spacing: 8) {
|
||||
if let render = device.renderUtilization {
|
||||
DetailRow(color: accentColor.opacity(0.9), label: "Renderer", value: StatsFormatting.percentage(render))
|
||||
}
|
||||
if let tiler = device.tilerUtilization {
|
||||
DetailRow(color: accentColor.opacity(0.6), label: "Tiler", value: StatsFormatting.percentage(tiler))
|
||||
}
|
||||
if let temp = device.temperature {
|
||||
DetailRow(color: nil, label: "Temperature", value: String(format: "%.0f°C", temp))
|
||||
}
|
||||
if let fan = device.fanSpeed {
|
||||
DetailRow(color: nil, label: "Fan Speed", value: "\(fan)%")
|
||||
}
|
||||
if let coreClock = device.coreClock {
|
||||
DetailRow(color: nil, label: "Core Clock", value: "\(coreClock) MHz")
|
||||
}
|
||||
if let memoryClock = device.memoryClock {
|
||||
DetailRow(color: nil, label: "Memory Clock", value: "\(memoryClock) MHz")
|
||||
}
|
||||
if let cores = device.cores {
|
||||
DetailRow(color: nil, label: "Cores", value: "\(cores)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
255
DynamicIsland/components/Stats/MemoryStatsDetailView.swift
Normal file
255
DynamicIsland/components/Stats/MemoryStatsDetailView.swift
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
//
|
||||
// MemoryStatsDetailView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Detailed memory dashboard mirroring the Stats app layout.
|
||||
//
|
||||
// Created by GitHub Copilot on 19/10/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MemoryStatsDetailView: View {
|
||||
@ObservedObject private var statsManager = StatsManager.shared
|
||||
@State private var topProcesses: [ProcessStats] = []
|
||||
|
||||
private let accentColor = Color.green
|
||||
private let cardBackground = Color(nsColor: .windowBackgroundColor).opacity(0.65)
|
||||
private let processDisplayLimit = 8
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
StatsCard(title: "Memory Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
MemoryUsageDashboard(breakdown: statsManager.memoryBreakdown, accentColor: accentColor)
|
||||
}
|
||||
|
||||
StatsCard(title: "Pressure & Swap", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
MemoryHealthView(breakdown: statsManager.memoryBreakdown, accentColor: accentColor)
|
||||
}
|
||||
|
||||
StatsCard(title: "Details", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
MemoryDetailsGrid(breakdown: statsManager.memoryBreakdown, accentColor: accentColor)
|
||||
}
|
||||
|
||||
StatsCard(title: "Top Processes", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
CPUProcessList(processes: topProcesses, accentColor: accentColor, displayLimit: processDisplayLimit)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.frame(minWidth: 380, minHeight: 420)
|
||||
.onAppear(perform: refreshProcesses)
|
||||
.onReceive(statsManager.$lastUpdated) { _ in
|
||||
refreshProcesses()
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshProcesses() {
|
||||
let processes = statsManager.getProcessesRankedByMemory()
|
||||
topProcesses = Array(processes.prefix(processDisplayLimit))
|
||||
}
|
||||
}
|
||||
|
||||
private struct MemoryUsageDashboard: View {
|
||||
let breakdown: MemoryBreakdown
|
||||
let accentColor: Color
|
||||
|
||||
private let freeColor = Color.gray.opacity(0.28)
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 28) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(freeColor.opacity(0.35), lineWidth: 14)
|
||||
.frame(width: 128, height: 128)
|
||||
|
||||
MemoryUsageRing(breakdown: breakdown, usedColor: accentColor, freeColor: freeColor)
|
||||
.frame(width: 128, height: 128)
|
||||
|
||||
VStack(spacing: 4) {
|
||||
Text(StatsFormatting.percentage(breakdown.usedPercentage))
|
||||
.font(.system(size: 30, weight: .bold, design: .rounded))
|
||||
Text("Used")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
DetailRow(color: accentColor.opacity(0.8), label: "Used", value: formattedBytes(breakdown.usedBytes))
|
||||
DetailRow(color: freeColor.opacity(0.9), label: "Free", value: formattedBytes(breakdown.freeBytes))
|
||||
DetailRow(color: nil, label: "Total", value: formattedBytes(breakdown.totalBytes))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Breakdown")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
MemoryBreakdownRow(label: "App", value: breakdown.appBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.85))
|
||||
MemoryBreakdownRow(label: "Cached", value: breakdown.cacheBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.65))
|
||||
MemoryBreakdownRow(label: "Wired", value: breakdown.wiredBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.45))
|
||||
MemoryBreakdownRow(label: "Compressed", value: breakdown.compressedBytes, total: breakdown.totalBytes, color: accentColor.opacity(0.35))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
private func formattedBytes(_ value: UInt64) -> String {
|
||||
StatsFormatting.bytes(value)
|
||||
}
|
||||
}
|
||||
|
||||
private struct MemoryUsageRing: View {
|
||||
let breakdown: MemoryBreakdown
|
||||
let usedColor: Color
|
||||
let freeColor: Color
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
let usedFraction = CGFloat(min(max(breakdown.usedPercentage / 100, 0), 1))
|
||||
if usedFraction > 0 {
|
||||
RingArc(start: 0, end: usedFraction, color: usedColor, lineWidth: 14)
|
||||
}
|
||||
if usedFraction < 1 {
|
||||
RingArc(start: usedFraction, end: 1, color: freeColor.opacity(0.65), lineWidth: 14)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MemoryBreakdownRow: View {
|
||||
let label: String
|
||||
let value: UInt64
|
||||
let total: UInt64
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
let percent = total > 0 ? Double(value) / Double(total) * 100 : 0
|
||||
return DetailRow(color: color, label: label, value: "\(StatsFormatting.bytes(value)) · \(StatsFormatting.percentage(percent))")
|
||||
}
|
||||
}
|
||||
|
||||
private struct MemoryDetailsGrid: View {
|
||||
let breakdown: MemoryBreakdown
|
||||
let accentColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
DetailRow(color: accentColor.opacity(0.85), label: "App Memory", value: StatsFormatting.bytes(breakdown.appBytes))
|
||||
DetailRow(color: accentColor.opacity(0.7), label: "Cached Files", value: StatsFormatting.bytes(breakdown.cacheBytes))
|
||||
DetailRow(color: accentColor.opacity(0.6), label: "Active", value: StatsFormatting.bytes(breakdown.activeBytes))
|
||||
DetailRow(color: accentColor.opacity(0.45), label: "Inactive", value: StatsFormatting.bytes(breakdown.inactiveBytes))
|
||||
DetailRow(color: accentColor.opacity(0.35), label: "Wired", value: StatsFormatting.bytes(breakdown.wiredBytes))
|
||||
DetailRow(color: accentColor.opacity(0.25), label: "Compressed", value: StatsFormatting.bytes(breakdown.compressedBytes))
|
||||
DetailRow(color: accentColor.opacity(0.18), label: "Free", value: StatsFormatting.bytes(breakdown.freeBytes))
|
||||
Divider().padding(.vertical, 4)
|
||||
DetailRow(color: nil, label: "Total Memory", value: StatsFormatting.bytes(breakdown.totalBytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MemoryHealthView: View {
|
||||
let breakdown: MemoryBreakdown
|
||||
let accentColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
MemoryPressureIndicator(pressure: breakdown.pressure)
|
||||
Divider().padding(.vertical, 4)
|
||||
SwapUsageView(swap: breakdown.swap, accentColor: accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct MemoryPressureIndicator: View {
|
||||
let pressure: MemoryPressure
|
||||
|
||||
private var status: (title: String, color: Color, description: String) {
|
||||
switch pressure.level {
|
||||
case .normal:
|
||||
return ("Normal", Color.green, "System memory pressure is nominal.")
|
||||
case .warning:
|
||||
return ("Warning", Color.orange, "Memory pressure is elevated. Closing apps may help.")
|
||||
case .critical:
|
||||
return ("Critical", Color.red, "Memory pressure is critical; the system may purge aggressively.")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Pressure")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
HStack {
|
||||
Capsule()
|
||||
.fill(status.color.opacity(0.16))
|
||||
.overlay(
|
||||
HStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(status.color)
|
||||
.frame(width: 10, height: 10)
|
||||
Text(status.title)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundColor(status.color)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
)
|
||||
.frame(height: 28)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Text(status.description)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SwapUsageView: View {
|
||||
let swap: MemorySwap
|
||||
let accentColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Swap")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if swap.totalBytes > 0 {
|
||||
DetailRow(color: accentColor.opacity(0.85), label: "Used", value: StatsFormatting.bytes(swap.usedBytes))
|
||||
DetailRow(color: accentColor.opacity(0.55), label: "Free", value: StatsFormatting.bytes(swap.freeBytes))
|
||||
DetailRow(color: nil, label: "Total", value: StatsFormatting.bytes(swap.totalBytes))
|
||||
SwapUsageBar(usedBytes: swap.usedBytes, totalBytes: swap.totalBytes, tint: accentColor)
|
||||
.frame(height: 8)
|
||||
} else {
|
||||
Text("Swap is disabled on this system.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SwapUsageBar: View {
|
||||
let usedBytes: UInt64
|
||||
let totalBytes: UInt64
|
||||
let tint: Color
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { proxy in
|
||||
let width = proxy.size.width
|
||||
let progress = totalBytes > 0 ? min(max(Double(usedBytes) / Double(totalBytes), 0), 1) : 0
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule()
|
||||
.fill(Color.primary.opacity(0.08))
|
||||
Capsule()
|
||||
.fill(tint.opacity(0.8))
|
||||
.frame(width: width * progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
264
DynamicIsland/components/Stats/NetworkStatsDetailView.swift
Normal file
264
DynamicIsland/components/Stats/NetworkStatsDetailView.swift
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
//
|
||||
// NetworkStatsDetailView.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Network detail dashboard with throughput history and totals.
|
||||
//
|
||||
// Created by GitHub Copilot on 19/10/2025.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct NetworkStatsDetailView: View {
|
||||
@ObservedObject private var statsManager = StatsManager.shared
|
||||
|
||||
private let downloadColor = Color.orange
|
||||
private let uploadColor = Color.red
|
||||
private let cardBackground = Color(nsColor: .windowBackgroundColor).opacity(0.65)
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
StatsCard(title: "Network Overview", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
NetworkOverview(
|
||||
download: statsManager.networkDownload,
|
||||
upload: statsManager.networkUpload,
|
||||
avgDownload: average(statsManager.networkDownloadHistory),
|
||||
avgUpload: average(statsManager.networkUploadHistory),
|
||||
peakDownload: statsManager.networkDownloadHistory.max() ?? 0,
|
||||
peakUpload: statsManager.networkUploadHistory.max() ?? 0,
|
||||
downloadColor: downloadColor,
|
||||
uploadColor: uploadColor
|
||||
)
|
||||
}
|
||||
|
||||
StatsCard(title: "Interfaces", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
NetworkInterfacesCard(
|
||||
interfaces: statsManager.networkInterfaces,
|
||||
downloadColor: downloadColor,
|
||||
uploadColor: uploadColor
|
||||
)
|
||||
}
|
||||
|
||||
StatsCard(title: "Session Totals", padding: 16, background: cardBackground, cornerRadius: 12) {
|
||||
NetworkTotalsView(totals: statsManager.networkTotals, downloadColor: downloadColor, uploadColor: uploadColor)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
}
|
||||
.frame(minWidth: 360, minHeight: 340)
|
||||
}
|
||||
|
||||
private func average(_ values: [Double]) -> Double {
|
||||
guard !values.isEmpty else { return 0 }
|
||||
let filtered = values.filter { $0 > 0 }
|
||||
guard !filtered.isEmpty else { return 0 }
|
||||
return filtered.reduce(0, +) / Double(filtered.count)
|
||||
}
|
||||
}
|
||||
|
||||
private struct NetworkOverview: View {
|
||||
let download: Double
|
||||
let upload: Double
|
||||
let avgDownload: Double
|
||||
let avgUpload: Double
|
||||
let peakDownload: Double
|
||||
let peakUpload: Double
|
||||
let downloadColor: Color
|
||||
let uploadColor: Color
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 24) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Download")
|
||||
.font(.headline)
|
||||
.foregroundColor(downloadColor)
|
||||
Text(StatsFormatting.mbPerSecond(download))
|
||||
.font(.system(size: 26, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
DetailRow(color: downloadColor.opacity(0.6), label: "Average", value: StatsFormatting.mbPerSecond(avgDownload))
|
||||
DetailRow(color: downloadColor.opacity(0.45), label: "Peak", value: StatsFormatting.mbPerSecond(peakDownload))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Divider()
|
||||
.frame(height: 110)
|
||||
.overlay(Color.white.opacity(0.06))
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("Upload")
|
||||
.font(.headline)
|
||||
.foregroundColor(uploadColor)
|
||||
Text(StatsFormatting.mbPerSecond(upload))
|
||||
.font(.system(size: 26, weight: .bold, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
DetailRow(color: uploadColor.opacity(0.6), label: "Average", value: StatsFormatting.mbPerSecond(avgUpload))
|
||||
DetailRow(color: uploadColor.opacity(0.45), label: "Peak", value: StatsFormatting.mbPerSecond(peakUpload))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NetworkTotalsView: View {
|
||||
let totals: NetworkTotals
|
||||
let downloadColor: Color
|
||||
let uploadColor: Color
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
DetailRow(color: downloadColor.opacity(0.75), label: "Downloaded", value: formattedBytes(totals.downloadedMB))
|
||||
DetailRow(color: uploadColor.opacity(0.75), label: "Uploaded", value: formattedBytes(totals.uploadedMB))
|
||||
}
|
||||
}
|
||||
|
||||
private func formattedBytes(_ megabytes: Double) -> String {
|
||||
let bytes = UInt64(megabytes * 1_048_576)
|
||||
return StatsFormatting.bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
private struct NetworkInterfacesCard: View {
|
||||
let interfaces: [NetworkInterfaceMetrics]
|
||||
let downloadColor: Color
|
||||
let uploadColor: Color
|
||||
|
||||
private var sortedInterfaces: [NetworkInterfaceMetrics] {
|
||||
interfaces.sorted { lhs, rhs in
|
||||
if lhs.isActive != rhs.isActive {
|
||||
return lhs.isActive && !rhs.isActive
|
||||
}
|
||||
return interfacePriority(lhs.type) < interfacePriority(rhs.type)
|
||||
}
|
||||
}
|
||||
|
||||
private func interfacePriority(_ type: NetworkInterfaceType) -> Int {
|
||||
switch type {
|
||||
case .wifi:
|
||||
return 0
|
||||
case .ethernet:
|
||||
return 1
|
||||
case .cellular:
|
||||
return 2
|
||||
case .other:
|
||||
return 3
|
||||
case .loopback:
|
||||
return 4
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if sortedInterfaces.isEmpty {
|
||||
Text("No active interfaces detected yet.")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {
|
||||
VStack(spacing: 12) {
|
||||
ForEach(sortedInterfaces) { interface in
|
||||
NetworkInterfaceRow(
|
||||
interface: interface,
|
||||
downloadColor: downloadColor,
|
||||
uploadColor: uploadColor
|
||||
)
|
||||
if interface.id != sortedInterfaces.last?.id {
|
||||
Divider().padding(.leading, 44)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct NetworkInterfaceRow: View {
|
||||
let interface: NetworkInterfaceMetrics
|
||||
let downloadColor: Color
|
||||
let uploadColor: Color
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
icon
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 8) {
|
||||
Text(interface.displayName)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundColor(.primary)
|
||||
if interface.isActive {
|
||||
Text("Active")
|
||||
.font(.caption2)
|
||||
.foregroundColor(downloadColor)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(downloadColor.opacity(0.16))
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
|
||||
if let ipv4 = interface.ipv4, !ipv4.isEmpty {
|
||||
Text("IPv4 · \(ipv4)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
if let ipv6 = interface.ipv6, !ipv6.isEmpty {
|
||||
Text("IPv6 · \(ipv6)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("Session · ↓ \(formattedBytes(interface.totalDownloaded)) · ↑ \(formattedBytes(interface.totalUploaded))")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
metricRow(label: "Down", value: StatsFormatting.mbPerSecond(interface.currentDownload), color: downloadColor)
|
||||
metricRow(label: "Up", value: StatsFormatting.mbPerSecond(interface.currentUpload), color: uploadColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var icon: some View {
|
||||
let isActive = interface.isActive
|
||||
return RoundedRectangle(cornerRadius: 8, style: .continuous)
|
||||
.fill(isActive ? downloadColor.opacity(0.18) : Color.primary.opacity(0.05))
|
||||
.overlay(
|
||||
Image(systemName: iconName)
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundColor(isActive ? downloadColor : .secondary)
|
||||
)
|
||||
.frame(width: 32, height: 32)
|
||||
}
|
||||
|
||||
private var iconName: String {
|
||||
switch interface.type {
|
||||
case .wifi:
|
||||
return "wifi"
|
||||
case .ethernet:
|
||||
return "cable.connector"
|
||||
case .cellular:
|
||||
return "antenna.radiowaves.left.and.right"
|
||||
case .loopback:
|
||||
return "arrow.triangle.2.circlepath"
|
||||
case .other:
|
||||
return "network"
|
||||
}
|
||||
}
|
||||
|
||||
private func metricRow(label: String, value: String, color: Color) -> some View {
|
||||
HStack(spacing: 6) {
|
||||
Text(label)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
Text(value)
|
||||
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||
.foregroundColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
private func formattedBytes(_ megabytes: Double) -> String {
|
||||
let bytes = UInt64(max(megabytes, 0) * 1_048_576)
|
||||
return StatsFormatting.bytes(bytes)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,12 +34,16 @@ enum ProcessRankingType {
|
|||
case cpu
|
||||
case memory
|
||||
case gpu
|
||||
case network
|
||||
case disk
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .cpu: return "CPU Usage"
|
||||
case .memory: return "Memory Usage"
|
||||
case .memory: return "Memory Usage"
|
||||
case .gpu: return "GPU Usage"
|
||||
case .network: return "Network Activity"
|
||||
case .disk: return "Disk Activity"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -48,6 +52,8 @@ enum ProcessRankingType {
|
|||
case .cpu: return "cpu"
|
||||
case .memory: return "memorychip"
|
||||
case .gpu: return "display"
|
||||
case .network: return "network"
|
||||
case .disk: return "internaldrive"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,6 +62,8 @@ enum ProcessRankingType {
|
|||
case .cpu: return .blue
|
||||
case .memory: return .green
|
||||
case .gpu: return .purple
|
||||
case .network: return .orange
|
||||
case .disk: return .cyan
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,188 +9,59 @@ import SwiftUI
|
|||
|
||||
struct RankedProcessPopover: View {
|
||||
let rankingType: ProcessRankingType
|
||||
@ObservedObject var statsManager = StatsManager.shared
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@State private var isHovering = false
|
||||
|
||||
// Callback to notify parent about hover state
|
||||
var onHoverChange: ((Bool) -> Void)?
|
||||
|
||||
private var rankedProcesses: [ProcessStats] {
|
||||
private var configuration: (width: CGFloat, minHeight: CGFloat, padding: CGFloat) {
|
||||
switch rankingType {
|
||||
case .cpu:
|
||||
return statsManager.getProcessesRankedByCPU()
|
||||
return (420, 420, 0)
|
||||
case .memory:
|
||||
return statsManager.getProcessesRankedByMemory()
|
||||
return (400, 380, 0)
|
||||
case .gpu:
|
||||
return statsManager.getProcessesRankedByGPU()
|
||||
return (380, 360, 0)
|
||||
case .network, .disk:
|
||||
return (360, 320, 0)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
// Header
|
||||
HStack {
|
||||
Image(systemName: rankingType.icon)
|
||||
.font(.title3)
|
||||
.foregroundColor(rankingType.color)
|
||||
|
||||
Text("Top Processes by \(rankingType.title)")
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Spacer()
|
||||
|
||||
popoverContent
|
||||
.padding(configuration.padding)
|
||||
.frame(width: configuration.width)
|
||||
.frame(minHeight: configuration.minHeight)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
|
||||
.shadow(color: .black.opacity(0.28), radius: 12, x: 0, y: 8)
|
||||
.overlay(alignment: .topTrailing) {
|
||||
Button(action: { dismiss() }) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(10)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
.padding(.bottom, 4)
|
||||
|
||||
Divider()
|
||||
|
||||
// Process list
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 6) {
|
||||
ForEach(Array(rankedProcesses.prefix(15).enumerated()), id: \.element.id) { index, process in
|
||||
RankedProcessRow(
|
||||
process: process,
|
||||
rank: index + 1,
|
||||
rankingType: rankingType
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
.onHover { hovering in
|
||||
onHoverChange?(hovering)
|
||||
}
|
||||
.frame(maxHeight: 300)
|
||||
|
||||
// Footer with refresh button
|
||||
HStack {
|
||||
Text("\(rankedProcesses.count) processes")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Refresh") {
|
||||
// Trigger stats update
|
||||
if statsManager.isMonitoring {
|
||||
// Stats will auto-refresh
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundColor(rankingType.color)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.frame(width: 320)
|
||||
.frame(minHeight: 200)
|
||||
.background(Color(NSColor.controlBackgroundColor))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.shadow(color: .black.opacity(0.3), radius: 8, x: 0, y: 4)
|
||||
.onHover { hovering in
|
||||
isHovering = hovering
|
||||
onHoverChange?(hovering)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RankedProcessRow: View {
|
||||
let process: ProcessStats
|
||||
let rank: Int
|
||||
let rankingType: ProcessRankingType
|
||||
@State private var isHovered = false
|
||||
|
||||
private var primaryValue: String {
|
||||
@ViewBuilder
|
||||
private var popoverContent: some View {
|
||||
switch rankingType {
|
||||
case .cpu:
|
||||
return process.cpuUsageString
|
||||
CPUStatsDetailView()
|
||||
case .memory:
|
||||
return process.memoryUsageString
|
||||
MemoryStatsDetailView()
|
||||
case .gpu:
|
||||
return process.cpuUsageString // Using CPU as proxy for GPU
|
||||
GPUStatsDetailView()
|
||||
case .network:
|
||||
NetworkStatsDetailView()
|
||||
case .disk:
|
||||
DiskStatsDetailView()
|
||||
}
|
||||
}
|
||||
|
||||
private var secondaryValue: String {
|
||||
switch rankingType {
|
||||
case .cpu:
|
||||
return process.memoryUsageString
|
||||
case .memory:
|
||||
return process.cpuUsageString
|
||||
case .gpu:
|
||||
return process.memoryUsageString
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
// Rank
|
||||
Text("\(rank)")
|
||||
.font(.caption)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(rankingType.color)
|
||||
.frame(width: 24, alignment: .center)
|
||||
|
||||
// App icon
|
||||
Group {
|
||||
if let icon = process.icon {
|
||||
Image(nsImage: icon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} else {
|
||||
Image(systemName: "app")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(width: 20, height: 20)
|
||||
|
||||
// Process info
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(process.name)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.primary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
|
||||
HStack(spacing: 8) {
|
||||
Text(primaryValue)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(rankingType.color)
|
||||
|
||||
Text(secondaryValue)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// PID
|
||||
Text("PID: \(process.pid)")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
.opacity(0.7)
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 6)
|
||||
.fill(isHovered ? Color(NSColor.selectedControlColor).opacity(0.2) : Color.clear)
|
||||
)
|
||||
.onHover { hovering in
|
||||
withAnimation(.easeInOut(duration: 0.15)) {
|
||||
isHovered = hovering
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
RankedProcessPopover(rankingType: .cpu)
|
||||
.background(Color.black)
|
||||
}
|
||||
48
DynamicIsland/components/Stats/StatsFormatting.swift
Normal file
48
DynamicIsland/components/Stats/StatsFormatting.swift
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// StatsFormatting.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Lightweight formatters shared across stats detail popovers.
|
||||
//
|
||||
// Created by GitHub Copilot on 19/10/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum StatsFormatting {
|
||||
static func bytes(_ value: UInt64) -> String {
|
||||
let formatter = ByteCountFormatter()
|
||||
formatter.allowedUnits = [.useMB, .useGB]
|
||||
formatter.countStyle = .memory
|
||||
formatter.includesUnit = true
|
||||
return formatter.string(fromByteCount: Int64(value))
|
||||
}
|
||||
|
||||
static func mbPerSecond(_ value: Double) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.maximumFractionDigits = value >= 10 ? 1 : 2
|
||||
let number = NSNumber(value: value)
|
||||
let formatted = formatter.string(from: number) ?? String(format: "%.2f", value)
|
||||
return "\(formatted) MB/s"
|
||||
}
|
||||
|
||||
static func percentage(_ value: Double) -> String {
|
||||
String(format: "%.1f%%", value)
|
||||
}
|
||||
|
||||
static func gigabytes(_ value: Double) -> String {
|
||||
let formatter = NumberFormatter()
|
||||
formatter.numberStyle = .decimal
|
||||
formatter.maximumFractionDigits = value >= 10 ? 1 : 2
|
||||
return (formatter.string(from: NSNumber(value: value)) ?? String(format: "%.2f", value)) + " GB"
|
||||
}
|
||||
|
||||
static func abbreviatedDuration(_ value: TimeInterval) -> String {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.allowedUnits = [.day, .hour, .minute]
|
||||
formatter.maximumUnitCount = 2
|
||||
formatter.unitsStyle = .abbreviated
|
||||
return formatter.string(from: value) ?? "—"
|
||||
}
|
||||
}
|
||||
|
|
@ -67,65 +67,10 @@ struct StatsPanelView: View {
|
|||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
} else {
|
||||
// Stats content - Compact two-row layout
|
||||
VStack(spacing: 16) {
|
||||
// Top row: System overview cards
|
||||
HStack(spacing: 12) {
|
||||
CompactOverviewCard(
|
||||
title: "CPU",
|
||||
value: statsManager.cpuUsageString,
|
||||
color: .blue,
|
||||
icon: "cpu",
|
||||
data: statsManager.cpuHistory
|
||||
)
|
||||
|
||||
CompactOverviewCard(
|
||||
title: "Memory",
|
||||
value: statsManager.memoryUsageString,
|
||||
color: .green,
|
||||
icon: "memorychip",
|
||||
data: statsManager.memoryHistory
|
||||
)
|
||||
|
||||
CompactOverviewCard(
|
||||
title: "GPU",
|
||||
value: statsManager.gpuUsageString,
|
||||
color: .purple,
|
||||
icon: "display",
|
||||
data: statsManager.gpuHistory
|
||||
)
|
||||
}
|
||||
|
||||
// Bottom row: Network and Disk graphs
|
||||
HStack(spacing: 12) {
|
||||
CompactDualGraph(
|
||||
title: "Network",
|
||||
primaryLabel: "Down",
|
||||
primaryValue: "\(String(format: "%.1f", statsManager.networkDownload / 1024 / 1024)) MB/s",
|
||||
primaryData: statsManager.networkDownloadHistory,
|
||||
primaryColor: .green,
|
||||
secondaryLabel: "Up",
|
||||
secondaryValue: "\(String(format: "%.1f", statsManager.networkUpload / 1024 / 1024)) MB/s",
|
||||
secondaryData: statsManager.networkUploadHistory,
|
||||
secondaryColor: .orange
|
||||
)
|
||||
|
||||
CompactDualGraph(
|
||||
title: "Disk",
|
||||
primaryLabel: "Read",
|
||||
primaryValue: "\(String(format: "%.1f", statsManager.diskRead / 1024 / 1024)) MB/s",
|
||||
primaryData: statsManager.diskReadHistory,
|
||||
primaryColor: .cyan,
|
||||
secondaryLabel: "Write",
|
||||
secondaryValue: "\(String(format: "%.1f", statsManager.diskWrite / 1024 / 1024)) MB/s",
|
||||
secondaryData: statsManager.diskWriteHistory,
|
||||
secondaryColor: .pink
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(16)
|
||||
CPUStatsDetailView()
|
||||
}
|
||||
}
|
||||
.frame(width: 650, height: 300)
|
||||
.frame(width: 520, height: 620)
|
||||
.background(VisualEffectView(material: .hudWindow, blendingMode: .behindWindow))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: .black.opacity(0.4), radius: 20, x: 0, y: 10)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
39
DynamicIsland/models/CPUMetrics.swift
Normal file
39
DynamicIsland/models/CPUMetrics.swift
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// CPUMetrics.swift
|
||||
// DynamicIsland
|
||||
//
|
||||
// Lightweight CPU metric models derived from StatsManager sampling.
|
||||
//
|
||||
// Created by GitHub Copilot on 18/10/2025.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct CPULoadBreakdown: Equatable {
|
||||
var user: Double
|
||||
var system: Double
|
||||
var idle: Double
|
||||
|
||||
static let zero = CPULoadBreakdown(user: 0, system: 0, idle: 100)
|
||||
|
||||
var activeUsage: Double {
|
||||
let active = user + system
|
||||
return min(max(active, 0), 100)
|
||||
}
|
||||
|
||||
var normalizedSegments: (user: Double, system: Double, idle: Double) {
|
||||
let total = max(user + system + idle, 0.001)
|
||||
let userFraction = min(max(user / total, 0), 1)
|
||||
let systemFraction = min(max(system / total, 0), 1)
|
||||
let idleFraction = min(max(idle / total, 0), 1)
|
||||
return (userFraction, systemFraction, idleFraction)
|
||||
}
|
||||
}
|
||||
|
||||
struct LoadAverage: Equatable {
|
||||
var oneMinute: Double
|
||||
var fiveMinutes: Double
|
||||
var fifteenMinutes: Double
|
||||
|
||||
static let zero = LoadAverage(oneMinute: 0, fiveMinutes: 0, fifteenMinutes: 0)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue