import AppKit import Charts import SwiftUI struct HardwareView: View { @Environment(SystemMonitor.self) private var monitor @AppStorage("showEmptyHardwarePorts") private var showEmptyPorts = true @State private var fanTargets: [Int: Double] = [:] @State private var fanCommandInProgress = false @State private var fanError: String? @State private var fanStatus: String? @State private var showingFanConfirmation = false private var battery: BatterySnapshot { monitor.hardware.battery } private var visiblePorts: [HardwarePortSnapshot] { showEmptyPorts ? monitor.hardware.ports : monitor.hardware.ports.filter(\.isConnected) } var body: some View { VStack(spacing: 0) { PageHeader(title: "Hardware", subtitle: "Power, battery, thermals, ports, and connection diagnostics") { HStack(spacing: 10) { Button("Export", systemImage: "square.and.arrow.up") { SystemReportExporter.export(monitor: monitor, selectedResource: "Hardware") } Button("Power Settings", systemImage: "gear") { openPowerSettings() } } } ScrollView { LazyVStack(alignment: .leading, spacing: 18) { summaryGrid verdictCard if battery.isPresent { batterySection } if let adapter = battery.adapter { adapterSection(adapter) } powerSection portsSection coolingSection topConsumersSection } .padding(20) } } .onChange(of: monitor.hardware.fans) { _, fans in seedFanTargets(fans) } .onAppear { seedFanTargets(monitor.hardware.fans) } .confirmationDialog("Apply manual fan targets?", isPresented: $showingFanConfirmation) { Button("Apply Targets") { updateFans(manual: true) } Button("Cancel", role: .cancel) { } } message: { Text("\(fanTargetSummary). macOS automatic fan control will be disabled until you choose Automatic.") } .alert("Fan control failed", isPresented: Binding( get: { fanError != nil }, set: { if !$0 { fanError = nil } } )) { Button("OK", role: .cancel) { fanError = nil } } message: { Text(fanError ?? "") } } private var summaryGrid: some View { LazyVGrid(columns: [GridItem(.adaptive(minimum: 180), spacing: 10)], spacing: 10) { summaryCard("CPU", value: Formatters.percent(monitor.snapshot.cpuPercent), detail: "\(monitor.snapshot.corePercents.count) logical processors", icon: "cpu", color: .blue) summaryCard("Memory", value: Formatters.percent(monitor.snapshot.memoryPercent), detail: Formatters.bytes.string(fromByteCount: Int64(monitor.snapshot.memoryUsed)), icon: "memorychip", color: .purple) summaryCard("System power", value: watts(battery.systemPowerWatts), detail: battery.externalPowerConnected ? "External power" : "Battery power", icon: "bolt.fill", color: .orange) summaryCard("Thermals", value: monitor.hardware.thermalState, detail: thermalDetail, icon: "thermometer.medium", color: thermalColor) } } private func summaryCard(_ title: String, value: String, detail: String, icon: String, color: Color) -> some View { VStack(alignment: .leading, spacing: 8) { Label(title, systemImage: icon).font(.caption.weight(.semibold)).foregroundStyle(color) Text(value).font(.title2.weight(.semibold)).monospacedDigit().lineLimit(1).minimumScaleFactor(0.75) Text(detail).font(.caption).foregroundStyle(.secondary).lineLimit(1) } .frame(maxWidth: .infinity, minHeight: 86, alignment: .leading) .padding(13) .background(color.opacity(0.08), in: RoundedRectangle(cornerRadius: 10)) .overlay { RoundedRectangle(cornerRadius: 10).stroke(color.opacity(0.18)) } } private var verdictCard: some View { HStack(alignment: .top, spacing: 12) { Image(systemName: verdictIcon).font(.title2).foregroundStyle(verdictColor).frame(width: 28) VStack(alignment: .leading, spacing: 4) { Text(verdictTitle).font(.headline) Text(verdictDetail).font(.callout).foregroundStyle(.secondary) } Spacer() } .padding(15) .background(verdictColor.opacity(0.09), in: RoundedRectangle(cornerRadius: 10)) .overlay { RoundedRectangle(cornerRadius: 10).stroke(verdictColor.opacity(0.22)) } } private var batterySection: some View { VStack(alignment: .leading, spacing: 14) { Text("Battery").font(.headline) HStack(alignment: .firstTextBaseline) { metric("Charge", Formatters.percent(battery.chargePercent)) Spacer() metric("Condition", battery.healthPercent > 80 ? "Normal" : "Service recommended") Spacer() metric("Health", Formatters.percent(battery.healthPercent)) } HStack(alignment: .firstTextBaseline) { metric("Cycles", cycleText) Spacer() metric("Temperature", String(format: "%.1f °C", battery.temperatureCelsius)) Spacer() metric("Voltage", String(format: "%.2f V", battery.voltageVolts)) } HStack(alignment: .firstTextBaseline) { metric("Battery power", watts(battery.batteryPowerWatts)) Spacer() metric("Full capacity", capacity(battery.fullChargeCapacityMAh)) Spacer() metric("Design capacity", capacity(battery.designCapacityMAh)) Spacer() metric("Time remaining", timeRemaining) } Divider() HStack { Label(powerSourceText, systemImage: battery.externalPowerConnected ? "powerplug.fill" : "battery.75percent") Spacer() Text(batteryStateText).foregroundStyle(.secondary) } .font(.callout.weight(.medium)) } .frame(maxWidth: .infinity, alignment: .leading) .padding(16) .background(Color.secondary.opacity(0.06), in: RoundedRectangle(cornerRadius: 10)) .overlay { RoundedRectangle(cornerRadius: 10).stroke(Color.secondary.opacity(0.12)) } } private func adapterSection(_ adapter: PowerAdapterSnapshot) -> some View { VStack(alignment: .leading, spacing: 13) { Text("Charger and USB Power Delivery").font(.headline) HStack(alignment: .firstTextBaseline) { VStack(alignment: .leading, spacing: 3) { Text(adapter.name).font(.headline) Text(adapter.manufacturer).font(.caption).foregroundStyle(.secondary) } Spacer() Text("\(Int(adapter.ratedWatts.rounded())) W").font(.title2.weight(.semibold)).monospacedDigit() } Divider() HStack(spacing: 24) { metric("Negotiated voltage", String(format: "%.1f V", adapter.negotiatedVoltage)) metric("Current limit", String(format: "%.2f A", adapter.negotiatedCurrent)) metric("Negotiated ceiling", watts(adapter.negotiatedWatts)) metric("System draw", watts(battery.systemPowerWatts)) Spacer() } if !adapter.profiles.isEmpty { VStack(alignment: .leading, spacing: 7) { Text("Advertised power profiles").font(.caption.weight(.semibold)).foregroundStyle(.secondary) HStack(spacing: 7) { ForEach(adapter.profiles) { profile in Text(String(format: "%.0f V × %.2f A · %.0f W", profile.voltageVolts, profile.currentAmps, profile.watts)) .font(.caption.monospacedDigit()) .padding(.horizontal, 9).padding(.vertical, 5) .background(Color.accentColor.opacity(profile.voltageVolts == adapter.negotiatedVoltage ? 0.18 : 0.07), in: Capsule()) } } } } Label(cableEvidence(adapter), systemImage: "info.circle") .font(.caption).foregroundStyle(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(16) .background(Color.secondary.opacity(0.06), in: RoundedRectangle(cornerRadius: 10)) .overlay { RoundedRectangle(cornerRadius: 10).stroke(Color.secondary.opacity(0.12)) } } private var powerSection: some View { VStack(alignment: .leading, spacing: 10) { Text("Power history — 60 seconds").font(.headline) Chart(monitor.systemPowerHistory) { sample in AreaMark(x: .value("Time", sample.date), y: .value("Watts", sample.value)) .foregroundStyle(.orange.opacity(0.13)) LineMark(x: .value("Time", sample.date), y: .value("Watts", sample.value)) .foregroundStyle(.orange).lineStyle(.init(lineWidth: 2)) } .chartYAxisLabel("W") .chartXAxis(.hidden) .frame(height: 150) } .padding(16) .background(Color.secondary.opacity(0.06), in: RoundedRectangle(cornerRadius: 10)) .overlay { RoundedRectangle(cornerRadius: 10).stroke(Color.secondary.opacity(0.12)) } } private var portsSection: some View { VStack(spacing: 0) { HStack { Text("USB-C, USB, and Thunderbolt").font(.headline) Spacer() Toggle("Show available ports", isOn: $showEmptyPorts).toggleStyle(.switch).controlSize(.small) } .padding(8) if !visiblePorts.isEmpty { Divider() } ForEach(visiblePorts) { port in portRow(port) if port.id != visiblePorts.last?.id { Divider() } } } .padding(8) .background(Color.secondary.opacity(0.06), in: RoundedRectangle(cornerRadius: 10)) .overlay { RoundedRectangle(cornerRadius: 10).stroke(Color.secondary.opacity(0.12)) } } private func portRow(_ port: HardwarePortSnapshot) -> some View { VStack(alignment: .leading, spacing: 9) { HStack { Image(systemName: port.isConnected ? "cable.connector" : "bolt.horizontal.circle") .foregroundStyle(port.isConnected ? Color.green : Color.secondary) .frame(width: 22) VStack(alignment: .leading, spacing: 2) { Text(port.name).font(.callout.weight(.semibold)) Text("\(port.transport) • \(port.maximumSpeed)").font(.caption).foregroundStyle(.secondary) } Spacer() Text(port.status).font(.caption.weight(.medium)).foregroundStyle(port.isConnected ? Color.green : Color.secondary) } ForEach(port.devices) { device in HStack(spacing: 8) { Image(systemName: "arrow.turn.down.right").foregroundStyle(.tertiary) VStack(alignment: .leading, spacing: 2) { Text(device.name).lineLimit(1) Text(deviceDetail(device)).font(.caption).foregroundStyle(.secondary).lineLimit(1) } Spacer() } .padding(.leading, CGFloat(device.depth * 18 + 30)) } } .padding(10) } private var topConsumersSection: some View { VStack(alignment: .leading, spacing: 8) { Text("Top live consumers").font(.headline) VStack(spacing: 0) { ForEach(Array(monitor.processes.sorted { $0.cpu > $1.cpu }.prefix(5))) { process in HStack { ProcessIcon(name: process.displayName, path: process.executablePath) Text(process.displayName).lineLimit(1) Spacer() Text(Formatters.percent(process.cpu)).monospacedDigit().frame(width: 64, alignment: .trailing) Text(Formatters.bytes.string(fromByteCount: Int64(process.residentBytes))).monospacedDigit().frame(width: 90, alignment: .trailing) } .padding(.vertical, 6) } } } .padding(16) .background(Color.secondary.opacity(0.06), in: RoundedRectangle(cornerRadius: 10)) .overlay { RoundedRectangle(cornerRadius: 10).stroke(Color.secondary.opacity(0.12)) } } private var coolingSection: some View { VStack(alignment: .leading, spacing: 13) { HStack { Label("Cooling and fan control", systemImage: "fan") .font(.headline) Spacer() Text(monitor.hardware.fans.allSatisfy(\.isAutomatic) ? "Automatic" : "Manual") .font(.caption.weight(.medium)) .foregroundStyle(monitor.hardware.fans.allSatisfy(\.isAutomatic) ? .green : .orange) } Divider() if monitor.hardware.fans.isEmpty { HStack(spacing: 12) { Image(systemName: "fan.slash").font(.title2).foregroundStyle(.secondary) VStack(alignment: .leading, spacing: 3) { Text("Fan telemetry unavailable").font(.callout.weight(.medium)) Text("This Mac is not publishing compatible SMC fan data.") .font(.caption).foregroundStyle(.secondary) } } } else { ForEach(monitor.hardware.fans) { fan in fanRow(fan) if fan.id != monitor.hardware.fans.last?.id { Divider().opacity(0.55) } } } HStack(spacing: 28) { metric("Thermal pressure", monitor.hardware.thermalState) metric("Low Power Mode", ProcessInfo.processInfo.isLowPowerModeEnabled ? "On" : "Off") metric("Control backend", FanControlService.isAvailable ? "Ready" : "Unavailable") Spacer() } Text("Puter reads fan sensors directly through the Mac's SMC interface. Manual targets are clamped to each fan's reported safe range; Automatic returns control to macOS.") .font(.caption).foregroundStyle(.secondary) HStack { Button("Apply targets", systemImage: "speedometer") { showingFanConfirmation = true } .disabled(monitor.hardware.fans.isEmpty || fanCommandInProgress || !FanControlService.isAvailable) Button("Automatic", systemImage: "arrow.counterclockwise") { updateFans(manual: false) } .disabled(monitor.hardware.fans.isEmpty || fanCommandInProgress || !FanControlService.isAvailable) Button("Reduce monitor refresh", systemImage: "tortoise") { monitor.updateSpeed = .low } Spacer() if fanCommandInProgress { ProgressView().controlSize(.small).accessibilityLabel("Applying fan settings") } else if let fanStatus { Label(fanStatus, systemImage: "checkmark.circle.fill") .font(.caption) .foregroundStyle(.green) } } } .frame(maxWidth: .infinity, alignment: .leading) .padding(16) .background(Color.secondary.opacity(0.06), in: RoundedRectangle(cornerRadius: 10)) .overlay { RoundedRectangle(cornerRadius: 10).stroke(Color.secondary.opacity(0.12)) } } private func fanRow(_ fan: FanSnapshot) -> some View { let target = Binding( get: { safeTarget(for: fan) }, set: { fanTargets[fan.id] = min(fan.maximumRPM, max(fan.minimumRPM, $0)) } ) return HStack(spacing: 16) { ZStack { Circle().fill(Color.cyan.opacity(0.12)).frame(width: 40, height: 40) Image(systemName: "fan.fill").foregroundStyle(.cyan) } VStack(alignment: .leading, spacing: 3) { Text(fan.name).font(.callout.weight(.semibold)) Text("\(Int(fan.actualRPM.rounded())) RPM now · \(Int(fan.minimumRPM))–\(Int(fan.maximumRPM)) RPM") .font(.caption).foregroundStyle(.secondary).monospacedDigit() } .frame(width: 220, alignment: .leading) Slider(value: target, in: fan.minimumRPM...max(fan.minimumRPM + 1, fan.maximumRPM), step: 25) .accessibilityLabel("\(fan.name) target speed") .accessibilityValue("\(Int(target.wrappedValue.rounded())) RPM") .accessibilityHint("Adjusts from \(Int(fan.minimumRPM)) to \(Int(fan.maximumRPM)) RPM") Text("\(Int(target.wrappedValue.rounded())) RPM") .font(.callout.monospacedDigit()).frame(width: 82, alignment: .trailing) Text(fan.isAutomatic ? "Auto" : "Manual") .font(.caption.weight(.medium)) .foregroundStyle(fan.isAutomatic ? .green : .orange) .frame(width: 52, alignment: .trailing) } } private func updateFans(manual: Bool) { fanCommandInProgress = true fanStatus = nil let targets = Dictionary(uniqueKeysWithValues: monitor.hardware.fans.map { fan in (fan.id, safeTarget(for: fan)) }) Task { do { try await Task.detached { if manual { try FanControlService.setManualTargets(targets) } else { try FanControlService.restoreAutomatic() } }.value fanStatus = manual ? "Targets applied" : "Automatic control restored" monitor.refresh() } catch { fanError = error.localizedDescription } fanCommandInProgress = false } } private var fanTargetSummary: String { monitor.hardware.fans.map { fan in let target = safeTarget(for: fan) return "\(fan.name): \(Int(target.rounded())) RPM" }.joined(separator: ", ") } private func seedFanTargets(_ fans: [FanSnapshot]) { for fan in fans where fanTargets[fan.id] == nil { let reported = fan.targetRPM > 0 ? fan.targetRPM : fan.actualRPM fanTargets[fan.id] = min(fan.maximumRPM, max(fan.minimumRPM, reported)) } } private func safeTarget(for fan: FanSnapshot) -> Double { let reported = fanTargets[fan.id] ?? (fan.targetRPM > 0 ? fan.targetRPM : fan.actualRPM) return min(fan.maximumRPM, max(fan.minimumRPM, reported)) } private func metric(_ title: String, _ value: String) -> some View { VStack(alignment: .leading, spacing: 2) { Text(title).font(.caption).foregroundStyle(.secondary) Text(value).font(.callout.weight(.medium)).monospacedDigit().lineLimit(1).minimumScaleFactor(0.75) } } private var verdictTitle: String { if !battery.isPresent { return "Power telemetry available for desktop hardware" } if battery.isFullyCharged && battery.externalPowerConnected { return "Battery full — running from external power" } if battery.isCharging { return "Battery charging normally" } if battery.externalPowerConnected { return "External power connected — battery is not charging" } return "Running on battery" } private var verdictDetail: String { guard battery.isPresent else { return "Battery-specific fields are hidden because this Mac reports no internal battery." } if let adapter = battery.adapter { return "macOS reports a \(Int(adapter.ratedWatts.rounded())) W adapter and a \(watts(adapter.negotiatedWatts)) negotiated ceiling. Current system input is \(watts(battery.systemPowerWatts))." } return "Current battery draw is \(watts(battery.batteryPowerWatts)). Charger and cable capability are shown only when macOS publishes them." } private var verdictIcon: String { battery.externalPowerConnected ? "checkmark.circle.fill" : "battery.75percent" } private var verdictColor: Color { monitor.hardware.thermalState == "Serious" || monitor.hardware.thermalState == "Critical" ? .red : .green } private var thermalColor: Color { switch monitor.hardware.thermalState { case "Critical", "Serious": .red case "Fair": .orange default: .green } } private var thermalDetail: String { switch monitor.hardware.thermalState { case "Critical": "Performance is heavily constrained" case "Serious": "Performance may be reduced" case "Fair": "Elevated thermal pressure" default: "No thermal pressure" } } private var batteryColor: Color { battery.chargePercent < 20 ? .red : (battery.isCharging ? .green : .accentColor) } private var cycleText: String { battery.designCycleCount > 0 ? "\(battery.cycleCount) of \(battery.designCycleCount)" : "\(battery.cycleCount)" } private var timeRemaining: String { guard let minutes = battery.timeRemainingMinutes else { return battery.isFullyCharged ? "Full" : "Calculating" } return "\(minutes / 60)h \(minutes % 60)m" } private var powerSourceText: String { battery.externalPowerConnected ? "Power adapter" : "Internal battery" } private var batteryStateText: String { battery.isFullyCharged ? "Fully charged" : (battery.isCharging ? "Charging" : "Not charging") } private func cableEvidence(_ adapter: PowerAdapterSnapshot) -> String { "Observed USB-PD contract: \(String(format: "%.1f V at %.2f A", adapter.negotiatedVoltage, adapter.negotiatedCurrent)). Cable e-marker identity is not claimed unless macOS exposes it." } private func deviceDetail(_ device: ConnectedHardwareDevice) -> String { var parts = [device.vendor, device.speed].filter { $0 != "Not reported" } if let required = device.currentRequiredMA { parts.append("requests \(required) mA") } if let available = device.currentAvailableMA { parts.append("\(available) mA available") } return parts.isEmpty ? "Technical details not reported by macOS" : parts.joined(separator: " • ") } private func watts(_ value: Double) -> String { value > 0 ? String(format: "%.1f W", value) : "—" } private func capacity(_ value: Double) -> String { value > 0 ? String(format: "%.0f mAh", value) : "Not reported" } private func openPowerSettings() { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.battery") { NSWorkspace.shared.open(url) } } }