Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75f8fc2494 | ||
|
|
f8f2cc0b90 | ||
|
|
9f5481a1a9 |
@@ -3,6 +3,7 @@ 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?
|
||||||
@@ -74,3 +75,62 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ public struct AlertsView: View {
|
|||||||
Text(event.alertID)
|
Text(event.alertID)
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.monospaced()
|
.mmMonospaced()
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(event.timestamp.formatted(date: .abbreviated, time: .standard))
|
Text(event.timestamp.formatted(date: .abbreviated, time: .standard))
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
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
|
||||||
@@ -43,8 +45,7 @@ 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(.caption)
|
.font(.system(size: 11, weight: .semibold))
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(color)
|
.foregroundColor(color)
|
||||||
Text(String(format: "Avg: %.1f %@", averageValue, unit))
|
Text(String(format: "Avg: %.1f %@", averageValue, unit))
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
@@ -55,35 +56,78 @@ public struct TelemetryTrendChartView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Chart {
|
#if canImport(Charts)
|
||||||
ForEach(Array(samples.enumerated()), id: \.element.id) { index, sample in
|
if #available(macOS 13.0, *) {
|
||||||
LineMark(
|
Chart {
|
||||||
x: .value("Sample", index),
|
ForEach(Array(samples.enumerated()), id: \.element.id) { index, sample in
|
||||||
y: .value("Value", sample.value)
|
LineMark(
|
||||||
)
|
x: .value("Sample", index),
|
||||||
.interpolationMethod(.monotone)
|
y: .value("Value", sample.value)
|
||||||
.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
|
||||||
}
|
}
|
||||||
.chartYScale(domain: 0...(maxY ?? max(1.0, maxValue * 1.15)))
|
#else
|
||||||
.chartXAxis(.hidden)
|
fallbackChart
|
||||||
.frame(height: 120)
|
#endif
|
||||||
}
|
}
|
||||||
.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import SwiftUI
|
|||||||
|
|
||||||
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
|
||||||
@@ -119,7 +119,7 @@ public struct ContentView: View {
|
|||||||
headerCard
|
headerCard
|
||||||
engineControlCard
|
engineControlCard
|
||||||
|
|
||||||
switch selectedTab {
|
switch selectedTab ?? "Dashboard" {
|
||||||
case "Dashboard":
|
case "Dashboard":
|
||||||
dashboardView
|
dashboardView
|
||||||
case "Architecture":
|
case "Architecture":
|
||||||
@@ -357,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)
|
||||||
|
|
||||||
Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
GridRow {
|
HStack {
|
||||||
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()
|
||||||
}
|
}
|
||||||
GridRow {
|
HStack {
|
||||||
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()
|
||||||
}
|
}
|
||||||
GridRow {
|
HStack {
|
||||||
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)
|
||||||
@@ -445,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)
|
||||||
|
|
||||||
Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
GridRow {
|
HStack {
|
||||||
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()
|
||||||
}
|
}
|
||||||
GridRow {
|
HStack {
|
||||||
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))
|
||||||
}
|
}
|
||||||
GridRow {
|
HStack {
|
||||||
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)
|
||||||
@@ -602,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)").monospaced()
|
Text("\(s.localAddress):\(s.localPort)").mmMonospaced()
|
||||||
}
|
}
|
||||||
TableColumn("Remote Endpoint") { s in
|
TableColumn("Remote Endpoint") { s in
|
||||||
Text("\(s.remoteAddress):\(s.remotePort)").monospaced()
|
Text("\(s.remoteAddress):\(s.remotePort)").mmMonospaced()
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -751,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 {
|
||||||
Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
GridRow {
|
HStack {
|
||||||
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()
|
||||||
}
|
}
|
||||||
GridRow {
|
HStack {
|
||||||
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")
|
||||||
}
|
}
|
||||||
GridRow {
|
HStack {
|
||||||
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)
|
||||||
@@ -797,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)).monospaced()
|
Text(String(format: "0x%04x", d.vendorID)).mmMonospaced()
|
||||||
}
|
}
|
||||||
TableColumn("Product ID") { d in
|
TableColumn("Product ID") { d in
|
||||||
Text(String(format: "0x%04x", d.productID)).monospaced()
|
Text(String(format: "0x%04x", d.productID)).mmMonospaced()
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -961,14 +961,14 @@ public struct ContentView: View {
|
|||||||
.font(.title3.bold().monospacedDigit())
|
.font(.title3.bold().monospacedDigit())
|
||||||
}
|
}
|
||||||
|
|
||||||
Grid(alignment: .leading, horizontalSpacing: 20, verticalSpacing: 6) {
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
GridRow {
|
HStack {
|
||||||
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()
|
||||||
}
|
}
|
||||||
GridRow {
|
HStack {
|
||||||
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)
|
||||||
@@ -989,20 +989,20 @@ public struct ContentView: View {
|
|||||||
Text("Host Architecture & Kernel")
|
Text("Host Architecture & Kernel")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
|
|
||||||
Grid(alignment: .leading, horizontalSpacing: 24, verticalSpacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
GridRow {
|
HStack {
|
||||||
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()
|
||||||
}
|
}
|
||||||
GridRow {
|
HStack {
|
||||||
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()
|
||||||
}
|
}
|
||||||
GridRow {
|
HStack {
|
||||||
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("Deployment Target:").foregroundStyle(.secondary)
|
||||||
@@ -1266,7 +1266,7 @@ struct ProcessDetailSheet: View {
|
|||||||
Text(s["protocol"] as? String ?? "TCP").bold()
|
Text(s["protocol"] as? String ?? "TCP").bold()
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("\(s["localAddress"] ?? ""):\(s["localPort"] ?? 0) → \(s["remoteAddress"] ?? ""):\(s["remotePort"] ?? 0)")
|
Text("\(s["localAddress"] ?? ""):\(s["localPort"] ?? 0) → \(s["remoteAddress"] ?? ""):\(s["remotePort"] ?? 0)")
|
||||||
.monospaced()
|
.mmMonospaced()
|
||||||
Text(s["tcpState"] as? String ?? "").foregroundStyle(.secondary)
|
Text(s["tcpState"] as? String ?? "").foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,9 +109,7 @@ public struct QuickGlancePopoverView: View {
|
|||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(value)
|
Text(value)
|
||||||
.font(.subheadline)
|
.font(.system(size: 13, weight: .semibold, design: .monospaced))
|
||||||
.fontWeight(.semibold)
|
|
||||||
.fontDesign(.monospaced)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,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)
|
||||||
LabeledContent("Threshold") {
|
MMLabeledContent("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")
|
||||||
.monospacedDigit()
|
.mmMonospacedDigit()
|
||||||
.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)
|
||||||
LabeledContent("Alert when CPU above") {
|
MMLabeledContent("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")
|
||||||
.monospacedDigit()
|
.mmMonospacedDigit()
|
||||||
.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.")
|
||||||
@@ -85,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)
|
||||||
LabeledContent("Free RAM below") {
|
MMLabeledContent("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")
|
||||||
.monospacedDigit()
|
.mmMonospacedDigit()
|
||||||
.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)
|
||||||
@@ -98,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)
|
||||||
LabeledContent("Free space below") {
|
MMLabeledContent("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")
|
||||||
.monospacedDigit()
|
.mmMonospacedDigit()
|
||||||
.frame(width: 48, alignment: .trailing)
|
.frame(width: 48, alignment: .trailing)
|
||||||
}
|
}
|
||||||
LabeledContent("Free percent below") {
|
MMLabeledContent("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))%")
|
||||||
.monospacedDigit()
|
.mmMonospacedDigit()
|
||||||
.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)
|
||||||
LabeledContent("Charge below") {
|
MMLabeledContent("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))%")
|
||||||
.monospacedDigit()
|
.mmMonospacedDigit()
|
||||||
.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.")
|
||||||
@@ -131,7 +131,7 @@ public struct AlertsSettingsView: View {
|
|||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.mmFormStyleGrouped()
|
||||||
.frame(minWidth: 480, minHeight: 520)
|
.frame(minWidth: 480, minHeight: 520)
|
||||||
.task { await refreshAuthorizationStatus() }
|
.task { await refreshAuthorizationStatus() }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user