397 lines
13 KiB
Swift
397 lines
13 KiB
Swift
import Foundation
|
|
import SwiftUI
|
|
|
|
enum TaskSection: String, CaseIterable, Identifiable {
|
|
case processes = "Processes"
|
|
case performance = "Performance"
|
|
case history = "App history"
|
|
case startup = "Startup apps"
|
|
case users = "Users"
|
|
case details = "Details"
|
|
case services = "Services"
|
|
case hardware = "Hardware"
|
|
case settings = "Settings"
|
|
|
|
var id: String { rawValue }
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .processes: "square.stack.3d.up"
|
|
case .performance: "waveform.path.ecg"
|
|
case .history: "clock.arrow.circlepath"
|
|
case .startup: "gauge.open.with.lines.needle.33percent"
|
|
case .users: "person.2"
|
|
case .details: "list.bullet.rectangle"
|
|
case .services: "gearshape.2"
|
|
case .hardware: "bolt.horizontal.circle"
|
|
case .settings: "gearshape"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct PowerProfile: Identifiable, Hashable, Sendable {
|
|
let voltageVolts: Double
|
|
let currentAmps: Double
|
|
var id: String { "\(voltageVolts)-\(currentAmps)" }
|
|
var watts: Double { voltageVolts * currentAmps }
|
|
}
|
|
|
|
struct PowerAdapterSnapshot: Hashable, Sendable {
|
|
var name = "Power adapter"
|
|
var manufacturer = "Not reported"
|
|
var serial = ""
|
|
var ratedWatts = 0.0
|
|
var negotiatedVoltage = 0.0
|
|
var negotiatedCurrent = 0.0
|
|
var profiles: [PowerProfile] = []
|
|
|
|
var negotiatedWatts: Double { negotiatedVoltage * negotiatedCurrent }
|
|
}
|
|
|
|
struct BatterySnapshot: Hashable, Sendable {
|
|
var isPresent = false
|
|
var chargePercent = 0.0
|
|
var isCharging = false
|
|
var isFullyCharged = false
|
|
var externalPowerConnected = false
|
|
var cycleCount = 0
|
|
var designCycleCount = 0
|
|
var currentCapacityMAh = 0.0
|
|
var fullChargeCapacityMAh = 0.0
|
|
var designCapacityMAh = 0.0
|
|
var voltageVolts = 0.0
|
|
var currentAmps = 0.0
|
|
var temperatureCelsius = 0.0
|
|
var timeRemainingMinutes: Int?
|
|
var serial = ""
|
|
var adapter: PowerAdapterSnapshot?
|
|
var systemPowerWatts = 0.0
|
|
|
|
var healthPercent: Double {
|
|
guard designCapacityMAh > 0 else { return 0 }
|
|
return min(100, fullChargeCapacityMAh / designCapacityMAh * 100)
|
|
}
|
|
|
|
var batteryPowerWatts: Double {
|
|
let watts = abs(voltageVolts * currentAmps)
|
|
return watts.isFinite && watts <= 500 ? watts : 0
|
|
}
|
|
}
|
|
|
|
struct ConnectedHardwareDevice: Identifiable, Hashable, Sendable {
|
|
let id: String
|
|
let name: String
|
|
let vendor: String
|
|
let speed: String
|
|
let currentAvailableMA: Int?
|
|
let currentRequiredMA: Int?
|
|
let depth: Int
|
|
}
|
|
|
|
struct HardwarePortSnapshot: Identifiable, Hashable, Sendable {
|
|
let id: String
|
|
let name: String
|
|
let transport: String
|
|
let maximumSpeed: String
|
|
let isConnected: Bool
|
|
let status: String
|
|
let devices: [ConnectedHardwareDevice]
|
|
}
|
|
|
|
struct FanSnapshot: Identifiable, Hashable, Sendable {
|
|
let id: Int
|
|
let name: String
|
|
let actualRPM: Double
|
|
let minimumRPM: Double
|
|
let maximumRPM: Double
|
|
let targetRPM: Double
|
|
let mode: String
|
|
|
|
var isAutomatic: Bool { mode.localizedCaseInsensitiveContains("automatic") }
|
|
}
|
|
|
|
struct HardwareSnapshot: Hashable, Sendable {
|
|
var battery = BatterySnapshot()
|
|
var ports: [HardwarePortSnapshot] = []
|
|
var fans: [FanSnapshot] = []
|
|
var thermalState = "Nominal"
|
|
var capturedAt = Date.distantPast
|
|
}
|
|
|
|
struct MacMemorySpecification: Hashable, Sendable {
|
|
let chip: String
|
|
let cpuCoreCount: Int?
|
|
let bandwidthGBps: Double
|
|
let isMaximum: Bool
|
|
|
|
var displayValue: String {
|
|
let bandwidth = bandwidthGBps.rounded() == bandwidthGBps
|
|
? String(format: "%.0f", bandwidthGBps)
|
|
: String(format: "%.2f", bandwidthGBps)
|
|
return "\(isMaximum ? "Up to " : "")\(bandwidth) GB/s bandwidth (Apple specification)"
|
|
}
|
|
}
|
|
|
|
enum MemoryBandwidthCatalog {
|
|
// Ordered from most-specific to broadest. Core-count entries distinguish
|
|
// configurations where Apple ships one chip name with multiple memory buses.
|
|
static let specifications: [MacMemorySpecification] = [
|
|
.init(chip: "M5 Max", cpuCoreCount: nil, bandwidthGBps: 614, isMaximum: true),
|
|
.init(chip: "M5 Pro", cpuCoreCount: nil, bandwidthGBps: 307, isMaximum: true),
|
|
.init(chip: "M5", cpuCoreCount: nil, bandwidthGBps: 153, isMaximum: false),
|
|
.init(chip: "M4 Max", cpuCoreCount: 16, bandwidthGBps: 546, isMaximum: false),
|
|
.init(chip: "M4 Max", cpuCoreCount: 14, bandwidthGBps: 410, isMaximum: false),
|
|
.init(chip: "M4 Pro", cpuCoreCount: nil, bandwidthGBps: 273, isMaximum: false),
|
|
.init(chip: "M4", cpuCoreCount: nil, bandwidthGBps: 120, isMaximum: false),
|
|
.init(chip: "M3 Ultra", cpuCoreCount: nil, bandwidthGBps: 819, isMaximum: false),
|
|
.init(chip: "M3 Max", cpuCoreCount: 16, bandwidthGBps: 400, isMaximum: false),
|
|
.init(chip: "M3 Max", cpuCoreCount: 14, bandwidthGBps: 300, isMaximum: false),
|
|
.init(chip: "M3 Pro", cpuCoreCount: nil, bandwidthGBps: 150, isMaximum: false),
|
|
.init(chip: "M3", cpuCoreCount: nil, bandwidthGBps: 100, isMaximum: false),
|
|
.init(chip: "M2 Ultra", cpuCoreCount: nil, bandwidthGBps: 800, isMaximum: false),
|
|
.init(chip: "M2 Max", cpuCoreCount: nil, bandwidthGBps: 400, isMaximum: false),
|
|
.init(chip: "M2 Pro", cpuCoreCount: nil, bandwidthGBps: 200, isMaximum: false),
|
|
.init(chip: "M2", cpuCoreCount: nil, bandwidthGBps: 100, isMaximum: false),
|
|
.init(chip: "M1 Ultra", cpuCoreCount: nil, bandwidthGBps: 800, isMaximum: false),
|
|
.init(chip: "M1 Max", cpuCoreCount: nil, bandwidthGBps: 400, isMaximum: false),
|
|
.init(chip: "M1 Pro", cpuCoreCount: nil, bandwidthGBps: 200, isMaximum: false),
|
|
.init(chip: "M1", cpuCoreCount: nil, bandwidthGBps: 68.25, isMaximum: false)
|
|
]
|
|
|
|
static func specification(for processor: String, cpuCoreCount: Int = ProcessInfo.processInfo.activeProcessorCount) -> MacMemorySpecification? {
|
|
let normalized = processor.replacingOccurrences(of: "Apple ", with: "")
|
|
return specifications.first {
|
|
normalized == $0.chip && ($0.cpuCoreCount == nil || $0.cpuCoreCount == cpuCoreCount)
|
|
}
|
|
}
|
|
|
|
static func description(for processor: String, cpuCoreCount: Int = ProcessInfo.processInfo.activeProcessorCount) -> String {
|
|
specification(for: processor, cpuCoreCount: cpuCoreCount)?.displayValue ?? "Clock not published by macOS"
|
|
}
|
|
}
|
|
|
|
enum UpdateSpeed: String, CaseIterable, Identifiable {
|
|
case high = "High"
|
|
case normal = "Normal"
|
|
case low = "Low"
|
|
case paused = "Paused"
|
|
|
|
var id: String { rawValue }
|
|
var interval: Duration {
|
|
switch self {
|
|
case .high: .seconds(1)
|
|
case .normal: .seconds(2)
|
|
case .low: .seconds(5)
|
|
case .paused: .seconds(2)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ProcessRecord: Identifiable, Hashable, Sendable {
|
|
let pid: Int32
|
|
let parentPID: Int32
|
|
let name: String
|
|
let executablePath: String
|
|
let user: String
|
|
let cpu: Double
|
|
let memoryPercent: Double
|
|
let residentBytes: UInt64
|
|
let state: String
|
|
let elapsed: String
|
|
let cpuTime: TimeInterval
|
|
let threadCount: Int
|
|
let openFileCount: Int
|
|
let architecture: String
|
|
let priority: Int
|
|
let nice: Int
|
|
|
|
var id: Int32 { pid }
|
|
|
|
var displayName: String {
|
|
let cleaned = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return cleaned.isEmpty ? "Process \(pid)" : cleaned
|
|
}
|
|
}
|
|
|
|
struct AppUsageRecord: Identifiable, Codable, Hashable, Sendable {
|
|
let id: String
|
|
var name: String
|
|
var executablePath: String
|
|
var user: String
|
|
var cpuSeconds: TimeInterval
|
|
var networkReceivedBytes: UInt64
|
|
var networkSentBytes: UInt64
|
|
var lastSeen: Date
|
|
|
|
var networkTotalBytes: UInt64 { networkReceivedBytes + networkSentBytes }
|
|
}
|
|
|
|
struct ProcessActivityRate: Hashable, Sendable {
|
|
var diskRead = 0.0
|
|
var diskWrite = 0.0
|
|
var networkReceive = 0.0
|
|
var networkSend = 0.0
|
|
|
|
var diskTotal: Double { diskRead + diskWrite }
|
|
var networkTotal: Double { networkReceive + networkSend }
|
|
}
|
|
|
|
enum ServiceDomain: String, CaseIterable, Identifiable, Sendable {
|
|
case user = "User"
|
|
case system = "System"
|
|
|
|
var id: String { rawValue }
|
|
var launchctlPrefix: String { self == .system ? "system" : "gui/\(getuid())" }
|
|
}
|
|
|
|
struct ServiceRecord: Identifiable, Hashable, Sendable {
|
|
let label: String
|
|
let pid: Int32?
|
|
let lastExitStatus: Int32
|
|
let domain: ServiceDomain
|
|
|
|
var id: String { "\(domain.rawValue):\(label)" }
|
|
var isRunning: Bool { pid != nil }
|
|
var isControllable: Bool { domain == .user }
|
|
|
|
var configurationPath: String? {
|
|
let roots = domain == .system
|
|
? ["/Library/LaunchDaemons", "/System/Library/LaunchDaemons"]
|
|
: [NSHomeDirectory() + "/Library/LaunchAgents", "/Library/LaunchAgents", "/System/Library/LaunchAgents"]
|
|
return roots.map { "\($0)/\(label).plist" }.first { FileManager.default.fileExists(atPath: $0) }
|
|
}
|
|
|
|
var displayName: String {
|
|
let parts = label.split(separator: ".").map(String.init)
|
|
let meaningful = parts.filter { part in
|
|
let isNumber = part.allSatisfy(\.isNumber)
|
|
let isUUID = UUID(uuidString: part) != nil
|
|
return !isNumber && !isUUID
|
|
}
|
|
guard let last = meaningful.last else { return label }
|
|
let generic = ["agent", "helper", "service", "xpcservice", "app"]
|
|
if generic.contains(last.lowercased()), meaningful.count > 1 {
|
|
return "\(meaningful[meaningful.count - 2]) \(last)"
|
|
}
|
|
return last
|
|
}
|
|
|
|
var publisher: String {
|
|
let parts = label.split(separator: ".").map(String.init)
|
|
guard let first = parts.first else { return "Unknown" }
|
|
if label.hasPrefix("com.apple") { return "Apple" }
|
|
if first == "application", parts.count > 2 {
|
|
let vendorIndex = ["com", "org", "net", "io"].contains(parts[1].lowercased()) ? 2 : 1
|
|
let vendor = parts[vendorIndex]
|
|
return vendor.prefix(1).uppercased() + vendor.dropFirst()
|
|
}
|
|
if ["com", "org", "net", "io", "us"].contains(first.lowercased()), parts.count > 1 {
|
|
return parts[1].prefix(1).uppercased() + parts[1].dropFirst()
|
|
}
|
|
return first.prefix(1).uppercased() + first.dropFirst()
|
|
}
|
|
}
|
|
|
|
struct MetricSample: Identifiable {
|
|
let id = UUID()
|
|
let date: Date
|
|
let value: Double
|
|
}
|
|
|
|
struct VolumeSnapshot: Identifiable, Hashable, Sendable {
|
|
let id: String
|
|
let name: String
|
|
let mountPath: String
|
|
let fileSystem: String
|
|
let capacity: UInt64
|
|
let available: UInt64
|
|
let isEjectable: Bool
|
|
|
|
var used: UInt64 { capacity > available ? capacity - available : 0 }
|
|
var usedPercent: Double { capacity > 0 ? Double(used) / Double(capacity) * 100 : 0 }
|
|
}
|
|
|
|
struct SystemSnapshot {
|
|
var cpuPercent = 0.0
|
|
var memoryUsed: UInt64 = 0
|
|
var memoryTotal: UInt64 = ProcessInfo.processInfo.physicalMemory
|
|
var memoryActive: UInt64 = 0
|
|
var memoryWired: UInt64 = 0
|
|
var memoryCompressed: UInt64 = 0
|
|
var memoryCached: UInt64 = 0
|
|
var swapUsed: UInt64 = 0
|
|
var swapTotal: UInt64 = 0
|
|
var memoryType = "Unified"
|
|
var memorySpeed = "Not reported by macOS"
|
|
var memoryManufacturer = "Apple unified memory"
|
|
var diskFree: UInt64 = 0
|
|
var diskTotal: UInt64 = 0
|
|
var diskReadRate = 0.0
|
|
var diskWriteRate = 0.0
|
|
var diskOperationsPerSecond = 0.0
|
|
var diskLatencyMilliseconds = 0.0
|
|
var diskActivePercent = 0.0
|
|
var gpuPercent = 0.0
|
|
var gpuRendererPercent = 0.0
|
|
var gpuTilerPercent = 0.0
|
|
var gpuMemoryBytes: UInt64 = 0
|
|
var gpuAllocatedBytes: UInt64 = 0
|
|
var gpuCoreCount = 0
|
|
var processCount = 0
|
|
var threadCount = 0
|
|
var uptime: TimeInterval = ProcessInfo.processInfo.systemUptime
|
|
var corePercents: [Double] = []
|
|
var networkReceiveRate = 0.0
|
|
var networkSendRate = 0.0
|
|
var networkInterface = "Network"
|
|
var removableVolumes: [VolumeSnapshot] = []
|
|
|
|
var memoryPercent: Double {
|
|
guard memoryTotal > 0 else { return 0 }
|
|
return Double(memoryUsed) / Double(memoryTotal) * 100
|
|
}
|
|
|
|
var diskPercent: Double {
|
|
guard diskTotal > 0 else { return 0 }
|
|
return Double(diskTotal - diskFree) / Double(diskTotal) * 100
|
|
}
|
|
|
|
var diskThroughput: Double { diskReadRate + diskWriteRate }
|
|
}
|
|
|
|
@MainActor
|
|
enum Formatters {
|
|
static let bytes: ByteCountFormatter = {
|
|
let formatter = ByteCountFormatter()
|
|
formatter.countStyle = .memory
|
|
formatter.allowedUnits = [.useKB, .useMB, .useGB, .useTB]
|
|
return formatter
|
|
}()
|
|
|
|
static func percent(_ value: Double) -> String {
|
|
value < 10 ? String(format: "%.1f%%", value) : String(format: "%.0f%%", value)
|
|
}
|
|
|
|
static func uptime(_ interval: TimeInterval) -> String {
|
|
let total = Int(interval)
|
|
let days = total / 86_400
|
|
let hours = (total % 86_400) / 3_600
|
|
let minutes = (total % 3_600) / 60
|
|
return days > 0 ? "\(days)d \(hours)h \(minutes)m" : "\(hours)h \(minutes)m"
|
|
}
|
|
|
|
static func rate(_ bytesPerSecond: Double) -> String {
|
|
"\(bytes.string(fromByteCount: Int64(max(0, bytesPerSecond))))/s"
|
|
}
|
|
|
|
static func duration(_ interval: TimeInterval) -> String {
|
|
let total = max(0, Int(interval.rounded()))
|
|
let hours = total / 3_600
|
|
let minutes = (total % 3_600) / 60
|
|
let seconds = total % 60
|
|
return hours > 0
|
|
? String(format: "%d:%02d:%02d", hours, minutes, seconds)
|
|
: String(format: "%02d:%02d", minutes, seconds)
|
|
}
|
|
}
|