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>
1282 lines
55 KiB
Swift
1282 lines
55 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
|
||
@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
|
||
}
|
||
}
|
||
}
|
||
}
|