Files
MacMonitor/Sources/UI/ContentView.swift
gronodandDevin <158243242+devin-ai-integration[bot]@users.noreply.github.com> a7f13542ae feat(alerts): implement threshold alert engine and Notification Center integration (fixes #24)
Introduce Sources/Intelligence with an @Observable AlertEngine that evaluates
five configurable rules (CPU temperature, fan stall, low memory, low storage,
low battery) against a Sendable AlertEvaluationContext built after each
telemetry decode pass. Breached conditions dispatch through a mockable
AlertNotificationDispatching protocol to UNUserNotificationCenter with
per-alert cooldowns, edge-triggered resolve detection, and a capped
in-memory history log.

Add a macOS Settings scene (AlertsSettingsView) for thresholds, cooldown,
permission status, and test notifications; a new Alerts and Notifications
sidebar tab with active-alert cards and the history log; header and menu bar
warning indicators; and an AppNavigationBus so notification actions
(Open MacMonitor, View Processes) steer the main window.

Also fix the memory snapshot decode to read memoryPressureStatus so
pressureLevel is populated correctly.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-08 13:53:48 +01:00

1282 lines
55 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
public struct ContentView: View {
@State private var store = SystemTelemetryStore.shared
@State private var selectedTab: String = "Dashboard"
@State private var processSearchText: String = ""
@State private var socketSearchText: String = ""
@State private var selectedPID: Int32? = nil
@State private var showingProcessDetail: Bool = false
@State private var navBus = AppNavigationBus.shared
public init() {}
public var body: some View {
NavigationSplitView {
sidebarContent
} detail: {
detailContent
}
.frame(minWidth: 1000, minHeight: 700)
.onAppear {
store.start()
}
.sheet(isPresented: $showingProcessDetail) {
if let pid = selectedPID {
ProcessDetailSheet(inspector: store.processInspector, pid: pid)
}
}
.onChange(of: navBus.requestCounter) { _, _ in
NSApp.activate(ignoringOtherApps: true)
if let window = NSApp.windows.first(where: { $0.canBecomeMain }) {
window.makeKeyAndOrderFront(nil)
}
if let tab = navBus.requestedTab {
selectedTab = tab
}
}
}
// MARK: - Sidebar
private var sidebarContent: some View {
List(selection: $selectedTab) {
Section("Overview") {
Label("Dashboard", systemImage: "gauge.with.needle")
.tag("Dashboard")
Label("Hardware Architecture", systemImage: "cpu")
.tag("Architecture")
}
Section("Core Telemetry") {
Label("CPU Load & Cores", systemImage: "chart.bar.xaxis")
.tag("CPU")
Label("Kernel & System Load", systemImage: "waveform.path.ecg")
.tag("Kernel")
Label("Thermal Matrix", systemImage: "flame")
.tag("Thermals")
Label("Fans & Cooling", systemImage: "fanblades")
.tag("Fans")
Label("Memory & Swap", systemImage: "memorychip")
.tag("Memory")
}
Section("I/O & Storage") {
Label("Storage & Disk I/O", systemImage: "internaldrive")
.tag("Storage")
Label("Network Traffic", systemImage: "network")
.tag("Network")
Label("Active Sockets", systemImage: "point.3.filled.connected.trianglepath.dotted")
.tag("Sockets")
}
Section("Hardware & Devices") {
Label("Displays & Brightness", systemImage: "sun.max")
.tag("Displays")
Label("Process Explorer", systemImage: "list.bullet.rectangle")
.tag("Processes")
Label("Graphics (GPU)", systemImage: "display")
.tag("GPU")
Label("Power & Battery", systemImage: "bolt.fill")
.tag("Power")
Label("Peripherals", systemImage: "cable.connector")
.tag("Peripherals")
Label("Audio Devices", systemImage: "speaker.wave.3.fill")
.tag("Audio")
Label("Historical Trends", systemImage: "chart.line.uptrend.xyaxis")
.tag("Trends")
}
Section("Intelligence") {
Label("Alerts & Notifications", systemImage: "bell.badge")
.tag("Alerts")
.badge(store.alertEngine.activeAlerts.isEmpty ? nil : Text("\(store.alertEngine.activeAlerts.count)"))
}
}
.listStyle(.sidebar)
.navigationSplitViewColumnWidth(min: 210, ideal: 240, max: 300)
}
// MARK: - Detail Content Router
@ViewBuilder
private var detailContent: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
headerCard
engineControlCard
switch selectedTab {
case "Dashboard":
dashboardView
case "Architecture":
specsCard
providersCard
case "CPU":
cpuLoadCard
specsCard
case "Kernel":
kernelCountersCard
systemLoadCard
case "Thermals":
cpuThermalCard
componentThermalCard
case "Fans":
fansCard
case "Memory":
memoryCard
case "Storage":
diskIOCard
storageCard
case "Network":
networkBandwidthCard
case "Sockets":
networkSocketsCard
case "Processes":
processExplorerCard
case "GPU":
gpuTelemetryCard
case "Power":
batteryHealthCard
powerCard
case "Peripherals":
peripheralsCard
case "Displays":
displaysCard
case "Audio":
audioDevicesCard
case "Trends":
historicalTrendsView
case "Alerts":
AlertsView()
default:
dashboardView
}
}
.padding(24)
}
.navigationTitle("MacMonitor — \(selectedTab)")
}
// MARK: - Dashboard Composite View
private var dashboardView: some View {
VStack(alignment: .leading, spacing: 20) {
HStack(alignment: .top, spacing: 16) {
cpuLoadCard
systemLoadCard
}
HStack(alignment: .top, spacing: 16) {
memoryCard
kernelCountersCard
}
HStack(alignment: .top, spacing: 16) {
cpuThermalCard
fansCard
}
HStack(alignment: .top, spacing: 16) {
diskIOCard
networkBandwidthCard
}
HStack(alignment: .top, spacing: 16) {
gpuTelemetryCard
batteryHealthCard
}
displaysCard
}
}
// MARK: - Header Banner
private var headerCard: some View {
HStack(alignment: .center, spacing: 16) {
Image(systemName: "macpro.gen3.fill")
.resizable()
.scaledToFit()
.frame(width: 44, height: 44)
.foregroundStyle(.blue)
VStack(alignment: .leading, spacing: 4) {
Text(store.hostModel)
.font(.title2.bold())
Text("Intel x86_64 • \(store.osVersion)")
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
if !store.alertEngine.activeAlerts.isEmpty {
Button {
selectedTab = "Alerts"
} label: {
HStack(spacing: 6) {
Image(systemName: "exclamationmark.triangle.fill")
Text("\(store.alertEngine.activeAlerts.count)")
.font(.caption.bold().monospacedDigit())
}
.foregroundStyle(.red)
.padding(.horizontal, 10)
.padding(.vertical, 6)
.background(Color.red.opacity(0.15), in: Capsule())
}
.buttonStyle(.plain)
.help("\(store.alertEngine.activeAlerts.count) active alert(s) — click to review")
}
HStack(spacing: 8) {
Circle()
.fill(store.isRunning ? Color.green : Color.red)
.frame(width: 10, height: 10)
Text(store.isRunning ? "ACTIVE" : "PAUSED")
.font(.caption.bold())
.foregroundStyle(store.isRunning ? .green : .secondary)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(.quaternary, in: Capsule())
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Telemetry Coordinator Controls
private var engineControlCard: some View {
HStack(spacing: 16) {
Button(action: {
if store.isRunning { store.stop() } else { store.start() }
}) {
Label(store.isRunning ? "Pause Engine" : "Start Engine",
systemImage: store.isRunning ? "pause.fill" : "play.fill")
}
.buttonStyle(.borderedProminent)
.tint(store.isRunning ? .orange : .green)
Button(action: { store.triggerManualSample() }) {
Label("Sample Now", systemImage: "arrow.clockwise")
}
.buttonStyle(.bordered)
Spacer()
Picker("Interval:", selection: Binding(
get: { store.sampleInterval },
set: { store.setSampleInterval($0) }
)) {
Text("0.5s").tag(0.5)
Text("1.0s").tag(1.0)
Text("2.0s").tag(2.0)
Text("5.0s").tag(5.0)
}
.pickerStyle(.segmented)
.frame(width: 220)
Text("Updated: \(store.lastUpdateTimestamp.formatted(date: .omitted, time: .standard))")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(14)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - CPU Load Card
private var cpuLoadCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("CPU Activity", systemImage: "cpu")
.font(.headline)
Spacer()
if store.cpuLoad.isThrottled {
Text("THROTTLED")
.font(.caption.bold())
.foregroundStyle(.red)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.red.opacity(0.15), in: Capsule())
}
Text(String(format: "%.1f%%", store.cpuLoad.totalLoad))
.font(.title3.bold().monospacedDigit())
}
ProgressView(value: min(store.cpuLoad.totalLoad / 100.0, 1.0))
.tint(store.cpuLoad.totalLoad > 85 ? .red : (store.cpuLoad.totalLoad > 60 ? .orange : .blue))
HStack {
Text("User: \(String(format: "%.1f%%", store.cpuLoad.userLoad))")
Spacer()
Text("System: \(String(format: "%.1f%%", store.cpuLoad.systemLoad))")
Spacer()
Text("Idle: \(String(format: "%.1f%%", store.cpuLoad.idleLoad))")
}
.font(.caption)
.foregroundStyle(.secondary)
if !store.cpuLoad.perCoreLoad.isEmpty {
Divider()
Text("Per-Core Utilization (\(store.cpuLoad.perCoreLoad.count) Cores)")
.font(.caption.bold())
.foregroundStyle(.secondary)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 70))], spacing: 8) {
ForEach(Array(store.cpuLoad.perCoreLoad.enumerated()), id: \.offset) { idx, load in
VStack(spacing: 4) {
Text("Core \(idx)")
.font(.system(size: 10))
.foregroundStyle(.secondary)
ProgressView(value: min(load / 100.0, 1.0))
.tint(load > 85 ? .red : .blue)
Text(String(format: "%.0f%%", load))
.font(.system(size: 10, weight: .bold, design: .monospaced))
}
}
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Kernel Counters Card
private var kernelCountersCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Kernel & VM Telemetry", systemImage: "waveform.path.ecg")
.font(.headline)
Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 8) {
GridRow {
Text("Context Switches:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.contextSwitchesPerSec))/s").bold().monospacedDigit()
Text("Syscalls:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.syscallsPerSec))/s").bold().monospacedDigit()
}
GridRow {
Text("Page Faults:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.pageFaultsPerSec))/s").bold().monospacedDigit()
Text("COW Faults:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.cowFaultsPerSec))/s").bold().monospacedDigit()
}
GridRow {
Text("Pageins / Pageouts:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.pageinsPerSec)) / \(Int(store.kernelCounters.pageoutsPerSec)) /s").bold().monospacedDigit()
Text("Zero-Fill:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.zeroFillFaultsPerSec))/s").bold().monospacedDigit()
}
}
.font(.caption)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - System Load & Mach Factor Card
private var systemLoadCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("System Load & Mach Factor", systemImage: "gauge.high")
.font(.headline)
HStack(spacing: 24) {
VStack(alignment: .leading, spacing: 2) {
Text("1m Load").font(.caption).foregroundStyle(.secondary)
Text(String(format: "%.2f", store.systemLoad.load1Min)).font(.title3.bold().monospacedDigit())
}
VStack(alignment: .leading, spacing: 2) {
Text("5m Load").font(.caption).foregroundStyle(.secondary)
Text(String(format: "%.2f", store.systemLoad.load5Min)).font(.title3.bold().monospacedDigit())
}
VStack(alignment: .leading, spacing: 2) {
Text("15m Load").font(.caption).foregroundStyle(.secondary)
Text(String(format: "%.2f", store.systemLoad.load15Min)).font(.title3.bold().monospacedDigit())
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text("Mach Factor").font(.caption).foregroundStyle(.secondary)
Text(String(format: "%.2f", store.systemLoad.machFactor)).font(.title3.bold().monospacedDigit()).foregroundStyle(.blue)
}
}
Divider()
HStack {
Text("Active Mach Tasks: \(store.systemLoad.taskCount)").font(.caption).foregroundStyle(.secondary)
Spacer()
Text("Active Mach Threads: \(store.systemLoad.threadCount)").font(.caption).foregroundStyle(.secondary)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Memory & Swap Card
private var memoryCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Memory & Swap", systemImage: "memorychip")
.font(.headline)
Spacer()
Text(store.memory.pressureLevel)
.font(.caption.bold())
.foregroundStyle(pressureColor(store.memory.pressureLevel))
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(pressureColor(store.memory.pressureLevel).opacity(0.15), in: Capsule())
Text(String(format: "%.1f%%", store.memory.utilizationPercentage))
.font(.title3.bold().monospacedDigit())
}
ProgressView(value: min(store.memory.utilizationPercentage / 100.0, 1.0))
.tint(store.memory.utilizationPercentage > 85 ? .red : .blue)
Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 6) {
GridRow {
Text("Used:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.usedBytes)).bold()
Text("Free:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.freeBytes)).bold()
}
GridRow {
Text("Wired:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.wiredBytes))
Text("Compressed:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.compressedBytes))
}
GridRow {
Text("Swap Used:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.swapUsedBytes))
Text("Swap Total:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.swapTotalBytes))
}
}
.font(.caption)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Disk I/O Card
private var diskIOCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Disk I/O & IOPS", systemImage: "externaldrive.badge.timemachine")
.font(.headline)
if store.diskIO.isEmpty {
Text("Sampling disk I/O metrics...").font(.caption).foregroundStyle(.secondary)
} else {
ForEach(store.diskIO) { disk in
HStack {
VStack(alignment: .leading, spacing: 2) {
Text(disk.bsdName).bold()
Text("IOPS: \(Int(disk.readIOPS + disk.writeIOPS)) (R: \(Int(disk.readIOPS)), W: \(Int(disk.writeIOPS)))")
.font(.caption2).foregroundStyle(.secondary)
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text("R: \(formatBps(disk.readBps))").font(.caption.bold().monospacedDigit()).foregroundStyle(.green)
Text("W: \(formatBps(disk.writeBps))").font(.caption.bold().monospacedDigit()).foregroundStyle(.blue)
}
}
.padding(.vertical, 2)
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Storage Volumes Card
private var storageCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Mounted Storage Volumes", systemImage: "internaldrive")
.font(.headline)
ForEach(store.storageVolumes) { vol in
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(vol.volumeName).bold()
Text("(\(vol.mountPoint)) • \(vol.fileSystem)").font(.caption).foregroundStyle(.secondary)
Spacer()
Text(String(format: "%.1f%%", vol.usedPercentage)).font(.caption.bold().monospacedDigit())
}
ProgressView(value: min(vol.usedPercentage / 100.0, 1.0))
.tint(vol.usedPercentage > 90 ? .red : .blue)
HStack {
Text("Used: \(formatBytes(vol.usedBytes))").font(.caption2).foregroundStyle(.secondary)
Spacer()
Text("Free: \(formatBytes(vol.freeBytes))").font(.caption2).foregroundStyle(.secondary)
}
}
.padding(.vertical, 4)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Network Bandwidth Card
private var networkBandwidthCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Network Bandwidth", systemImage: "network")
.font(.headline)
if store.networkBandwidth.isEmpty {
Text("No network interfaces detected").font(.caption).foregroundStyle(.secondary)
} else {
ForEach(store.networkBandwidth) { iface in
HStack {
VStack(alignment: .leading, spacing: 2) {
HStack {
Circle().fill(iface.isUp ? Color.green : Color.gray).frame(width: 8, height: 8)
Text(iface.interfaceName).bold()
}
if !iface.ipAddress.isEmpty {
Text(iface.ipAddress).font(.caption2).foregroundStyle(.secondary)
}
}
Spacer()
VStack(alignment: .trailing, spacing: 2) {
Text("↓ \(formatBps(iface.downloadBps)) (\(Int(iface.downloadPps)) pps)")
.font(.caption.bold().monospacedDigit()).foregroundStyle(.green)
Text("↑ \(formatBps(iface.uploadBps)) (\(Int(iface.uploadPps)) pps)")
.font(.caption.bold().monospacedDigit()).foregroundStyle(.blue)
}
}
.padding(.vertical, 4)
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Network Sockets Card
private var networkSocketsCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Active Network Sockets (\(store.networkSockets.count))", systemImage: "point.3.filled.connected.trianglepath.dotted")
.font(.headline)
Spacer()
TextField("Filter socket/PID/process...", text: $socketSearchText)
.textFieldStyle(.roundedBorder)
.frame(width: 200)
}
let filtered = store.networkSockets.filter {
socketSearchText.isEmpty ||
$0.processName.localizedCaseInsensitiveContains(socketSearchText) ||
$0.localAddress.localizedCaseInsensitiveContains(socketSearchText) ||
$0.remoteAddress.localizedCaseInsensitiveContains(socketSearchText) ||
"\($0.pid)".contains(socketSearchText)
}
Table(filtered.prefix(150)) {
TableColumn("Process") { s in
Text("\(s.processName) (\(s.pid))").bold()
}
TableColumn("Proto") { s in
Text(s.protocolName)
}
TableColumn("Local Endpoint") { s in
Text("\(s.localAddress):\(s.localPort)").monospaced()
}
TableColumn("Remote Endpoint") { s in
Text("\(s.remoteAddress):\(s.remotePort)").monospaced()
}
TableColumn("State") { s in
Text(s.tcpState).font(.caption).foregroundStyle(s.tcpState == "ESTABLISHED" ? .green : .secondary)
}
}
.frame(minHeight: 350)
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Process Explorer Card
private var processExplorerCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Process Explorer (\(store.processes.count))", systemImage: "list.bullet.rectangle")
.font(.headline)
Spacer()
TextField("Search name or PID...", text: $processSearchText)
.textFieldStyle(.roundedBorder)
.frame(width: 220)
}
let filtered = store.processes.filter {
processSearchText.isEmpty ||
$0.name.localizedCaseInsensitiveContains(processSearchText) ||
"\($0.pid)".contains(processSearchText) ||
$0.username.localizedCaseInsensitiveContains(processSearchText)
}
Table(filtered.prefix(200)) {
TableColumn("PID") { p in
Text("\(p.pid)").monospacedDigit()
}
TableColumn("Process Name") { p in
HStack {
Text(p.name).bold()
if p.isStopped {
Text("STOP").font(.caption2).foregroundStyle(.red)
}
}
}
TableColumn("% CPU") { p in
Text(String(format: "%.1f%%", p.cpuPercent))
.monospacedDigit()
.foregroundStyle(p.cpuPercent > 50 ? .red : .primary)
}
TableColumn("RSS Memory") { p in
Text(formatBytes(p.residentSize)).monospacedDigit()
}
TableColumn("Threads") { p in
Text("\(p.threadCount)").monospacedDigit()
}
TableColumn("User") { p in
Text(p.username)
}
TableColumn("Actions") { p in
HStack(spacing: 8) {
Button("Inspect") {
selectedPID = p.pid
showingProcessDetail = true
}
.buttonStyle(.borderless)
.foregroundStyle(.blue)
Button("Kill") {
_ = store.terminateProcess(pid: p.pid, force: false)
}
.buttonStyle(.borderless)
.foregroundStyle(.red)
}
}
}
.frame(minHeight: 400)
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - GPU Card
private var gpuTelemetryCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Graphics Accelerators (GPU)", systemImage: "display")
.font(.headline)
if store.gpus.isEmpty {
Text("No GPU accelerators detected").font(.caption).foregroundStyle(.secondary)
} else {
ForEach(store.gpus) { gpu in
VStack(alignment: .leading, spacing: 6) {
HStack {
Text(gpu.name).bold()
Spacer()
if gpu.temperature > 0 {
Text(String(format: "%.1f°C", gpu.temperature))
.font(.caption.bold())
.foregroundStyle(gpu.temperature > 85 ? .red : .primary)
}
Text(String(format: "%.1f%%", gpu.utilizationPercent))
.font(.title3.bold().monospacedDigit())
}
ProgressView(value: min(gpu.utilizationPercent / 100.0, 1.0))
.tint(gpu.utilizationPercent > 80 ? .red : .blue)
HStack {
Text(gpu.isDiscrete ? "Discrete GPU" : "Integrated GPU").font(.caption2).foregroundStyle(.secondary)
Spacer()
if gpu.vramTotalBytes > 0 {
Text("VRAM: \(formatBytes(gpu.vramUsedBytes)) / \(formatBytes(gpu.vramTotalBytes))").font(.caption2).foregroundStyle(.secondary)
}
}
}
.padding(.vertical, 4)
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Battery Health Card
private var batteryHealthCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Battery & Health", systemImage: "battery.100.bolt")
.font(.headline)
Spacer()
if store.batteryHealth.hasBattery {
Text(store.batteryHealth.healthCondition)
.font(.caption.bold())
.foregroundStyle(store.batteryHealth.healthCondition == "Normal" ? .green : .orange)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.green.opacity(0.15), in: Capsule())
Text(String(format: "%.1f%%", store.batteryHealth.healthPercent))
.font(.title3.bold().monospacedDigit())
}
}
if !store.batteryHealth.hasBattery {
Text("Desktop Mac / No battery installed").font(.caption).foregroundStyle(.secondary)
} else {
Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 6) {
GridRow {
Text("Cycle Count:").foregroundStyle(.secondary)
Text("\(store.batteryHealth.cycleCount) / \(store.batteryHealth.designCycleCount)").bold()
Text("Temperature:").foregroundStyle(.secondary)
Text(String(format: "%.1f°C", store.batteryHealth.temperature)).bold()
}
GridRow {
Text("Voltage / Power:").foregroundStyle(.secondary)
Text("\(String(format: "%.2f V", store.batteryHealth.voltage)) (\(String(format: "%.1f W", store.batteryHealth.watts)))")
Text("Power Adapter:").foregroundStyle(.secondary)
Text(store.batteryHealth.externalConnected ? "\(store.batteryHealth.adapterWatts)W Connected" : "On Battery")
}
GridRow {
Text("Capacity:").foregroundStyle(.secondary)
Text("\(store.batteryHealth.currentCapacity) / \(store.batteryHealth.maxCapacity) mAh")
Text("Manufacturer:").foregroundStyle(.secondary)
Text(store.batteryHealth.manufacturer)
}
}
.font(.caption)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Peripherals Card
private var peripheralsCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Connected Peripherals (\(store.peripherals.count))", systemImage: "cable.connector")
.font(.headline)
Table(store.peripherals) {
TableColumn("Device Name") { d in
Text(d.name).bold()
}
TableColumn("Bus") { d in
Text(d.busType)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.secondary.opacity(0.15), in: Capsule())
}
TableColumn("Vendor ID") { d in
Text(String(format: "0x%04x", d.vendorID)).monospaced()
}
TableColumn("Product ID") { d in
Text(String(format: "0x%04x", d.productID)).monospaced()
}
TableColumn("Built-In") { d in
Text(d.isBuiltIn ? "Yes" : "External").foregroundStyle(d.isBuiltIn ? Color.secondary : Color.blue)
}
}
.frame(minHeight: 350)
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Audio Devices Card
private var audioDevicesCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Audio Devices (\(store.audioDevices.count))", systemImage: "speaker.wave.3.fill")
.font(.headline)
ForEach(store.audioDevices) { audio in
HStack {
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(audio.name).bold()
if audio.isDefaultOutput {
Text("DEFAULT OUT").font(.caption2.bold()).foregroundStyle(.green)
}
if audio.isDefaultInput {
Text("DEFAULT IN").font(.caption2.bold()).foregroundStyle(.blue)
}
}
Text("\(audio.manufacturer)\(Int(audio.sampleRate)) Hz • \(audio.channelCount) Channels")
.font(.caption2).foregroundStyle(.secondary)
}
Spacer()
if audio.isOutput {
Text("Vol: \(Int(audio.volume * 100))%")
.font(.caption.bold().monospacedDigit())
}
}
.padding(.vertical, 4)
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - CPU Thermal Card
private var cpuThermalCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("CPU Temperature", systemImage: "thermometer.medium")
.font(.headline)
Spacer()
Text(store.cpuThermal.thermalPressureState)
.font(.caption.bold())
.foregroundStyle(store.cpuThermal.thermalPressureState == "Nominal" ? .green : .red)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.green.opacity(0.15), in: Capsule())
Text(String(format: "%.1f°C", store.cpuThermal.packageTemperature))
.font(.title3.bold().monospacedDigit())
}
ProgressView(value: min(store.cpuThermal.packageTemperature / 105.0, 1.0))
.tint(store.cpuThermal.packageTemperature > 85 ? .red : (store.cpuThermal.packageTemperature > 70 ? .orange : .blue))
HStack {
Text("Avg Core: \(String(format: "%.1f°C", store.cpuThermal.averageCoreTemperature))")
Spacer()
Text("Peak Core: \(String(format: "%.1f°C", store.cpuThermal.peakCoreTemperature))")
Spacer()
Text("Throttling: \(store.cpuThermal.isThrottling ? "YES" : "No")")
.foregroundStyle(store.cpuThermal.isThrottling ? .red : .secondary)
}
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Fans Card
private var fansCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Cooling Fans", systemImage: "fanblades")
.font(.headline)
if store.fans.isEmpty {
Text("No SMC fan sensors detected").font(.caption).foregroundStyle(.secondary)
} else {
ForEach(store.fans) { fan in
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(fan.name).bold()
Spacer()
Text("\(Int(fan.currentRPM)) RPM").font(.title3.bold().monospacedDigit())
}
ProgressView(value: min(fan.utilization / 100.0, 1.0))
.tint(fan.utilization > 80 ? .orange : .blue)
HStack {
Text("Min: \(Int(fan.minRPM)) RPM").font(.caption2).foregroundStyle(.secondary)
Spacer()
Text("Max: \(Int(fan.maxRPM)) RPM").font(.caption2).foregroundStyle(.secondary)
}
}
.padding(.vertical, 4)
}
}
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Component Thermal Matrix Card
private var componentThermalCard: some View {
VStack(alignment: .leading, spacing: 12) {
Label("Component Thermal Sensors (\(store.componentTemps.count) Probed)", systemImage: "flame")
.font(.headline)
LazyVGrid(columns: [GridItem(.adaptive(minimum: 160))], spacing: 10) {
ForEach(store.componentTemps) { item in
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(item.name).font(.caption).bold().lineLimit(1)
Spacer()
Text(String(format: "%.1f°C", item.temperature))
.font(.caption.bold().monospacedDigit())
.foregroundStyle(item.temperature > 80 ? .red : (item.temperature > 65 ? .orange : .primary))
}
ProgressView(value: min(item.temperature / 100.0, 1.0))
.tint(item.temperature > 80 ? .red : .blue)
Text("\(item.category)\(item.key)").font(.system(size: 9)).foregroundStyle(.secondary)
}
.padding(8)
.background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8))
}
}
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Power & Voltage Card
private var powerCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Power Draw & Rails", systemImage: "bolt.fill")
.font(.headline)
Spacer()
Text(String(format: "%.1f W", store.power.systemTotalWatts))
.font(.title3.bold().monospacedDigit())
}
Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 6) {
GridRow {
Text("CPU Power:").foregroundStyle(.secondary)
Text(String(format: "%.1f W", store.power.cpuWatts)).bold()
Text("GPU Power:").foregroundStyle(.secondary)
Text(String(format: "%.1f W", store.power.gpuWatts)).bold()
}
GridRow {
Text("CPU Voltage:").foregroundStyle(.secondary)
Text(String(format: "%.2f V", store.power.cpuVoltage))
Text("CPU Current:").foregroundStyle(.secondary)
Text(String(format: "%.2f A", store.power.cpuCurrent))
}
}
.font(.caption)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Hardware Specs Card
private var specsCard: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Host Architecture & Kernel")
.font(.headline)
Grid(alignment: .leading, horizontalSpacing: 24, verticalSpacing: 10) {
GridRow {
Text("Hardware Model:").foregroundStyle(.secondary)
Text(store.hostModel).bold()
Text("Physical Cores:").foregroundStyle(.secondary)
Text("\(store.physicalCpuCount)").bold()
}
GridRow {
Text("Darwin Kernel:").foregroundStyle(.secondary)
Text(store.kernelVersion).bold()
Text("Logical Cores:").foregroundStyle(.secondary)
Text("\(store.logicalCpuCount)").bold()
}
GridRow {
Text("Target Architecture:").foregroundStyle(.secondary)
Text("Intel x86_64").bold()
Text("Deployment Target:").foregroundStyle(.secondary)
Text("macOS 14.0 (Sonoma)").bold()
}
}
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Providers Card
private var providersCard: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Registered Telemetry Providers")
.font(.headline)
Spacer()
Text("\(store.registeredProviderCount) Active")
.font(.caption.bold())
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Color.blue.opacity(0.15))
.foregroundStyle(.blue)
.clipShape(Capsule())
}
ForEach(Array(store.latestSnapshot.keys.sorted()), id: \.self) { key in
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
Text(key)
.font(.system(.body, design: .monospaced))
Spacer()
Text("\(store.latestSnapshot[key]?.count ?? 0) metrics")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 4)
}
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Displays Card
private var displaysCard: some View {
VStack(alignment: .leading, spacing: 14) {
HStack {
Label("Connected Displays & Brightness", systemImage: "sun.max")
.font(.headline)
Spacer()
Text("\(store.displays.count) Display\(store.displays.count == 1 ? "" : "s")")
.font(.caption.bold())
.foregroundStyle(.secondary)
}
if store.displays.isEmpty {
Text("No active displays detected.")
.font(.caption)
.foregroundStyle(.secondary)
} else {
ForEach(store.displays, id: \.displayID) { display in
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: display.isBuiltin ? "laptopcomputer" : "display")
.foregroundStyle(.blue)
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(display.name)
.font(.subheadline.bold())
if display.isMain {
Text("MAIN")
.font(.caption2.bold())
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.blue.opacity(0.15), in: Capsule())
}
}
Text("\(display.width) × \(display.height) @ \(Int(display.refreshRate))Hz")
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
if display.brightness >= 0 {
Text("\(Int(display.brightness * 100))%")
.font(.subheadline.bold().monospacedDigit())
}
}
if display.brightness >= 0 {
Slider(
value: Binding(
get: { Double(display.brightness) },
set: { newBrightness in
_ = store.displayManager.setBrightness(Float(newBrightness), forDisplay: display.displayID)
store.triggerManualSample()
}
),
in: 0.0...1.0
)
} else {
Text("Hardware brightness control not supported for this monitor.")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
.padding(12)
.background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 8))
}
}
}
.padding(16)
.background(.background, in: RoundedRectangle(cornerRadius: 12))
.overlay(RoundedRectangle(cornerRadius: 12).stroke(.separator, lineWidth: 1))
}
// MARK: - Historical Trends View
private var historicalTrendsView: some View {
VStack(alignment: .leading, spacing: 16) {
TelemetryTrendChartView(
title: "CPU Utilization History",
unit: "%",
color: .blue,
samples: store.historyStore.cpuHistory.samples,
maxY: 100.0
)
TelemetryTrendChartView(
title: "Memory Utilization History",
unit: "%",
color: .purple,
samples: store.historyStore.memoryHistory.samples,
maxY: 100.0
)
TelemetryTrendChartView(
title: "CPU Temperature History",
unit: "°C",
color: .orange,
samples: store.historyStore.cpuTempHistory.samples,
maxY: 110.0
)
TelemetryTrendChartView(
title: "System Power Draw History",
unit: "W",
color: .yellow,
samples: store.historyStore.powerHistory.samples
)
HStack(spacing: 16) {
TelemetryTrendChartView(
title: "Network Download Rate",
unit: "B/s",
color: .green,
samples: store.historyStore.networkInHistory.samples
)
TelemetryTrendChartView(
title: "Network Upload Rate",
unit: "B/s",
color: .teal,
samples: store.historyStore.networkOutHistory.samples
)
}
HStack(spacing: 16) {
TelemetryTrendChartView(
title: "Disk Read Throughput",
unit: "B/s",
color: .indigo,
samples: store.historyStore.diskReadHistory.samples
)
TelemetryTrendChartView(
title: "Disk Write Throughput",
unit: "B/s",
color: .pink,
samples: store.historyStore.diskWriteHistory.samples
)
}
}
}
// MARK: - Formatting Helpers
private func formatBytes(_ bytes: UInt64) -> String {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useGB, .useMB, .useKB]
formatter.countStyle = .memory
return formatter.string(fromByteCount: Int64(bytes))
}
private func formatBps(_ bps: Double) -> String {
if bps >= 1_000_000_000 {
return String(format: "%.2f GB/s", bps / 1_000_000_000.0)
} else if bps >= 1_000_000 {
return String(format: "%.1f MB/s", bps / 1_000_000.0)
} else if bps >= 1_000 {
return String(format: "%.1f KB/s", bps / 1_000.0)
} else {
return String(format: "%.0f B/s", bps)
}
}
private func pressureColor(_ level: String) -> Color {
switch level {
case "Critical": return .red
case "Warning": return .orange
default: return .green
}
}
}
// MARK: - Process Detail Inspector Sheet
struct ProcessDetailSheet: View {
let inspector: MMProcessDetailInspector
let pid: Int32
@Environment(\.dismiss) private var dismiss
@State private var threads: [[String: Any]] = []
@State private var files: [String] = []
@State private var sockets: [[String: Any]] = []
@State private var selectedDetailTab: String = "Threads"
var body: some View {
VStack(alignment: .leading, spacing: 16) {
HStack {
Text("Process Inspector (PID \(pid))")
.font(.title2.bold())
Spacer()
Button("Done") { dismiss() }
}
Picker("", selection: $selectedDetailTab) {
Text("Threads (\(threads.count))").tag("Threads")
Text("Open Files (\(files.count))").tag("Files")
Text("Open Sockets (\(sockets.count))").tag("Sockets")
}
.pickerStyle(.segmented)
if selectedDetailTab == "Threads" {
List(threads.indices, id: \.self) { idx in
let t = threads[idx]
HStack {
Text("Thread \(t["threadID"] ?? idx)").bold()
Spacer()
Text("CPU: \(String(format: "%.1f%%", (t["cpuPercent"] as? Double) ?? 0.0))").monospacedDigit()
Text(t["state"] as? String ?? "").foregroundStyle(.secondary)
}
}
} else if selectedDetailTab == "Files" {
List(files, id: \.self) { path in
Text(path).font(.system(.caption, design: .monospaced))
}
} else {
List(sockets.indices, id: \.self) { idx in
let s = sockets[idx]
HStack {
Text(s["protocol"] as? String ?? "TCP").bold()
Spacer()
Text("\(s["localAddress"] ?? ""):\(s["localPort"] ?? 0)\(s["remoteAddress"] ?? ""):\(s["remotePort"] ?? 0)")
.monospaced()
Text(s["tcpState"] as? String ?? "").foregroundStyle(.secondary)
}
}
}
}
.padding(24)
.frame(minWidth: 600, minHeight: 450)
.onAppear {
loadDetails()
}
}
private func loadDetails() {
DispatchQueue.global(qos: .userInitiated).async {
let res = MMProcessDetailInspector.inspectProcess(withPID: pid) ?? [:]
let t = (res["threads"] as? [[String: Any]]) ?? []
let fds = (res["fileDescriptors"] as? [[String: Any]]) ?? []
let f = fds.compactMap { $0["path"] as? String }
let s = (res["sockets"] as? [[String: Any]]) ?? []
DispatchQueue.main.async {
self.threads = t
self.files = f
self.sockets = s
}
}
}
}