Files
puter/Sources/Puter/Models.swift
T

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)
}
}