254 lines
7.8 KiB
Swift
254 lines
7.8 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 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 .settings: "gearshape"
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|