Working stuffgit add .

This commit is contained in:
Hariharan Mudaliar 2025-10-19 02:20:26 +05:30
commit 630431a008
15 changed files with 2577 additions and 308 deletions

View file

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

View file

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

View 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))
}
}
}

View 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)
}
}
}
}
}

View 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)
}
}
}
}

View 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)")
}
}
}
}
}

View 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)
}
}
}
}

View 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)
}
}

View file

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

View file

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

View 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) ?? "—"
}
}

View file

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

View 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)
}