4 Commits
8 changed files with 79 additions and 192 deletions
-60
View File
@@ -3,7 +3,6 @@ import Combine
/// Bridges app-lifecycle events into SwiftUI: requests Notification Center /// Bridges app-lifecycle events into SwiftUI: requests Notification Center
/// authorization once at launch so threshold alerts can post banners. /// authorization once at launch so threshold alerts can post banners.
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate { final class AppDelegate: NSObject, NSApplicationDelegate {
var statusItem: NSStatusItem? var statusItem: NSStatusItem?
var popover: NSPopover? var popover: NSPopover?
@@ -75,62 +74,3 @@ struct MacMonitorApp: App {
} }
} }
} }
// MARK: - UI Shims (macOS 12 Support)
public struct MMLabeledContent<Label: View, Content: View>: View {
public let label: Label
public let content: Content
public init(@ViewBuilder content: () -> Content, @ViewBuilder label: () -> Label) {
self.label = label()
self.content = content()
}
public init(_ titleKey: LocalizedStringKey, @ViewBuilder content: () -> Content) where Label == Text {
self.label = Text(titleKey)
self.content = content()
}
public init<S: StringProtocol>(_ title: S, @ViewBuilder content: () -> Content) where Label == Text {
self.label = Text(title)
self.content = content()
}
public var body: some View {
HStack {
label
Spacer()
content
}
}
}
public extension View {
@ViewBuilder
func mmMonospaced() -> some View {
if #available(macOS 13.0, *) {
self.monospaced()
} else {
self
}
}
@ViewBuilder
func mmMonospacedDigit() -> some View {
if #available(macOS 13.0, *) {
self.monospacedDigit()
} else {
self
}
}
@ViewBuilder
func mmFormStyleGrouped() -> some View {
if #available(macOS 13.0, *) {
self.formStyle(.grouped)
} else {
self
}
}
}
@@ -118,12 +118,11 @@ public final class UNAlertNotificationDispatcher: NSObject, AlertNotificationDis
didReceive response: UNNotificationResponse, didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void withCompletionHandler completionHandler: @escaping () -> Void
) { ) {
let targetTab: String? let targetTab: String? = switch response.actionIdentifier {
switch response.actionIdentifier {
case AlertNotificationAction.viewProcessesIdentifier: case AlertNotificationAction.viewProcessesIdentifier:
targetTab = "Processes" "Processes"
default: default:
targetTab = nil nil
} }
Task { @MainActor in Task { @MainActor in
AppNavigationBus.shared.request(tab: targetTab) AppNavigationBus.shared.request(tab: targetTab)
+2 -3
View File
@@ -4,9 +4,8 @@ import SwiftUI
/// Sidebar tab showing currently breached thresholds and the in-app /// Sidebar tab showing currently breached thresholds and the in-app
/// alert history log maintained by `AlertEngine`. /// alert history log maintained by `AlertEngine`.
@MainActor
public struct AlertsView: View { public struct AlertsView: View {
@ObservedObject private var engine = AlertEngine.shared @State private var engine = AlertEngine.shared
public init() {} public init() {}
@@ -110,7 +109,7 @@ public struct AlertsView: View {
Text(event.alertID) Text(event.alertID)
.font(.caption2) .font(.caption2)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.mmMonospaced() .monospaced()
} }
Spacer() Spacer()
Text(event.timestamp.formatted(date: .abbreviated, time: .standard)) Text(event.timestamp.formatted(date: .abbreviated, time: .standard))
+25 -69
View File
@@ -1,7 +1,5 @@
import SwiftUI import SwiftUI
#if canImport(Charts)
import Charts import Charts
#endif
public struct TelemetryTrendChartView: View { public struct TelemetryTrendChartView: View {
public let title: String public let title: String
@@ -45,7 +43,8 @@ public struct TelemetryTrendChartView: View {
Spacer() Spacer()
HStack(spacing: 12) { HStack(spacing: 12) {
Text(String(format: "Cur: %.1f %@", currentValue, unit)) Text(String(format: "Cur: %.1f %@", currentValue, unit))
.font(.system(size: 11, weight: .semibold)) .font(.caption)
.fontWeight(.semibold)
.foregroundColor(color) .foregroundColor(color)
Text(String(format: "Avg: %.1f %@", averageValue, unit)) Text(String(format: "Avg: %.1f %@", averageValue, unit))
.font(.caption) .font(.caption)
@@ -56,78 +55,35 @@ public struct TelemetryTrendChartView: View {
} }
} }
#if canImport(Charts) Chart {
if #available(macOS 13.0, *) { ForEach(Array(samples.enumerated()), id: \.element.id) { index, sample in
Chart { LineMark(
ForEach(Array(samples.enumerated()), id: \.element.id) { index, sample in x: .value("Sample", index),
LineMark( y: .value("Value", sample.value)
x: .value("Sample", index), )
y: .value("Value", sample.value) .interpolationMethod(.monotone)
.foregroundStyle(color)
AreaMark(
x: .value("Sample", index),
y: .value("Value", sample.value)
)
.interpolationMethod(.monotone)
.foregroundStyle(
LinearGradient(
colors: [color.opacity(0.35), color.opacity(0.05)],
startPoint: .top,
endPoint: .bottom
) )
.interpolationMethod(.monotone) )
.foregroundStyle(color)
AreaMark(
x: .value("Sample", index),
y: .value("Value", sample.value)
)
.interpolationMethod(.monotone)
.foregroundStyle(
LinearGradient(
colors: [color.opacity(0.35), color.opacity(0.05)],
startPoint: .top,
endPoint: .bottom
)
)
}
} }
.chartYScale(domain: 0...(maxY ?? max(1.0, maxValue * 1.15)))
.chartXAxis(.hidden)
.frame(height: 120)
} else {
fallbackChart
} }
#else .chartYScale(domain: 0...(maxY ?? max(1.0, maxValue * 1.15)))
fallbackChart .chartXAxis(.hidden)
#endif .frame(height: 120)
} }
.padding(14) .padding(14)
.background(Color(NSColor.controlBackgroundColor)) .background(Color(NSColor.controlBackgroundColor))
.cornerRadius(10) .cornerRadius(10)
} }
@ViewBuilder
private var fallbackChart: some View {
Canvas { context, size in
guard samples.count > 1 else { return }
let effectiveMaxY = maxY ?? max(1.0, maxValue * 1.15)
let dx = size.width / CGFloat(samples.count - 1)
var path = Path()
var areaPath = Path()
for (index, sample) in samples.enumerated() {
let x = CGFloat(index) * dx
let y = size.height - (CGFloat(sample.value) / CGFloat(effectiveMaxY)) * size.height
if index == 0 {
path.move(to: CGPoint(x: x, y: y))
areaPath.move(to: CGPoint(x: x, y: size.height))
areaPath.addLine(to: CGPoint(x: x, y: y))
} else {
path.addLine(to: CGPoint(x: x, y: y))
areaPath.addLine(to: CGPoint(x: x, y: y))
}
}
if let lastIndex = samples.indices.last {
let lastX = CGFloat(lastIndex) * dx
areaPath.addLine(to: CGPoint(x: lastX, y: size.height))
areaPath.closeSubpath()
}
context.fill(areaPath, with: .linearGradient(Gradient(colors: [color.opacity(0.35), color.opacity(0.05)]), startPoint: .zero, endPoint: CGPoint(x: 0, y: size.height)))
context.stroke(path, with: .color(color), lineWidth: 2)
}
.frame(height: 120)
}
} }
+32 -38
View File
@@ -1,9 +1,8 @@
import SwiftUI import SwiftUI
@MainActor
public struct ContentView: View { public struct ContentView: View {
@StateObject private var store = SystemTelemetryStore.shared @StateObject private var store = SystemTelemetryStore.shared
@State private var selectedTab: String? = "Dashboard" @State private var selectedTab: String = "Dashboard"
@State private var processSearchText: String = "" @State private var processSearchText: String = ""
@State private var socketSearchText: String = "" @State private var socketSearchText: String = ""
@State private var selectedPID: Int32? = nil @State private var selectedPID: Int32? = nil
@@ -120,7 +119,7 @@ public struct ContentView: View {
headerCard headerCard
engineControlCard engineControlCard
switch selectedTab ?? "Dashboard" { switch selectedTab {
case "Dashboard": case "Dashboard":
dashboardView dashboardView
case "Architecture": case "Architecture":
@@ -169,7 +168,7 @@ public struct ContentView: View {
} }
.padding(24) .padding(24)
} }
.navigationTitle("MacMonitor — \(selectedTab ?? "Dashboard")") .navigationTitle("MacMonitor — \(selectedTab)")
} }
// MARK: - Dashboard Composite View // MARK: - Dashboard Composite View
@@ -358,20 +357,20 @@ public struct ContentView: View {
Label("Kernel & VM Telemetry", systemImage: "waveform.path.ecg") Label("Kernel & VM Telemetry", systemImage: "waveform.path.ecg")
.font(.headline) .font(.headline)
VStack(alignment: .leading, spacing: 8) { Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 8) {
HStack { GridRow {
Text("Context Switches:").foregroundStyle(.secondary) Text("Context Switches:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.contextSwitchesPerSec))/s").bold().monospacedDigit() Text("\(Int(store.kernelCounters.contextSwitchesPerSec))/s").bold().monospacedDigit()
Text("Syscalls:").foregroundStyle(.secondary) Text("Syscalls:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.syscallsPerSec))/s").bold().monospacedDigit() Text("\(Int(store.kernelCounters.syscallsPerSec))/s").bold().monospacedDigit()
} }
HStack { GridRow {
Text("Page Faults:").foregroundStyle(.secondary) Text("Page Faults:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.pageFaultsPerSec))/s").bold().monospacedDigit() Text("\(Int(store.kernelCounters.pageFaultsPerSec))/s").bold().monospacedDigit()
Text("COW Faults:").foregroundStyle(.secondary) Text("COW Faults:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.cowFaultsPerSec))/s").bold().monospacedDigit() Text("\(Int(store.kernelCounters.cowFaultsPerSec))/s").bold().monospacedDigit()
} }
HStack { GridRow {
Text("Pageins / Pageouts:").foregroundStyle(.secondary) Text("Pageins / Pageouts:").foregroundStyle(.secondary)
Text("\(Int(store.kernelCounters.pageinsPerSec)) / \(Int(store.kernelCounters.pageoutsPerSec)) /s").bold().monospacedDigit() Text("\(Int(store.kernelCounters.pageinsPerSec)) / \(Int(store.kernelCounters.pageoutsPerSec)) /s").bold().monospacedDigit()
Text("Zero-Fill:").foregroundStyle(.secondary) Text("Zero-Fill:").foregroundStyle(.secondary)
@@ -446,20 +445,20 @@ public struct ContentView: View {
ProgressView(value: min(store.memory.utilizationPercentage / 100.0, 1.0)) ProgressView(value: min(store.memory.utilizationPercentage / 100.0, 1.0))
.tint(store.memory.utilizationPercentage > 85 ? .red : .blue) .tint(store.memory.utilizationPercentage > 85 ? .red : .blue)
VStack(alignment: .leading, spacing: 6) { Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 6) {
HStack { GridRow {
Text("Used:").foregroundStyle(.secondary) Text("Used:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.usedBytes)).bold() Text(formatBytes(store.memory.usedBytes)).bold()
Text("Free:").foregroundStyle(.secondary) Text("Free:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.freeBytes)).bold() Text(formatBytes(store.memory.freeBytes)).bold()
} }
HStack { GridRow {
Text("Wired:").foregroundStyle(.secondary) Text("Wired:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.wiredBytes)) Text(formatBytes(store.memory.wiredBytes))
Text("Compressed:").foregroundStyle(.secondary) Text("Compressed:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.compressedBytes)) Text(formatBytes(store.memory.compressedBytes))
} }
HStack { GridRow {
Text("Swap Used:").foregroundStyle(.secondary) Text("Swap Used:").foregroundStyle(.secondary)
Text(formatBytes(store.memory.swapUsedBytes)) Text(formatBytes(store.memory.swapUsedBytes))
Text("Swap Total:").foregroundStyle(.secondary) Text("Swap Total:").foregroundStyle(.secondary)
@@ -603,10 +602,10 @@ public struct ContentView: View {
Text(s.protocolName) Text(s.protocolName)
} }
TableColumn("Local Endpoint") { s in TableColumn("Local Endpoint") { s in
Text("\(s.localAddress):\(s.localPort)").mmMonospaced() Text("\(s.localAddress):\(s.localPort)").monospaced()
} }
TableColumn("Remote Endpoint") { s in TableColumn("Remote Endpoint") { s in
Text("\(s.remoteAddress):\(s.remotePort)").mmMonospaced() Text("\(s.remoteAddress):\(s.remotePort)").monospaced()
} }
TableColumn("State") { s in TableColumn("State") { s in
Text(s.tcpState).font(.caption).foregroundStyle(s.tcpState == "ESTABLISHED" ? .green : .secondary) Text(s.tcpState).font(.caption).foregroundStyle(s.tcpState == "ESTABLISHED" ? .green : .secondary)
@@ -752,20 +751,20 @@ public struct ContentView: View {
if !store.batteryHealth.hasBattery { if !store.batteryHealth.hasBattery {
Text("Desktop Mac / No battery installed").font(.caption).foregroundStyle(.secondary) Text("Desktop Mac / No battery installed").font(.caption).foregroundStyle(.secondary)
} else { } else {
VStack(alignment: .leading, spacing: 6) { Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 6) {
HStack { GridRow {
Text("Cycle Count:").foregroundStyle(.secondary) Text("Cycle Count:").foregroundStyle(.secondary)
Text("\(store.batteryHealth.cycleCount) / \(store.batteryHealth.designCycleCount)").bold() Text("\(store.batteryHealth.cycleCount) / \(store.batteryHealth.designCycleCount)").bold()
Text("Temperature:").foregroundStyle(.secondary) Text("Temperature:").foregroundStyle(.secondary)
Text(String(format: "%.1f°C", store.batteryHealth.temperature)).bold() Text(String(format: "%.1f°C", store.batteryHealth.temperature)).bold()
} }
HStack { GridRow {
Text("Voltage / Power:").foregroundStyle(.secondary) Text("Voltage / Power:").foregroundStyle(.secondary)
Text("\(String(format: "%.2f V", store.batteryHealth.voltage)) (\(String(format: "%.1f W", store.batteryHealth.watts)))") Text("\(String(format: "%.2f V", store.batteryHealth.voltage)) (\(String(format: "%.1f W", store.batteryHealth.watts)))")
Text("Power Adapter:").foregroundStyle(.secondary) Text("Power Adapter:").foregroundStyle(.secondary)
Text(store.batteryHealth.externalConnected ? "\(store.batteryHealth.adapterWatts)W Connected" : "On Battery") Text(store.batteryHealth.externalConnected ? "\(store.batteryHealth.adapterWatts)W Connected" : "On Battery")
} }
HStack { GridRow {
Text("Capacity:").foregroundStyle(.secondary) Text("Capacity:").foregroundStyle(.secondary)
Text("\(store.batteryHealth.currentCapacity) / \(store.batteryHealth.maxCapacity) mAh") Text("\(store.batteryHealth.currentCapacity) / \(store.batteryHealth.maxCapacity) mAh")
Text("Manufacturer:").foregroundStyle(.secondary) Text("Manufacturer:").foregroundStyle(.secondary)
@@ -798,10 +797,10 @@ public struct ContentView: View {
.background(Color.secondary.opacity(0.15), in: Capsule()) .background(Color.secondary.opacity(0.15), in: Capsule())
} }
TableColumn("Vendor ID") { d in TableColumn("Vendor ID") { d in
Text(String(format: "0x%04x", d.vendorID)).mmMonospaced() Text(String(format: "0x%04x", d.vendorID)).monospaced()
} }
TableColumn("Product ID") { d in TableColumn("Product ID") { d in
Text(String(format: "0x%04x", d.productID)).mmMonospaced() Text(String(format: "0x%04x", d.productID)).monospaced()
} }
TableColumn("Built-In") { d in TableColumn("Built-In") { d in
Text(d.isBuiltIn ? "Yes" : "External").foregroundStyle(d.isBuiltIn ? Color.secondary : Color.blue) Text(d.isBuiltIn ? "Yes" : "External").foregroundStyle(d.isBuiltIn ? Color.secondary : Color.blue)
@@ -962,14 +961,14 @@ public struct ContentView: View {
.font(.title3.bold().monospacedDigit()) .font(.title3.bold().monospacedDigit())
} }
VStack(alignment: .leading, spacing: 6) { Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 6) {
HStack { GridRow {
Text("CPU Power:").foregroundStyle(.secondary) Text("CPU Power:").foregroundStyle(.secondary)
Text(String(format: "%.1f W", store.power.cpuWatts)).bold() Text(String(format: "%.1f W", store.power.cpuWatts)).bold()
Text("GPU Power:").foregroundStyle(.secondary) Text("GPU Power:").foregroundStyle(.secondary)
Text(String(format: "%.1f W", store.power.gpuWatts)).bold() Text(String(format: "%.1f W", store.power.gpuWatts)).bold()
} }
HStack { GridRow {
Text("CPU Voltage:").foregroundStyle(.secondary) Text("CPU Voltage:").foregroundStyle(.secondary)
Text(String(format: "%.2f V", store.power.cpuVoltage)) Text(String(format: "%.2f V", store.power.cpuVoltage))
Text("CPU Current:").foregroundStyle(.secondary) Text("CPU Current:").foregroundStyle(.secondary)
@@ -990,22 +989,24 @@ public struct ContentView: View {
Text("Host Architecture & Kernel") Text("Host Architecture & Kernel")
.font(.headline) .font(.headline)
VStack(alignment: .leading, spacing: 10) { Grid(alignment: .leading, horizontalSpacing: 24, verticalSpacing: 10) {
HStack { GridRow {
Text("Hardware Model:").foregroundStyle(.secondary) Text("Hardware Model:").foregroundStyle(.secondary)
Text(store.hostModel).bold() Text(store.hostModel).bold()
Text("Physical Cores:").foregroundStyle(.secondary) Text("Physical Cores:").foregroundStyle(.secondary)
Text("\(store.physicalCpuCount)").bold() Text("\(store.physicalCpuCount)").bold()
} }
HStack { GridRow {
Text("Darwin Kernel:").foregroundStyle(.secondary) Text("Darwin Kernel:").foregroundStyle(.secondary)
Text(store.kernelVersion).bold() Text(store.kernelVersion).bold()
Text("Logical Cores:").foregroundStyle(.secondary) Text("Logical Cores:").foregroundStyle(.secondary)
Text("\(store.logicalCpuCount)").bold() Text("\(store.logicalCpuCount)").bold()
} }
HStack { GridRow {
Text("Target Architecture:").foregroundStyle(.secondary) Text("Target Architecture:").foregroundStyle(.secondary)
Text("Intel x86_64").bold() Text("Intel x86_64").bold()
Text("Deployment Target:").foregroundStyle(.secondary)
Text("macOS 14.0 (Sonoma)").bold()
} }
} }
} }
@@ -1219,7 +1220,6 @@ public struct ContentView: View {
} }
// MARK: - Process Detail Inspector Sheet // MARK: - Process Detail Inspector Sheet
@MainActor
struct ProcessDetailSheet: View { struct ProcessDetailSheet: View {
let inspector: MMProcessDetailInspector let inspector: MMProcessDetailInspector
let pid: Int32 let pid: Int32
@@ -1248,9 +1248,8 @@ struct ProcessDetailSheet: View {
if selectedDetailTab == "Threads" { if selectedDetailTab == "Threads" {
List(threads.indices, id: \.self) { idx in List(threads.indices, id: \.self) { idx in
let t = threads[idx] let t = threads[idx]
let threadID = "\(t["threadID"] ?? idx)"
HStack { HStack {
Text("Thread \(threadID)").bold() Text("Thread \(t["threadID"] ?? idx)").bold()
Spacer() Spacer()
Text("CPU: \(String(format: "%.1f%%", (t["cpuPercent"] as? Double) ?? 0.0))").monospacedDigit() Text("CPU: \(String(format: "%.1f%%", (t["cpuPercent"] as? Double) ?? 0.0))").monospacedDigit()
Text(t["state"] as? String ?? "").foregroundStyle(.secondary) Text(t["state"] as? String ?? "").foregroundStyle(.secondary)
@@ -1263,16 +1262,11 @@ struct ProcessDetailSheet: View {
} else { } else {
List(sockets.indices, id: \.self) { idx in List(sockets.indices, id: \.self) { idx in
let s = sockets[idx] let s = sockets[idx]
let proto = s["protocol"] as? String ?? "TCP"
let localAddr = (s["localAddress"] as? String) ?? ""
let localPort = (s["localPort"] as? Int) ?? 0
let remoteAddr = (s["remoteAddress"] as? String) ?? ""
let remotePort = (s["remotePort"] as? Int) ?? 0
HStack { HStack {
Text(proto).bold() Text(s["protocol"] as? String ?? "TCP").bold()
Spacer() Spacer()
Text("\(localAddr):\(localPort) → \(remoteAddr):\(remotePort)") Text("\(s["localAddress"] ?? ""):\(s["localPort"] ?? 0) → \(s["remoteAddress"] ?? ""):\(s["remotePort"] ?? 0)")
.mmMonospaced() .monospaced()
Text(s["tcpState"] as? String ?? "").foregroundStyle(.secondary) Text(s["tcpState"] as? String ?? "").foregroundStyle(.secondary)
} }
} }
@@ -1,8 +1,7 @@
import SwiftUI import SwiftUI
@MainActor
public struct QuickGlancePopoverView: View { public struct QuickGlancePopoverView: View {
@ObservedObject var store: SystemTelemetryStore var store: SystemTelemetryStore
public init(store: SystemTelemetryStore) { public init(store: SystemTelemetryStore) {
self.store = store self.store = store
@@ -110,7 +109,9 @@ public struct QuickGlancePopoverView: View {
} }
Spacer() Spacer()
Text(value) Text(value)
.font(.system(size: 13, weight: .semibold, design: .monospaced)) .font(.subheadline)
.fontWeight(.semibold)
.fontDesign(.monospaced)
} }
} }
+13 -14
View File
@@ -5,7 +5,6 @@ import UserNotifications
/// Preferences UI for threshold alert rules. Bound directly to /// Preferences UI for threshold alert rules. Bound directly to
/// `AlertEngine.shared.configuration`, which auto-persists to `UserDefaults`. /// `AlertEngine.shared.configuration`, which auto-persists to `UserDefaults`.
@MainActor
public struct AlertsSettingsView: View { public struct AlertsSettingsView: View {
@ObservedObject private var engine = AlertEngine.shared @ObservedObject private var engine = AlertEngine.shared
@State private var authorizationStatus: UNAuthorizationStatus = .notDetermined @State private var authorizationStatus: UNAuthorizationStatus = .notDetermined
@@ -59,24 +58,24 @@ public struct AlertsSettingsView: View {
Section("CPU Temperature") { Section("CPU Temperature") {
Toggle("High CPU Temperature Alert", isOn: $engine.configuration.cpuTempEnabled) Toggle("High CPU Temperature Alert", isOn: $engine.configuration.cpuTempEnabled)
MMLabeledContent("Threshold") { LabeledContent("Threshold") {
Slider(value: $engine.configuration.cpuTempThresholdC, in: 60...110, step: 1) { Slider(value: $engine.configuration.cpuTempThresholdC, in: 60...110, step: 1) {
EmptyView() EmptyView()
} }
Text("\(Int(engine.configuration.cpuTempThresholdC))°C") Text("\(Int(engine.configuration.cpuTempThresholdC))°C")
.mmMonospacedDigit() .monospacedDigit()
.frame(width: 48, alignment: .trailing) .frame(width: 48, alignment: .trailing)
} }
} }
Section("Cooling Fans") { Section("Cooling Fans") {
Toggle("Fan Stall Alert", isOn: $engine.configuration.fanStallEnabled) Toggle("Fan Stall Alert", isOn: $engine.configuration.fanStallEnabled)
MMLabeledContent("Alert when CPU above") { LabeledContent("Alert when CPU above") {
Slider(value: $engine.configuration.fanStallTempThresholdC, in: 40...100, step: 1) { Slider(value: $engine.configuration.fanStallTempThresholdC, in: 40...100, step: 1) {
EmptyView() EmptyView()
} }
Text("\(Int(engine.configuration.fanStallTempThresholdC))°C") Text("\(Int(engine.configuration.fanStallTempThresholdC))°C")
.mmMonospacedDigit() .monospacedDigit()
.frame(width: 48, alignment: .trailing) .frame(width: 48, alignment: .trailing)
} }
Text("Triggers when a fan reports 0 RPM while the CPU is hot.") Text("Triggers when a fan reports 0 RPM while the CPU is hot.")
@@ -86,12 +85,12 @@ public struct AlertsSettingsView: View {
Section("Memory") { Section("Memory") {
Toggle("Low Memory Alert", isOn: $engine.configuration.memoryEnabled) Toggle("Low Memory Alert", isOn: $engine.configuration.memoryEnabled)
MMLabeledContent("Free RAM below") { LabeledContent("Free RAM below") {
Slider(value: $engine.configuration.memoryFreeMBThreshold, in: 100...4096, step: 50) { Slider(value: $engine.configuration.memoryFreeMBThreshold, in: 100...4096, step: 50) {
EmptyView() EmptyView()
} }
Text("\(Int(engine.configuration.memoryFreeMBThreshold)) MB") Text("\(Int(engine.configuration.memoryFreeMBThreshold)) MB")
.mmMonospacedDigit() .monospacedDigit()
.frame(width: 64, alignment: .trailing) .frame(width: 64, alignment: .trailing)
} }
Toggle("Alert on Critical Memory Pressure", isOn: $engine.configuration.memoryAlertOnCriticalPressure) Toggle("Alert on Critical Memory Pressure", isOn: $engine.configuration.memoryAlertOnCriticalPressure)
@@ -99,32 +98,32 @@ public struct AlertsSettingsView: View {
Section("Storage") { Section("Storage") {
Toggle("Low Disk Space Alert", isOn: $engine.configuration.storageEnabled) Toggle("Low Disk Space Alert", isOn: $engine.configuration.storageEnabled)
MMLabeledContent("Free space below") { LabeledContent("Free space below") {
Slider(value: $engine.configuration.storageFreeGBThreshold, in: 1...100, step: 1) { Slider(value: $engine.configuration.storageFreeGBThreshold, in: 1...100, step: 1) {
EmptyView() EmptyView()
} }
Text("\(Int(engine.configuration.storageFreeGBThreshold)) GB") Text("\(Int(engine.configuration.storageFreeGBThreshold)) GB")
.mmMonospacedDigit() .monospacedDigit()
.frame(width: 48, alignment: .trailing) .frame(width: 48, alignment: .trailing)
} }
MMLabeledContent("Free percent below") { LabeledContent("Free percent below") {
Slider(value: $engine.configuration.storageFreePercentThreshold, in: 1...50, step: 1) { Slider(value: $engine.configuration.storageFreePercentThreshold, in: 1...50, step: 1) {
EmptyView() EmptyView()
} }
Text("\(Int(engine.configuration.storageFreePercentThreshold))%") Text("\(Int(engine.configuration.storageFreePercentThreshold))%")
.mmMonospacedDigit() .monospacedDigit()
.frame(width: 48, alignment: .trailing) .frame(width: 48, alignment: .trailing)
} }
} }
Section("Battery") { Section("Battery") {
Toggle("Low Battery Alert", isOn: $engine.configuration.batteryEnabled) Toggle("Low Battery Alert", isOn: $engine.configuration.batteryEnabled)
MMLabeledContent("Charge below") { LabeledContent("Charge below") {
Slider(value: $engine.configuration.batteryPercentThreshold, in: 1...50, step: 1) { Slider(value: $engine.configuration.batteryPercentThreshold, in: 1...50, step: 1) {
EmptyView() EmptyView()
} }
Text("\(Int(engine.configuration.batteryPercentThreshold))%") Text("\(Int(engine.configuration.batteryPercentThreshold))%")
.mmMonospacedDigit() .monospacedDigit()
.frame(width: 48, alignment: .trailing) .frame(width: 48, alignment: .trailing)
} }
Text("Only fires while discharging on battery power.") Text("Only fires while discharging on battery power.")
@@ -132,7 +131,7 @@ public struct AlertsSettingsView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
} }
.mmFormStyleGrouped() .formStyle(.grouped)
.frame(minWidth: 480, minHeight: 520) .frame(minWidth: 480, minHeight: 520)
.task { await refreshAuthorizationStatus() } .task { await refreshAuthorizationStatus() }
} }
-1
View File
@@ -2,7 +2,6 @@ import XCTest
import SwiftUI import SwiftUI
@testable import MacMonitor @testable import MacMonitor
@MainActor
final class MMMenuBarTests: XCTestCase { final class MMMenuBarTests: XCTestCase {
func testMenuBarStatusViewInitialization() { func testMenuBarStatusViewInitialization() {