Files
MacMonitor/Sources/UI/ContentView.swift
gronod c8743e2578
MacMonitor CI/CD Pipeline / Build & Test (Intel x86_64) (push) Canceled after 0s
feat(ui): integrate Milestone 3 telemetry views, process inspector, and reactive cards
2026-09-08 12:54:34 +01:00

1097 lines
47 KiB
Swift

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
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)
}
}
}
// 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("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")
}
}
.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 "Audio":
audioDevicesCard
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
}
}
}
// 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()
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: - 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
}
}
}
}