Initial commit: TopManager macOS system monitor

A native SwiftUI app for monitoring macOS system resources:
- Processes tab with sorting, filtering, and process control
- Apps tab for running applications
- Performance tab with CPU, memory, and network charts
- Power & Storage tab with system info, GPU, storage, and network

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hariel1985
2026-01-30 23:56:51 +01:00
commit 41010541a7
48 fájl változott, egészen pontosan 5251 új sor hozzáadva és 0 régi sor törölve

22
TopManager/Info.plist Normal file
Fájl megtekintése

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

Fájl megtekintése

@@ -0,0 +1,64 @@
import Foundation
struct CPUInfo {
let globalUsage: Double
let userUsage: Double
let systemUsage: Double
let idleUsage: Double
let coreUsages: [CoreUsage]
let pCoreUsages: [CoreUsage]
let eCoreUsages: [CoreUsage]
let timestamp: Date
init(
globalUsage: Double,
userUsage: Double,
systemUsage: Double,
idleUsage: Double,
coreUsages: [CoreUsage],
pCoreUsages: [CoreUsage] = [],
eCoreUsages: [CoreUsage] = [],
timestamp: Date = Date()
) {
self.globalUsage = globalUsage
self.userUsage = userUsage
self.systemUsage = systemUsage
self.idleUsage = idleUsage
self.coreUsages = coreUsages
self.pCoreUsages = pCoreUsages
self.eCoreUsages = eCoreUsages
self.timestamp = timestamp
}
}
struct CoreUsage: Identifiable {
let id: Int
let usage: Double
let coreType: CoreType
enum CoreType: String {
case performance = "P"
case efficiency = "E"
case unknown = ""
}
init(id: Int, usage: Double, coreType: CoreType = .unknown) {
self.id = id
self.usage = usage
self.coreType = coreType
}
}
struct CPUHistoryPoint: Identifiable {
let id = UUID()
let timestamp: Date
let usage: Double
let userUsage: Double
let systemUsage: Double
}
struct CoreHistoryPoint: Identifiable {
let id = UUID()
let timestamp: Date
let usage: Double
}

Fájl megtekintése

@@ -0,0 +1,57 @@
import Foundation
struct DiskInfo {
let volumes: [VolumeInfo]
let timestamp: Date
init(volumes: [VolumeInfo], timestamp: Date = Date()) {
self.volumes = volumes
self.timestamp = timestamp
}
}
struct VolumeInfo: Identifiable {
let id: String
let name: String
let mountPoint: String
let totalSpace: UInt64
let freeSpace: UInt64
let usedSpace: UInt64
let fileSystem: String
let isRemovable: Bool
let isInternal: Bool
var usagePercentage: Double {
guard totalSpace > 0 else { return 0 }
return Double(usedSpace) / Double(totalSpace) * 100
}
init(
name: String,
mountPoint: String,
totalSpace: UInt64,
freeSpace: UInt64,
fileSystem: String,
isRemovable: Bool,
isInternal: Bool
) {
self.id = mountPoint
self.name = name
self.mountPoint = mountPoint
self.totalSpace = totalSpace
self.freeSpace = freeSpace
self.usedSpace = totalSpace > freeSpace ? totalSpace - freeSpace : 0
self.fileSystem = fileSystem
self.isRemovable = isRemovable
self.isInternal = isInternal
}
}
struct DiskIOStats: Identifiable {
let id = UUID()
let timestamp: Date
let readBytes: UInt64
let writeBytes: UInt64
let readRate: Double
let writeRate: Double
}

Fájl megtekintése

@@ -0,0 +1,45 @@
import Foundation
struct GPUInfo {
let name: String
let vramUsed: UInt64
let vramTotal: UInt64?
let utilizationPercent: Double?
let isUnifiedMemory: Bool
let coreCount: Int?
let timestamp: Date
var vramUsagePercentage: Double? {
guard let total = vramTotal, total > 0 else { return nil }
return Double(vramUsed) / Double(total) * 100
}
var memoryLabel: String {
isUnifiedMemory ? "GPU Memory" : "VRAM"
}
init(
name: String,
vramUsed: UInt64,
vramTotal: UInt64?,
utilizationPercent: Double?,
isUnifiedMemory: Bool = false,
coreCount: Int? = nil,
timestamp: Date = Date()
) {
self.name = name
self.vramUsed = vramUsed
self.vramTotal = vramTotal
self.utilizationPercent = utilizationPercent
self.isUnifiedMemory = isUnifiedMemory
self.coreCount = coreCount
self.timestamp = timestamp
}
}
struct GPUHistoryPoint: Identifiable {
let id = UUID()
let timestamp: Date
let vramUsed: UInt64
let utilizationPercent: Double?
}

Fájl megtekintése

@@ -0,0 +1,81 @@
import Foundation
struct MemoryInfo {
let totalMemory: UInt64
let usedMemory: UInt64
let freeMemory: UInt64
let activeMemory: UInt64
let inactiveMemory: UInt64
let wiredMemory: UInt64
let compressedMemory: UInt64
let cachedMemory: UInt64
let swapUsed: UInt64
let swapTotal: UInt64
let memoryPressure: MemoryPressure
let timestamp: Date
var usagePercentage: Double {
guard totalMemory > 0 else { return 0 }
return Double(usedMemory) / Double(totalMemory) * 100
}
var appMemory: UInt64 {
activeMemory + inactiveMemory
}
init(
totalMemory: UInt64,
usedMemory: UInt64,
freeMemory: UInt64,
activeMemory: UInt64,
inactiveMemory: UInt64,
wiredMemory: UInt64,
compressedMemory: UInt64,
cachedMemory: UInt64,
swapUsed: UInt64,
swapTotal: UInt64,
memoryPressure: MemoryPressure,
timestamp: Date = Date()
) {
self.totalMemory = totalMemory
self.usedMemory = usedMemory
self.freeMemory = freeMemory
self.activeMemory = activeMemory
self.inactiveMemory = inactiveMemory
self.wiredMemory = wiredMemory
self.compressedMemory = compressedMemory
self.cachedMemory = cachedMemory
self.swapUsed = swapUsed
self.swapTotal = swapTotal
self.memoryPressure = memoryPressure
self.timestamp = timestamp
}
}
enum MemoryPressure: String {
case nominal = "Normal"
case warning = "Warning"
case critical = "Critical"
case unknown = "Unknown"
var color: String {
switch self {
case .nominal: return "green"
case .warning: return "yellow"
case .critical: return "red"
case .unknown: return "gray"
}
}
}
struct MemoryHistoryPoint: Identifiable {
let id = UUID()
let timestamp: Date
let usedMemory: UInt64
let totalMemory: UInt64
var usagePercentage: Double {
guard totalMemory > 0 else { return 0 }
return Double(usedMemory) / Double(totalMemory) * 100
}
}

Fájl megtekintése

@@ -0,0 +1,63 @@
import Foundation
struct NetworkInfo {
let interfaces: [NetworkInterface]
let totalDownloadRate: Double
let totalUploadRate: Double
let totalDownloadBytes: UInt64
let totalUploadBytes: UInt64
let timestamp: Date
init(
interfaces: [NetworkInterface],
totalDownloadRate: Double,
totalUploadRate: Double,
totalDownloadBytes: UInt64,
totalUploadBytes: UInt64,
timestamp: Date = Date()
) {
self.interfaces = interfaces
self.totalDownloadRate = totalDownloadRate
self.totalUploadRate = totalUploadRate
self.totalDownloadBytes = totalDownloadBytes
self.totalUploadBytes = totalUploadBytes
self.timestamp = timestamp
}
}
struct NetworkInterface: Identifiable {
let id: String
let name: String
let displayName: String
let downloadBytes: UInt64
let uploadBytes: UInt64
let downloadRate: Double
let uploadRate: Double
let isActive: Bool
init(
name: String,
displayName: String,
downloadBytes: UInt64,
uploadBytes: UInt64,
downloadRate: Double,
uploadRate: Double,
isActive: Bool
) {
self.id = name
self.name = name
self.displayName = displayName
self.downloadBytes = downloadBytes
self.uploadBytes = uploadBytes
self.downloadRate = downloadRate
self.uploadRate = uploadRate
self.isActive = isActive
}
}
struct NetworkHistoryPoint: Identifiable {
let id = UUID()
let timestamp: Date
let downloadRate: Double
let uploadRate: Double
}

Fájl megtekintése

@@ -0,0 +1,73 @@
import Foundation
import AppKit
struct ProcessItem: Identifiable, Hashable {
let id: pid_t
let pid: pid_t
let name: String
let user: String
let cpuUsage: Double
let memoryUsage: Int64
let threadCount: Int32
let state: ProcessState
let icon: NSImage?
let parentPid: pid_t
let startTime: Date?
var iconPlaceholder: String { "" }
init(
pid: pid_t,
name: String,
user: String,
cpuUsage: Double,
memoryUsage: Int64,
threadCount: Int32,
state: ProcessState,
icon: NSImage?,
parentPid: pid_t,
startTime: Date?
) {
self.id = pid
self.pid = pid
self.name = name
self.user = user
self.cpuUsage = cpuUsage
self.memoryUsage = memoryUsage
self.threadCount = threadCount
self.state = state
self.icon = icon
self.parentPid = parentPid
self.startTime = startTime
}
func hash(into hasher: inout Hasher) {
hasher.combine(pid)
}
static func == (lhs: ProcessItem, rhs: ProcessItem) -> Bool {
lhs.pid == rhs.pid &&
lhs.cpuUsage == rhs.cpuUsage &&
lhs.memoryUsage == rhs.memoryUsage &&
lhs.threadCount == rhs.threadCount &&
lhs.state == rhs.state
}
}
enum ProcessState: String {
case running = "Running"
case sleeping = "Sleeping"
case stopped = "Stopped"
case zombie = "Zombie"
case unknown = "Unknown"
var symbol: String {
switch self {
case .running: return "play.circle.fill"
case .sleeping: return "moon.fill"
case .stopped: return "pause.circle.fill"
case .zombie: return "exclamationmark.triangle.fill"
case .unknown: return "questionmark.circle"
}
}
}

Fájl megtekintése

@@ -0,0 +1,68 @@
{
"images" : [
{
"filename" : "icon_16x16.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "16x16"
},
{
"filename" : "icon_16x16@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "16x16"
},
{
"filename" : "icon_32x32.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "32x32"
},
{
"filename" : "icon_32x32@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "32x32"
},
{
"filename" : "icon_128x128.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "128x128"
},
{
"filename" : "icon_128x128@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "128x128"
},
{
"filename" : "icon_256x256.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "256x256"
},
{
"filename" : "icon_256x256@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "256x256"
},
{
"filename" : "icon_512x512.png",
"idiom" : "mac",
"scale" : "1x",
"size" : "512x512"
},
{
"filename" : "icon_512x512@2x.png",
"idiom" : "mac",
"scale" : "2x",
"size" : "512x512"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Méret: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Méret: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Méret: 969 B

Binary file not shown.

After

Width:  |  Height:  |  Méret: 969 B

Binary file not shown.

After

Width:  |  Height:  |  Méret: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Méret: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Méret: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Méret: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Méret: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Méret: 337 KiB

Fájl megtekintése

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

Fájl megtekintése

@@ -0,0 +1,180 @@
import Foundation
import Darwin
final class CPUMonitor {
private var previousCpuInfo: [processor_cpu_load_info]?
private var previousGlobalInfo: host_cpu_load_info?
private var pCoreCount: Int = 0
private var eCoreCount: Int = 0
private var isAppleSiliconCPU: Bool = false
init() {
detectCPUArchitecture()
}
private func detectCPUArchitecture() {
var sysinfo = utsname()
uname(&sysinfo)
let machine = withUnsafePointer(to: &sysinfo.machine) {
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
String(cString: $0)
}
}
isAppleSiliconCPU = machine.hasPrefix("arm64")
if isAppleSiliconCPU {
detectPerfLevelCounts()
}
}
private func detectPerfLevelCounts() {
var pCores: Int32 = 0
var eCores: Int32 = 0
var size = MemoryLayout<Int32>.size
if sysctlbyname("hw.perflevel1.logicalcpu", &pCores, &size, nil, 0) == 0,
sysctlbyname("hw.perflevel0.logicalcpu", &eCores, &size, nil, 0) == 0 {
self.pCoreCount = Int(pCores)
self.eCoreCount = Int(eCores)
}
}
func fetchCPUInfo() -> CPUInfo? {
guard let perCoreUsages = fetchPerCoreUsages(),
let globalUsage = fetchGlobalUsage() else {
return nil
}
var pCoreUsages: [CoreUsage] = []
var eCoreUsages: [CoreUsage] = []
if isAppleSiliconCPU && pCoreCount > 0 && eCoreCount > 0 {
for core in perCoreUsages {
if core.id < pCoreCount {
pCoreUsages.append(CoreUsage(id: core.id, usage: core.usage, coreType: .performance))
} else {
eCoreUsages.append(CoreUsage(id: core.id, usage: core.usage, coreType: .efficiency))
}
}
}
return CPUInfo(
globalUsage: globalUsage.usage,
userUsage: globalUsage.user,
systemUsage: globalUsage.system,
idleUsage: globalUsage.idle,
coreUsages: perCoreUsages,
pCoreUsages: pCoreUsages,
eCoreUsages: eCoreUsages
)
}
private func fetchPerCoreUsages() -> [CoreUsage]? {
var numCPUs: natural_t = 0
var cpuInfo: processor_info_array_t?
var numCpuInfo: mach_msg_type_number_t = 0
let result = host_processor_info(
mach_host_self(),
PROCESSOR_CPU_LOAD_INFO,
&numCPUs,
&cpuInfo,
&numCpuInfo
)
guard result == KERN_SUCCESS, let cpuInfo = cpuInfo else {
return nil
}
defer {
vm_deallocate(
mach_task_self_,
vm_address_t(bitPattern: cpuInfo),
vm_size_t(Int(numCpuInfo) * MemoryLayout<integer_t>.size)
)
}
var coreUsages: [CoreUsage] = []
let cpuLoadInfo = cpuInfo.withMemoryRebound(to: processor_cpu_load_info.self, capacity: Int(numCPUs)) { ptr in
Array(UnsafeBufferPointer(start: ptr, count: Int(numCPUs)))
}
for (index, currentInfo) in cpuLoadInfo.enumerated() {
var usage = 0.0
if let previous = previousCpuInfo, index < previous.count {
let prevInfo = previous[index]
let userDelta = Int64(currentInfo.cpu_ticks.0) - Int64(prevInfo.cpu_ticks.0)
let systemDelta = Int64(currentInfo.cpu_ticks.1) - Int64(prevInfo.cpu_ticks.1)
let idleDelta = Int64(currentInfo.cpu_ticks.2) - Int64(prevInfo.cpu_ticks.2)
let niceDelta = Int64(currentInfo.cpu_ticks.3) - Int64(prevInfo.cpu_ticks.3)
let totalDelta = userDelta + systemDelta + idleDelta + niceDelta
if totalDelta > 0 {
usage = Double(userDelta + systemDelta) / Double(totalDelta) * 100
}
}
let coreType: CoreUsage.CoreType
if isAppleSiliconCPU && pCoreCount > 0 {
coreType = index < pCoreCount ? .performance : .efficiency
} else {
coreType = .unknown
}
coreUsages.append(CoreUsage(id: index, usage: usage, coreType: coreType))
}
previousCpuInfo = cpuLoadInfo
return coreUsages
}
private func fetchGlobalUsage() -> (usage: Double, user: Double, system: Double, idle: Double)? {
var size = mach_msg_type_number_t(MemoryLayout<host_cpu_load_info_data_t>.size / MemoryLayout<integer_t>.size)
var cpuLoadInfo = host_cpu_load_info_data_t()
let result = withUnsafeMutablePointer(to: &cpuLoadInfo) { ptr in
ptr.withMemoryRebound(to: integer_t.self, capacity: Int(size)) { intPtr in
host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, intPtr, &size)
}
}
guard result == KERN_SUCCESS else { return nil }
var usage = 0.0
var userPct = 0.0
var systemPct = 0.0
var idlePct = 0.0
if let previous = previousGlobalInfo {
let userDelta = Int64(cpuLoadInfo.cpu_ticks.0) - Int64(previous.cpu_ticks.0)
let systemDelta = Int64(cpuLoadInfo.cpu_ticks.1) - Int64(previous.cpu_ticks.1)
let idleDelta = Int64(cpuLoadInfo.cpu_ticks.2) - Int64(previous.cpu_ticks.2)
let niceDelta = Int64(cpuLoadInfo.cpu_ticks.3) - Int64(previous.cpu_ticks.3)
let totalDelta = userDelta + systemDelta + idleDelta + niceDelta
if totalDelta > 0 {
userPct = Double(userDelta + niceDelta) / Double(totalDelta) * 100
systemPct = Double(systemDelta) / Double(totalDelta) * 100
idlePct = Double(idleDelta) / Double(totalDelta) * 100
usage = userPct + systemPct
}
}
previousGlobalInfo = cpuLoadInfo
return (usage, userPct, systemPct, idlePct)
}
var isAppleSilicon: Bool {
isAppleSiliconCPU
}
var performanceCoreCount: Int {
pCoreCount
}
var efficiencyCoreCount: Int {
eCoreCount
}
}

Fájl megtekintése

@@ -0,0 +1,80 @@
import Foundation
final class DiskMonitor {
func fetchDiskInfo() -> DiskInfo {
let volumes = fetchVolumes()
return DiskInfo(volumes: volumes)
}
private func fetchVolumes() -> [VolumeInfo] {
var volumes: [VolumeInfo] = []
let fileManager = FileManager.default
guard let mountedVolumeURLs = fileManager.mountedVolumeURLs(
includingResourceValuesForKeys: [
.volumeNameKey,
.volumeTotalCapacityKey,
.volumeAvailableCapacityKey,
.volumeIsRemovableKey,
.volumeIsInternalKey,
.volumeLocalizedFormatDescriptionKey
],
options: [.skipHiddenVolumes]
) else {
return []
}
for volumeURL in mountedVolumeURLs {
do {
let resourceValues = try volumeURL.resourceValues(forKeys: [
.volumeNameKey,
.volumeTotalCapacityKey,
.volumeAvailableCapacityKey,
.volumeIsRemovableKey,
.volumeIsInternalKey,
.volumeLocalizedFormatDescriptionKey
])
let name = resourceValues.volumeName ?? volumeURL.lastPathComponent
let totalSpace = UInt64(resourceValues.volumeTotalCapacity ?? 0)
let freeSpace = UInt64(resourceValues.volumeAvailableCapacity ?? 0)
let isRemovable = resourceValues.volumeIsRemovable ?? false
let isInternal = resourceValues.volumeIsInternal ?? true
let fileSystem = resourceValues.volumeLocalizedFormatDescription ?? "Unknown"
let mountPoint = volumeURL.path
// Skip system volumes
guard shouldIncludeVolume(mountPoint: mountPoint) else {
continue
}
volumes.append(VolumeInfo(
name: name,
mountPoint: mountPoint,
totalSpace: totalSpace,
freeSpace: freeSpace,
fileSystem: fileSystem,
isRemovable: isRemovable,
isInternal: isInternal
))
} catch {
continue
}
}
return volumes.sorted { $0.mountPoint < $1.mountPoint }
}
private func shouldIncludeVolume(mountPoint: String) -> Bool {
let excludedPaths = [
"/System/Volumes/VM",
"/System/Volumes/Preboot",
"/System/Volumes/Update",
"/System/Volumes/xarts",
"/System/Volumes/iSCPreboot",
"/System/Volumes/Hardware"
]
return !excludedPaths.contains(where: { mountPoint.hasPrefix($0) })
}
}

Fájl megtekintése

@@ -0,0 +1,253 @@
import Foundation
import IOKit
import Metal
final class GPUMonitor {
private let isAppleSilicon: Bool
private let chipName: String
private let gpuCoreCount: Int?
init() {
var sysinfo = utsname()
uname(&sysinfo)
let machine = withUnsafePointer(to: &sysinfo.machine) {
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
String(cString: $0)
}
}
isAppleSilicon = machine.hasPrefix("arm64")
chipName = GPUMonitor.getChipName()
gpuCoreCount = GPUMonitor.getGPUCoreCount()
}
private static func getGPUCoreCount() -> Int? {
// Try to get GPU core count from Metal
guard let device = MTLCreateSystemDefaultDevice() else { return nil }
// For Apple Silicon, we can infer from the chip name or use Metal properties
// Metal doesn't directly expose core count, but we can check recommended working set size
// as a proxy or use the chip name to look up known values
// Try IOKit for more detailed info
let matchingDict = IOServiceMatching("AGXAccelerator")
var iterator: io_iterator_t = 0
if IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &iterator) == KERN_SUCCESS {
defer { IOObjectRelease(iterator) }
let service = IOIteratorNext(iterator)
if service != 0 {
defer { IOObjectRelease(service) }
if let props = IORegistryEntryCreateCFProperty(
service,
"gpu-core-count" as CFString,
kCFAllocatorDefault,
0
)?.takeRetainedValue() as? Int {
return props
}
}
}
// Fallback: infer from device name
let name = device.name.lowercased()
if name.contains("m3 max") { return 40 }
if name.contains("m3 pro") { return 18 }
if name.contains("m3") { return 10 }
if name.contains("m2 ultra") { return 76 }
if name.contains("m2 max") { return 38 }
if name.contains("m2 pro") { return 19 }
if name.contains("m2") { return 10 }
if name.contains("m1 ultra") { return 64 }
if name.contains("m1 max") { return 32 }
if name.contains("m1 pro") { return 16 }
if name.contains("m1") { return 8 }
return nil
}
private static func getChipName() -> String {
var size: size_t = 0
sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0)
var brand = [CChar](repeating: 0, count: size)
sysctlbyname("machdep.cpu.brand_string", &brand, &size, nil, 0)
let brandString = String(cString: brand)
// For Apple Silicon, this returns "Apple M1", "Apple M2", etc.
if brandString.hasPrefix("Apple") {
return brandString
}
// Fallback: try to get chip from IOKit
return getAppleSiliconChipName() ?? "Apple GPU"
}
private static func getAppleSiliconChipName() -> String? {
let matchingDict = IOServiceMatching("IOPlatformExpertDevice")
var service: io_service_t = 0
guard IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &service) == KERN_SUCCESS else {
return nil
}
let platformExpert = IOServiceGetMatchingService(kIOMainPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
defer { IOObjectRelease(platformExpert) }
if let modelData = IORegistryEntryCreateCFProperty(
platformExpert,
"model" as CFString,
kCFAllocatorDefault,
0
)?.takeRetainedValue() as? Data {
let model = String(data: modelData, encoding: .utf8)?.trimmingCharacters(in: .controlCharacters) ?? ""
// Map model identifiers to chip names
if model.contains("Mac14") || model.contains("Mac15") {
return "Apple M2/M3"
} else if model.contains("Mac13") {
return "Apple M2"
} else if model.contains("Mac12") || model.contains("MacBookAir10") || model.contains("MacBookPro17") || model.contains("MacBookPro18") || model.contains("Macmini9") || model.contains("iMac21") {
return "Apple M1"
}
}
return nil
}
func fetchGPUInfo() -> GPUInfo? {
if isAppleSilicon {
return fetchAppleSiliconGPUInfo()
} else {
return fetchDiscreteGPUInfo()
}
}
private func fetchAppleSiliconGPUInfo() -> GPUInfo? {
// Apple Silicon uses unified memory - get memory pressure instead
let totalMemory = ProcessInfo.processInfo.physicalMemory
// Try to get GPU utilization from IOKit
var utilizationPercent: Double? = nil
var inUseSystemMemory: UInt64 = 0
let matchingDict = IOServiceMatching("IOAccelerator")
var iterator: io_iterator_t = 0
if IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &iterator) == KERN_SUCCESS {
defer { IOObjectRelease(iterator) }
var service = IOIteratorNext(iterator)
while service != 0 {
defer {
IOObjectRelease(service)
service = IOIteratorNext(iterator)
}
if let props = IORegistryEntryCreateCFProperty(
service,
"PerformanceStatistics" as CFString,
kCFAllocatorDefault,
0
)?.takeRetainedValue() as? [String: Any] {
// Try various keys for GPU memory on Apple Silicon
if let inUse = props["In use system memory"] as? UInt64 {
inUseSystemMemory = inUse
} else if let inUse = props["Alloc system memory"] as? UInt64 {
inUseSystemMemory = inUse
} else if let inUse = props["inUseSystemMemory"] as? UInt64 {
inUseSystemMemory = inUse
}
// GPU utilization
if let util = props["Device Utilization %"] as? Int {
utilizationPercent = Double(util)
} else if let util = props["GPU Activity(%)"] as? Int {
utilizationPercent = Double(util)
}
break
}
}
}
// For Apple Silicon, show unified memory usage
return GPUInfo(
name: chipName + " GPU",
vramUsed: inUseSystemMemory,
vramTotal: inUseSystemMemory > 0 ? totalMemory : nil,
utilizationPercent: utilizationPercent,
isUnifiedMemory: true,
coreCount: gpuCoreCount
)
}
private func fetchDiscreteGPUInfo() -> GPUInfo? {
let matchingDict = IOServiceMatching("IOAccelerator")
var iterator: io_iterator_t = 0
guard IOServiceGetMatchingServices(kIOMainPortDefault, matchingDict, &iterator) == KERN_SUCCESS else {
return nil
}
defer { IOObjectRelease(iterator) }
var service = IOIteratorNext(iterator)
while service != 0 {
defer {
IOObjectRelease(service)
service = IOIteratorNext(iterator)
}
var name = "GPU"
if let modelData = IORegistryEntryCreateCFProperty(
service,
"model" as CFString,
kCFAllocatorDefault,
0
)?.takeRetainedValue() as? Data {
name = String(data: modelData, encoding: .utf8)?
.trimmingCharacters(in: .controlCharacters) ?? "GPU"
}
if let props = IORegistryEntryCreateCFProperty(
service,
"PerformanceStatistics" as CFString,
kCFAllocatorDefault,
0
)?.takeRetainedValue() as? [String: Any] {
let vramUsed = (props["vramUsedBytes"] as? UInt64)
?? (props["VRAM,totalMB"] as? UInt64).map { $0 * 1024 * 1024 }
?? (props["inUseVidMemoryBytes"] as? UInt64)
?? 0
let vramFree = props["vramFreeBytes"] as? UInt64
let vramTotal: UInt64?
if let free = vramFree {
vramTotal = vramUsed + free
} else if let totalMB = props["VRAM,totalMB"] as? UInt64 {
vramTotal = totalMB * 1024 * 1024
} else {
vramTotal = nil
}
let utilization = props["Device Utilization %"] as? Double
?? props["GPU Core Utilization"] as? Double
return GPUInfo(
name: name,
vramUsed: vramUsed,
vramTotal: vramTotal,
utilizationPercent: utilization,
isUnifiedMemory: false
)
}
}
return nil
}
}

Fájl megtekintése

@@ -0,0 +1,97 @@
import Foundation
import Darwin
final class MemoryMonitor {
private let pageSize: UInt64
init() {
var pageSizeValue: vm_size_t = 0
host_page_size(mach_host_self(), &pageSizeValue)
pageSize = UInt64(pageSizeValue)
}
func fetchMemoryInfo() -> MemoryInfo? {
guard let vmStats = fetchVMStatistics() else { return nil }
let totalMemory = ProcessInfo.processInfo.physicalMemory
let freeMemory = vmStats.freePages * pageSize
let activeMemory = vmStats.activePages * pageSize
let inactiveMemory = vmStats.inactivePages * pageSize
let wiredMemory = vmStats.wiredPages * pageSize
let compressedMemory = vmStats.compressedPages * pageSize
let cachedMemory = vmStats.cachedPages * pageSize
let usedMemory = activeMemory + wiredMemory + compressedMemory
let swapInfo = fetchSwapInfo()
let memoryPressure = fetchMemoryPressure()
return MemoryInfo(
totalMemory: totalMemory,
usedMemory: usedMemory,
freeMemory: freeMemory,
activeMemory: activeMemory,
inactiveMemory: inactiveMemory,
wiredMemory: wiredMemory,
compressedMemory: compressedMemory,
cachedMemory: cachedMemory,
swapUsed: swapInfo.used,
swapTotal: swapInfo.total,
memoryPressure: memoryPressure
)
}
private func fetchVMStatistics() -> (
freePages: UInt64,
activePages: UInt64,
inactivePages: UInt64,
wiredPages: UInt64,
compressedPages: UInt64,
cachedPages: UInt64
)? {
var stats = vm_statistics64_data_t()
var count = mach_msg_type_number_t(MemoryLayout<vm_statistics64_data_t>.size / MemoryLayout<integer_t>.size)
let result = withUnsafeMutablePointer(to: &stats) { ptr in
ptr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { intPtr in
host_statistics64(mach_host_self(), HOST_VM_INFO64, intPtr, &count)
}
}
guard result == KERN_SUCCESS else { return nil }
return (
freePages: UInt64(stats.free_count),
activePages: UInt64(stats.active_count),
inactivePages: UInt64(stats.inactive_count),
wiredPages: UInt64(stats.wire_count),
compressedPages: UInt64(stats.compressor_page_count),
cachedPages: UInt64(stats.external_page_count)
)
}
private func fetchSwapInfo() -> (used: UInt64, total: UInt64) {
var swapUsage = xsw_usage()
var size = MemoryLayout<xsw_usage>.size
if sysctlbyname("vm.swapusage", &swapUsage, &size, nil, 0) == 0 {
return (UInt64(swapUsage.xsu_used), UInt64(swapUsage.xsu_total))
}
return (0, 0)
}
private func fetchMemoryPressure() -> MemoryPressure {
var pressure: Int32 = 0
var size = MemoryLayout<Int32>.size
if sysctlbyname("kern.memorystatus_vm_pressure_level", &pressure, &size, nil, 0) == 0 {
switch pressure {
case 1: return .nominal
case 2: return .warning
case 4: return .critical
default: return .nominal
}
}
return .unknown
}
}

Fájl megtekintése

@@ -0,0 +1,124 @@
import Foundation
import Darwin
final class NetworkMonitor {
private var previousStats: [String: (download: UInt64, upload: UInt64, timestamp: Date)] = [:]
func fetchNetworkInfo() -> NetworkInfo {
let interfaces = fetchInterfaces()
let totalDownload = interfaces.reduce(0.0) { $0 + $1.downloadRate }
let totalUpload = interfaces.reduce(0.0) { $0 + $1.uploadRate }
let totalDownloadBytes = interfaces.reduce(UInt64(0)) { $0 + $1.downloadBytes }
let totalUploadBytes = interfaces.reduce(UInt64(0)) { $0 + $1.uploadBytes }
return NetworkInfo(
interfaces: interfaces,
totalDownloadRate: totalDownload,
totalUploadRate: totalUpload,
totalDownloadBytes: totalDownloadBytes,
totalUploadBytes: totalUploadBytes
)
}
private func fetchInterfaces() -> [NetworkInterface] {
var ifaddr: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&ifaddr) == 0, let firstAddr = ifaddr else {
return []
}
defer { freeifaddrs(ifaddr) }
var interfaceStats: [String: (download: UInt64, upload: UInt64)] = [:]
var currentAddr: UnsafeMutablePointer<ifaddrs>? = firstAddr
while let addr = currentAddr {
let interface = addr.pointee
if interface.ifa_addr.pointee.sa_family == UInt8(AF_LINK) {
let name = String(cString: interface.ifa_name)
if shouldIncludeInterface(name: name),
let data = interface.ifa_data {
let networkData = data.assumingMemoryBound(to: if_data.self).pointee
let download = UInt64(networkData.ifi_ibytes)
let upload = UInt64(networkData.ifi_obytes)
if let existing = interfaceStats[name] {
interfaceStats[name] = (
download: existing.download + download,
upload: existing.upload + upload
)
} else {
interfaceStats[name] = (download: download, upload: upload)
}
}
}
currentAddr = interface.ifa_next
}
let now = Date()
var interfaces: [NetworkInterface] = []
for (name, stats) in interfaceStats {
var downloadRate: Double = 0
var uploadRate: Double = 0
if let previous = previousStats[name] {
let timeDelta = now.timeIntervalSince(previous.timestamp)
if timeDelta > 0 {
// Handle counter reset/overflow: only compute rate if current >= previous
if stats.download >= previous.download {
downloadRate = Double(stats.download - previous.download) / timeDelta
}
if stats.upload >= previous.upload {
uploadRate = Double(stats.upload - previous.upload) / timeDelta
}
}
}
previousStats[name] = (stats.download, stats.upload, now)
let isActive = downloadRate > 0 || uploadRate > 0 || stats.download > 0 || stats.upload > 0
interfaces.append(NetworkInterface(
name: name,
displayName: displayName(for: name),
downloadBytes: stats.download,
uploadBytes: stats.upload,
downloadRate: downloadRate,
uploadRate: uploadRate,
isActive: isActive
))
}
return interfaces.sorted { $0.name < $1.name }
}
private func shouldIncludeInterface(name: String) -> Bool {
// Include common physical interfaces
let includedPrefixes = ["en", "bridge", "awdl", "llw", "utun"]
return includedPrefixes.contains { name.hasPrefix($0) }
}
private func displayName(for interface: String) -> String {
if interface.hasPrefix("en") {
if interface == "en0" {
return "Wi-Fi / Ethernet"
}
return "Ethernet \(interface)"
} else if interface.hasPrefix("bridge") {
return "Bridge"
} else if interface.hasPrefix("awdl") {
return "AirDrop"
} else if interface.hasPrefix("llw") {
return "Low Latency WLAN"
} else if interface.hasPrefix("utun") {
return "VPN Tunnel"
}
return interface
}
}

Fájl megtekintése

@@ -0,0 +1,296 @@
import Foundation
import Darwin
import AppKit
final class ProcessMonitor {
private var previousCPUTimes: [pid_t: (user: UInt64, system: UInt64, timestamp: Date)] = [:]
private let iconCache = NSCache<NSNumber, NSImage>()
private var nameCache: [pid_t: String] = [:]
private var userCache: [uid_t: String] = [:]
private let timebaseInfo: mach_timebase_info_data_t
init() {
var info = mach_timebase_info_data_t()
mach_timebase_info(&info)
self.timebaseInfo = info
}
func fetchProcesses() -> [ProcessItem] {
var pids = [pid_t](repeating: 0, count: 2048)
let bytesUsed = proc_listpids(UInt32(PROC_ALL_PIDS), 0, &pids, Int32(pids.count * MemoryLayout<pid_t>.size))
guard bytesUsed > 0 else { return [] }
let pidCount = Int(bytesUsed) / MemoryLayout<pid_t>.size
var processes: [ProcessItem] = []
var currentPids = Set<pid_t>()
for i in 0..<pidCount {
let pid = pids[i]
guard pid > 0 else { continue }
currentPids.insert(pid)
if let process = fetchProcessInfo(pid: pid) {
processes.append(process)
}
}
// Clean up caches for terminated processes
let stalePids = Set(nameCache.keys).subtracting(currentPids)
for pid in stalePids {
nameCache.removeValue(forKey: pid)
iconCache.removeObject(forKey: NSNumber(value: pid))
previousCPUTimes.removeValue(forKey: pid)
}
return processes
}
private func fetchProcessInfo(pid: pid_t) -> ProcessItem? {
var bsdInfo = proc_bsdinfo()
let bsdInfoSize = Int32(MemoryLayout<proc_bsdinfo>.size)
let bsdResult = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsdInfo, bsdInfoSize)
// If proc_pidinfo fails, use sysctl to get basic info (works for processes we don't own)
if bsdResult != bsdInfoSize {
return fetchBasicProcessInfo(pid: pid)
}
var taskInfo = proc_taskinfo()
let taskInfoSize = Int32(MemoryLayout<proc_taskinfo>.size)
let taskResult = proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, taskInfoSize)
let hasTaskInfo = taskResult == taskInfoSize
let name = fetchProcessName(pid: pid, bsdInfo: bsdInfo)
let user = fetchUsername(uid: bsdInfo.pbi_uid)
let parentPid = pid_t(bsdInfo.pbi_ppid)
let startTime = Date(timeIntervalSince1970: TimeInterval(bsdInfo.pbi_start_tvsec))
// For processes without task info, use defaults
let memoryUsage: Int64
let threadCount: Int32
let cpuUsage: Double
if hasTaskInfo {
let rusageData = fetchRusageData(pid: pid)
memoryUsage = rusageData.memory
threadCount = taskInfo.pti_threadnum
cpuUsage = calculateCPUUsage(
pid: pid,
userTime: rusageData.userTime,
systemTime: rusageData.systemTime
)
} else {
memoryUsage = 0
threadCount = 0
cpuUsage = 0
}
let state = determineProcessState(status: bsdInfo.pbi_status, cpuUsage: cpuUsage)
let icon = fetchIcon(pid: pid)
return ProcessItem(
pid: pid,
name: name,
user: user,
cpuUsage: cpuUsage,
memoryUsage: memoryUsage,
threadCount: threadCount,
state: state,
icon: icon,
parentPid: parentPid,
startTime: startTime
)
}
private func fetchProcessName(pid: pid_t, bsdInfo: proc_bsdinfo) -> String {
// Check cache first
if let cached = nameCache[pid] {
return cached
}
// PROC_PIDPATHINFO_MAXSIZE is 4 * MAXPATHLEN = 4096
var pathBuffer = [CChar](repeating: 0, count: 4096)
let pathLength = proc_pidpath(pid, &pathBuffer, UInt32(pathBuffer.count))
let name: String
if pathLength > 0 {
let path = String(cString: pathBuffer)
name = (path as NSString).lastPathComponent
} else {
let bsdName = withUnsafePointer(to: bsdInfo.pbi_name) { ptr -> String in
ptr.withMemoryRebound(to: CChar.self, capacity: Int(MAXCOMLEN)) {
String(cString: $0)
}
}
name = bsdName.isEmpty ? "(Unknown)" : bsdName
}
nameCache[pid] = name
return name
}
private func fetchUsername(uid: uid_t) -> String {
if let cached = userCache[uid] {
return cached
}
let name: String
if let pw = getpwuid(uid) {
name = String(cString: pw.pointee.pw_name)
} else {
name = String(uid)
}
userCache[uid] = name
return name
}
private func fetchRusageData(pid: pid_t) -> (memory: Int64, userTime: UInt64, systemTime: UInt64) {
var rusage = rusage_info_v4()
let result = withUnsafeMutablePointer(to: &rusage) { ptr -> Int32 in
ptr.withMemoryRebound(to: rusage_info_t?.self, capacity: 1) { rusagePtr in
proc_pid_rusage(pid, RUSAGE_INFO_V4, rusagePtr)
}
}
if result == 0 {
return (
memory: Int64(rusage.ri_phys_footprint),
userTime: rusage.ri_user_time,
systemTime: rusage.ri_system_time
)
}
// Fallback to proc_taskinfo if rusage fails
var taskInfo = proc_taskinfo()
let taskInfoSize = Int32(MemoryLayout<proc_taskinfo>.size)
if proc_pidinfo(pid, PROC_PIDTASKINFO, 0, &taskInfo, taskInfoSize) == taskInfoSize {
return (
memory: Int64(taskInfo.pti_resident_size),
userTime: taskInfo.pti_total_user,
systemTime: taskInfo.pti_total_system
)
}
return (memory: 0, userTime: 0, systemTime: 0)
}
private func fetchBasicProcessInfo(pid: pid_t) -> ProcessItem? {
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, pid]
var info = kinfo_proc()
var size = MemoryLayout<kinfo_proc>.size
let result = sysctl(&mib, UInt32(mib.count), &info, &size, nil, 0)
guard result == 0, size > 0 else { return nil }
// Extract name from kp_proc.p_comm
var name = withUnsafePointer(to: info.kp_proc.p_comm) { ptr -> String in
ptr.withMemoryRebound(to: CChar.self, capacity: 16) {
String(cString: $0)
}
}
if name.isEmpty {
name = "(Unknown)"
}
// Check cache for full name
if let cachedName = nameCache[pid] {
name = cachedName
} else {
nameCache[pid] = name
}
let user = fetchUsername(uid: info.kp_eproc.e_ucred.cr_uid)
let parentPid = info.kp_eproc.e_ppid
// Determine state from p_stat
let state: ProcessState
switch info.kp_proc.p_stat {
case 5: state = .zombie
case 4: state = .stopped
case 1: state = .unknown
default: state = .sleeping // Can't determine CPU usage, assume sleeping
}
return ProcessItem(
pid: pid,
name: name,
user: user,
cpuUsage: 0,
memoryUsage: 0,
threadCount: 0,
state: state,
icon: nil,
parentPid: parentPid,
startTime: nil
)
}
private func determineProcessState(status: UInt32, cpuUsage: Double) -> ProcessState {
// Status values: SIDL=1, SRUN=2, SSLEEP=3, SSTOP=4, SZOMB=5
switch status {
case 5: return .zombie
case 4: return .stopped
case 1: return .unknown
default:
// For runnable processes (stat == 2 or 3), use CPU usage to determine display state
return cpuUsage > 1.0 ? .running : .sleeping
}
}
private func calculateCPUUsage(pid: pid_t, userTime: UInt64, systemTime: UInt64) -> Double {
let now = Date()
let totalTime = userTime + systemTime
defer {
previousCPUTimes[pid] = (userTime, systemTime, now)
}
guard let previous = previousCPUTimes[pid] else {
return 0
}
let timeDelta = now.timeIntervalSince(previous.timestamp)
guard timeDelta > 0 else { return 0 }
let previousTotal = previous.user + previous.system
// Handle case where times might wrap or process restarted
guard totalTime >= previousTotal else { return 0 }
let cpuDelta = totalTime - previousTotal
// Convert Mach absolute time to nanoseconds
let nanoseconds = cpuDelta * UInt64(timebaseInfo.numer) / UInt64(timebaseInfo.denom)
// Convert nanoseconds to seconds
let cpuSeconds = Double(nanoseconds) / 1_000_000_000
// Calculate percentage: (CPU time / wall time) * 100
let cpuUsage = (cpuSeconds / timeDelta) * 100
return min(cpuUsage, 100 * Double(ProcessInfo.processInfo.processorCount))
}
private func fetchIcon(pid: pid_t) -> NSImage? {
let cacheKey = NSNumber(value: pid)
if let cached = iconCache.object(forKey: cacheKey) {
return cached
}
if let app = NSRunningApplication(processIdentifier: pid),
let icon = app.icon {
iconCache.setObject(icon, forKey: cacheKey)
return icon
}
return nil
}
func clearCPUHistory() {
previousCPUTimes.removeAll()
}
}

Fájl megtekintése

@@ -0,0 +1,183 @@
import Foundation
import Combine
@MainActor
final class SystemMonitor: ObservableObject {
static let shared = SystemMonitor()
// Published properties
@Published var cpuInfo: CPUInfo?
@Published var memoryInfo: MemoryInfo?
@Published var processes: [ProcessItem] = []
@Published var diskInfo: DiskInfo?
@Published var networkInfo: NetworkInfo?
@Published var gpuInfo: GPUInfo?
@Published var lastError: String?
// History for charts
@Published var cpuHistory: [CPUHistoryPoint] = []
@Published var coreHistories: [Int: [CoreHistoryPoint]] = [:]
@Published var memoryHistory: [MemoryHistoryPoint] = []
@Published var networkHistory: [NetworkHistoryPoint] = []
// Sub-monitors
private let cpuMonitor = CPUMonitor()
private let memoryMonitor = MemoryMonitor()
private let processMonitor = ProcessMonitor()
private let diskMonitor = DiskMonitor()
private let networkMonitor = NetworkMonitor()
private let gpuMonitor = GPUMonitor()
// Timer
private var timer: Timer?
private let historyLimit = 60
private var refreshCount = 0
var isAppleSilicon: Bool {
cpuMonitor.isAppleSilicon
}
var thermalState: ProcessInfo.ThermalState {
ProcessInfo.processInfo.thermalState
}
private init() {}
func startMonitoring() {
// Initial fetch
Task {
await refreshAll()
}
// Start periodic refresh (2 second interval to reduce CPU usage)
timer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { [weak self] _ in
Task { @MainActor in
await self?.refreshAll()
}
}
}
func stopMonitoring() {
timer?.invalidate()
timer = nil
}
private func refreshAll() async {
refreshCount += 1
await withTaskGroup(of: Void.self) { group in
group.addTask { await self.refreshCPU() }
group.addTask { await self.refreshMemory() }
group.addTask { await self.refreshNetwork() }
// Refresh processes every other cycle = 4 seconds (expensive operation)
if self.refreshCount % 2 == 0 {
group.addTask { await self.refreshProcesses() }
}
// Refresh disk and GPU every 3 cycles = 6 seconds (less volatile)
if self.refreshCount % 3 == 0 {
group.addTask { await self.refreshDisk() }
group.addTask { await self.refreshGPU() }
}
}
}
private func refreshCPU() async {
if let info = cpuMonitor.fetchCPUInfo() {
cpuInfo = info
// Global CPU history
let historyPoint = CPUHistoryPoint(
timestamp: info.timestamp,
usage: info.globalUsage,
userUsage: info.userUsage,
systemUsage: info.systemUsage
)
cpuHistory.append(historyPoint)
if cpuHistory.count > historyLimit {
cpuHistory.removeFirst()
}
// Per-core history
for core in info.coreUsages {
let corePoint = CoreHistoryPoint(
timestamp: info.timestamp,
usage: core.usage
)
if coreHistories[core.id] == nil {
coreHistories[core.id] = []
}
coreHistories[core.id]?.append(corePoint)
if coreHistories[core.id]?.count ?? 0 > historyLimit {
coreHistories[core.id]?.removeFirst()
}
}
}
}
private func refreshMemory() async {
if let info = memoryMonitor.fetchMemoryInfo() {
memoryInfo = info
let historyPoint = MemoryHistoryPoint(
timestamp: info.timestamp,
usedMemory: info.usedMemory,
totalMemory: info.totalMemory
)
memoryHistory.append(historyPoint)
if memoryHistory.count > historyLimit {
memoryHistory.removeFirst()
}
}
}
private func refreshProcesses() async {
processes = processMonitor.fetchProcesses()
}
private func refreshDisk() async {
diskInfo = diskMonitor.fetchDiskInfo()
}
private func refreshNetwork() async {
if let info = networkMonitor.fetchNetworkInfo() as NetworkInfo? {
networkInfo = info
let historyPoint = NetworkHistoryPoint(
timestamp: info.timestamp,
downloadRate: info.totalDownloadRate,
uploadRate: info.totalUploadRate
)
networkHistory.append(historyPoint)
if networkHistory.count > historyLimit {
networkHistory.removeFirst()
}
}
}
private func refreshGPU() async {
gpuInfo = gpuMonitor.fetchGPUInfo()
}
// Process control methods
@discardableResult
func terminateProcess(_ pid: pid_t) -> Bool {
kill(pid, SIGTERM) == 0
}
@discardableResult
func forceKillProcess(_ pid: pid_t) -> Bool {
kill(pid, SIGKILL) == 0
}
@discardableResult
func suspendProcess(_ pid: pid_t) -> Bool {
kill(pid, SIGSTOP) == 0
}
@discardableResult
func resumeProcess(_ pid: pid_t) -> Bool {
kill(pid, SIGCONT) == 0
}
}

Fájl megtekintése

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
</dict>
</plist>

Fájl megtekintése

@@ -0,0 +1,35 @@
import SwiftUI
@main
struct TopManagerApp: App {
@StateObject private var monitor = SystemMonitor.shared
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(monitor)
.onAppear {
monitor.startMonitoring()
}
}
.windowStyle(.automatic)
.defaultSize(width: 1000, height: 700)
.commands {
CommandGroup(replacing: .newItem) { }
}
// Menu Bar Extra showing CPU %
MenuBarExtra {
MenuBarView()
.environmentObject(monitor)
} label: {
if let cpu = monitor.cpuInfo {
Text(String(format: "%.0f%%", cpu.globalUsage))
.monospacedDigit()
} else {
Image(systemName: "cpu")
}
}
.menuBarExtraStyle(.window)
}
}

BINáris
TopManager/TopManager_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Méret: 337 KiB

Fájl megtekintése

@@ -0,0 +1,66 @@
import Foundation
func formatBytes(_ bytes: UInt64) -> String {
let formatter = ByteCountFormatter()
formatter.countStyle = .binary
formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB, .useTB]
return formatter.string(fromByteCount: Int64(bytes))
}
func formatBytes(_ bytes: Int64) -> String {
let formatter = ByteCountFormatter()
formatter.countStyle = .binary
formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB, .useTB]
return formatter.string(fromByteCount: bytes)
}
func formatBytes(_ bytes: Double) -> String {
formatBytes(UInt64(max(0, bytes)))
}
func formatBytesPerSecond(_ bytesPerSecond: Double) -> String {
let absValue = abs(bytesPerSecond)
if absValue < 1 {
return "0 B/s"
}
let formatter = ByteCountFormatter()
formatter.countStyle = .binary
formatter.allowedUnits = [.useBytes, .useKB, .useMB, .useGB]
return formatter.string(fromByteCount: Int64(absValue)) + "/s"
}
func formatBytesCompact(_ bytes: Double) -> String {
let absValue = abs(bytes)
if absValue < 1 {
return "0"
} else if absValue < 1024 {
return String(format: "%.0f B", absValue)
} else if absValue < 1024 * 1024 {
return String(format: "%.1f KB", absValue / 1024)
} else if absValue < 1024 * 1024 * 1024 {
return String(format: "%.1f MB", absValue / (1024 * 1024))
} else {
return String(format: "%.2f GB", absValue / (1024 * 1024 * 1024))
}
}
func formatPercentage(_ value: Double, decimals: Int = 1) -> String {
String(format: "%.\(decimals)f%%", value)
}
func formatUptime(_ interval: TimeInterval) -> String {
let days = Int(interval) / 86400
let hours = (Int(interval) % 86400) / 3600
let minutes = (Int(interval) % 3600) / 60
if days > 0 {
return "\(days)d \(hours)h \(minutes)m"
} else if hours > 0 {
return "\(hours)h \(minutes)m"
} else {
return "\(minutes)m"
}
}

Fájl megtekintése

@@ -0,0 +1,233 @@
import SwiftUI
import AppKit
struct RunningApp: Identifiable {
let id: pid_t
let pid: pid_t
let name: String
let bundleIdentifier: String?
let icon: NSImage?
let isActive: Bool
let isHidden: Bool
let launchDate: Date?
var cpuUsage: Double
var memoryUsage: Int64
}
struct AppsView: View {
@EnvironmentObject var monitor: SystemMonitor
@State private var runningApps: [RunningApp] = []
@State private var selectedApp: Set<pid_t> = []
@State private var sortOrder: [KeyPathComparator<RunningApp>] = [
.init(\.name, order: .forward)
]
@State private var searchText = ""
var filteredApps: [RunningApp] {
let filtered = runningApps.filter {
searchText.isEmpty ||
$0.name.localizedCaseInsensitiveContains(searchText) ||
($0.bundleIdentifier?.localizedCaseInsensitiveContains(searchText) ?? false)
}
return filtered.sorted(using: sortOrder)
}
var body: some View {
VStack(spacing: 0) {
// Summary bar
HStack {
Text("\(runningApps.count) apps")
.foregroundColor(.secondary)
Spacer()
if let activeApp = runningApps.first(where: { $0.isActive }) {
HStack(spacing: 4) {
Text("Active:")
.foregroundColor(.secondary)
if let icon = activeApp.icon {
Image(nsImage: icon)
.resizable()
.frame(width: 14, height: 14)
}
Text(activeApp.name)
.fontWeight(.medium)
}
}
TextField("Search...", text: $searchText)
.textFieldStyle(.roundedBorder)
.frame(width: 150)
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(nsColor: .windowBackgroundColor))
Divider()
// Apps table
Table(filteredApps, selection: $selectedApp, sortOrder: $sortOrder) {
TableColumn("") { app in
if let icon = app.icon {
Image(nsImage: icon)
.resizable()
.frame(width: 20, height: 20)
} else {
Image(systemName: "app.dashed")
.frame(width: 20, height: 20)
}
}
.width(28)
TableColumn("Name", value: \.name) { app in
HStack(spacing: 6) {
Text(app.name)
.lineLimit(1)
if app.isActive {
Circle()
.fill(Color.green)
.frame(width: 6, height: 6)
}
if app.isHidden {
Image(systemName: "eye.slash")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
.width(min: 150, ideal: 200)
TableColumn("PID", value: \.pid) { app in
Text("\(app.pid)")
.monospacedDigit()
}
.width(60)
TableColumn("CPU %", value: \.cpuUsage) { app in
Text(String(format: "%.1f%%", app.cpuUsage))
.monospacedDigit()
.foregroundColor(cpuColor(app.cpuUsage))
}
.width(70)
TableColumn("Memory", value: \.memoryUsage) { app in
Text(formatBytes(app.memoryUsage))
.monospacedDigit()
}
.width(90)
TableColumn("Bundle ID") { app in
Text(app.bundleIdentifier ?? "-")
.font(.caption)
.foregroundColor(.secondary)
.lineLimit(1)
}
.width(min: 150, ideal: 200)
}
.contextMenu(forSelectionType: pid_t.self) { selection in
if let pid = selection.first, let app = runningApps.first(where: { $0.pid == pid }) {
Button("Activate") {
activateApp(pid: pid)
}
Button("Hide") {
hideApp(pid: pid)
}
Divider()
Button("Quit") {
quitApp(pid: pid)
}
Button("Force Quit") {
forceQuitApp(pid: pid)
}
Divider()
Button("Copy Bundle ID") {
if let bundleId = app.bundleIdentifier {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(bundleId, forType: .string)
}
}
.disabled(app.bundleIdentifier == nil)
}
} primaryAction: { selection in
if let pid = selection.first {
activateApp(pid: pid)
}
}
}
.onAppear {
refreshApps()
}
.onChange(of: monitor.processes) { _ in
refreshApps()
}
}
private func refreshApps() {
let workspace = NSWorkspace.shared
let apps = workspace.runningApplications
// Build a map of PID to process info for CPU/memory data
let processMap = Dictionary(uniqueKeysWithValues: monitor.processes.map { ($0.pid, $0) })
runningApps = apps.compactMap { app -> RunningApp? in
// Only include regular apps (not background agents)
guard app.activationPolicy == .regular else { return nil }
let pid = app.processIdentifier
let processInfo = processMap[pid]
return RunningApp(
id: pid,
pid: pid,
name: app.localizedName ?? app.bundleIdentifier ?? "Unknown",
bundleIdentifier: app.bundleIdentifier,
icon: app.icon,
isActive: app.isActive,
isHidden: app.isHidden,
launchDate: app.launchDate,
cpuUsage: processInfo?.cpuUsage ?? 0,
memoryUsage: processInfo?.memoryUsage ?? 0
)
}
}
private func activateApp(pid: pid_t) {
if let app = NSRunningApplication(processIdentifier: pid) {
app.activate(options: [.activateIgnoringOtherApps])
}
}
private func hideApp(pid: pid_t) {
if let app = NSRunningApplication(processIdentifier: pid) {
app.hide()
}
}
private func quitApp(pid: pid_t) {
if let app = NSRunningApplication(processIdentifier: pid) {
app.terminate()
}
}
private func forceQuitApp(pid: pid_t) {
if let app = NSRunningApplication(processIdentifier: pid) {
app.forceTerminate()
}
}
private func cpuColor(_ usage: Double) -> Color {
if usage > 80 {
return .red
} else if usage > 50 {
return .orange
} else if usage > 20 {
return .yellow
}
return .primary
}
}
#Preview {
AppsView()
.environmentObject(SystemMonitor.shared)
}

Fájl megtekintése

@@ -0,0 +1,35 @@
import SwiftUI
struct ContentView: View {
@EnvironmentObject var monitor: SystemMonitor
var body: some View {
TabView {
ProcessView()
.tabItem {
Label("Processes", systemImage: "list.bullet.rectangle")
}
AppsView()
.tabItem {
Label("Apps", systemImage: "square.grid.2x2")
}
PerformanceView()
.tabItem {
Label("Performance", systemImage: "chart.line.uptrend.xyaxis")
}
PowerStorageView()
.tabItem {
Label("Power & Storage", systemImage: "battery.100.bolt")
}
}
.frame(minWidth: 800, minHeight: 500)
}
}
#Preview {
ContentView()
.environmentObject(SystemMonitor.shared)
}

Fájl megtekintése

@@ -0,0 +1,83 @@
import SwiftUI
struct MenuBarView: View {
@EnvironmentObject var monitor: SystemMonitor
var body: some View {
VStack(alignment: .leading, spacing: 8) {
if let cpu = monitor.cpuInfo {
HStack {
Image(systemName: "cpu")
.frame(width: 16)
Text("CPU:")
Spacer()
Text(String(format: "%.1f%%", cpu.globalUsage))
.monospacedDigit()
}
}
if let mem = monitor.memoryInfo {
HStack {
Image(systemName: "memorychip")
.frame(width: 16)
Text("Memory:")
Spacer()
Text(String(format: "%.1f%%", mem.usagePercentage))
.monospacedDigit()
}
}
if let net = monitor.networkInfo {
HStack {
Image(systemName: "arrow.down.circle")
.frame(width: 16)
Text("Download:")
Spacer()
Text(formatBytesPerSecond(net.totalDownloadRate))
.monospacedDigit()
}
HStack {
Image(systemName: "arrow.up.circle")
.frame(width: 16)
Text("Upload:")
Spacer()
Text(formatBytesPerSecond(net.totalUploadRate))
.monospacedDigit()
}
}
if let gpu = monitor.gpuInfo {
HStack {
Image(systemName: "gpu")
.frame(width: 16)
Text("GPU VRAM:")
Spacer()
Text(formatBytes(gpu.vramUsed))
.monospacedDigit()
}
}
Divider()
Button("Open TopManager") {
NSApp.activate(ignoringOtherApps: true)
if let window = NSApp.windows.first(where: { $0.canBecomeMain }) {
window.makeKeyAndOrderFront(nil)
}
}
Button("Quit") {
NSApplication.shared.terminate(nil)
}
.keyboardShortcut("q")
}
.padding(12)
.frame(width: 220)
}
}
#Preview {
MenuBarView()
.environmentObject(SystemMonitor.shared)
}

Fájl megtekintése

@@ -0,0 +1,87 @@
import SwiftUI
import Charts
struct CPULineChart: View {
let history: [CPUHistoryPoint]
var body: some View {
Chart {
ForEach(history) { point in
// User usage line
LineMark(
x: .value("Time", point.timestamp),
y: .value("User", point.userUsage),
series: .value("Type", "User")
)
.foregroundStyle(.blue)
.lineStyle(StrokeStyle(lineWidth: 1.5))
// System usage line
LineMark(
x: .value("Time", point.timestamp),
y: .value("System", point.systemUsage),
series: .value("Type", "System")
)
.foregroundStyle(.red)
.lineStyle(StrokeStyle(lineWidth: 1.5))
// Total usage line
LineMark(
x: .value("Time", point.timestamp),
y: .value("Total", point.usage),
series: .value("Type", "Total")
)
.foregroundStyle(.gray)
.lineStyle(StrokeStyle(lineWidth: 2))
}
}
.chartYScale(domain: -5...105)
.chartYAxis {
AxisMarks(position: .leading, values: [0, 25, 50, 75, 100]) { value in
AxisGridLine()
AxisValueLabel {
if let intValue = value.as(Int.self) {
Text("\(intValue)%")
.font(.caption2)
}
}
}
}
.padding(.vertical, 8)
.chartXAxis {
AxisMarks(values: .automatic(desiredCount: 5)) { _ in
AxisGridLine()
}
}
.chartLegend(position: .bottom, spacing: 10) {
HStack(spacing: 16) {
ChartLegendItem(color: .blue, label: "User")
ChartLegendItem(color: .red, label: "System")
ChartLegendItem(color: .gray, label: "Total")
}
}
.drawingGroup()
}
}
struct ChartLegendItem: View {
let color: Color
let label: String
var body: some View {
HStack(spacing: 4) {
RoundedRectangle(cornerRadius: 2)
.fill(color)
.frame(width: 12, height: 3)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
#Preview {
CPULineChart(history: [])
.frame(height: 200)
.padding()
}

Fájl megtekintése

@@ -0,0 +1,112 @@
import SwiftUI
struct MemoryDonutView: View {
@EnvironmentObject var monitor: SystemMonitor
var memorySegments: [MemorySegment] {
guard let mem = monitor.memoryInfo else { return [] }
return [
MemorySegment(name: "App", value: Double(mem.appMemory), color: .blue),
MemorySegment(name: "Wired", value: Double(mem.wiredMemory), color: .orange),
MemorySegment(name: "Compressed", value: Double(mem.compressedMemory), color: .yellow),
MemorySegment(name: "Free", value: Double(mem.freeMemory + mem.cachedMemory), color: .green.opacity(0.5))
]
}
var body: some View {
ZStack {
// Donut chart
DonutChart(segments: memorySegments)
// Center text
if let mem = monitor.memoryInfo {
VStack {
Text(formatBytes(mem.usedMemory))
.font(.title3)
.fontWeight(.semibold)
.monospacedDigit()
Text("of \(formatBytes(mem.totalMemory))")
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
}
struct DonutChart: View {
let segments: [MemorySegment]
let lineWidth: CGFloat = 30
var total: Double {
segments.reduce(0) { $0 + $1.value }
}
var body: some View {
GeometryReader { geometry in
let size = min(geometry.size.width, geometry.size.height)
ZStack {
ForEach(Array(segments.enumerated()), id: \.element.id) { index, segment in
DonutSegment(
startAngle: startAngle(for: index),
endAngle: endAngle(for: index),
lineWidth: lineWidth
)
.fill(segment.color)
}
}
.frame(width: size, height: size)
.position(x: geometry.size.width / 2, y: geometry.size.height / 2)
}
}
private func startAngle(for index: Int) -> Angle {
guard total > 0 else { return .degrees(-90) }
let precedingSum = segments.prefix(index).reduce(0) { $0 + $1.value }
return .degrees((precedingSum / total) * 360 - 90)
}
private func endAngle(for index: Int) -> Angle {
guard total > 0 else { return .degrees(-90) }
let sum = segments.prefix(index + 1).reduce(0) { $0 + $1.value }
return .degrees((sum / total) * 360 - 90)
}
}
struct DonutSegment: Shape {
let startAngle: Angle
let endAngle: Angle
let lineWidth: CGFloat
func path(in rect: CGRect) -> Path {
var path = Path()
let center = CGPoint(x: rect.midX, y: rect.midY)
let radius = min(rect.width, rect.height) / 2 - lineWidth / 2
path.addArc(
center: center,
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: false
)
return path.strokedPath(StrokeStyle(lineWidth: lineWidth, lineCap: .butt))
}
}
struct MemorySegment: Identifiable {
let id = UUID()
let name: String
let value: Double
let color: Color
}
#Preview {
MemoryDonutView()
.environmentObject(SystemMonitor.shared)
.frame(width: 250, height: 250)
.padding()
}

Fájl megtekintése

@@ -0,0 +1,44 @@
import SwiftUI
import Charts
struct MemoryLineChart: View {
let history: [MemoryHistoryPoint]
var body: some View {
Chart {
ForEach(history) { point in
LineMark(
x: .value("Time", point.timestamp),
y: .value("Usage", point.usagePercentage)
)
.foregroundStyle(.blue)
.lineStyle(StrokeStyle(lineWidth: 2))
}
}
.chartYScale(domain: -5...105)
.chartYAxis {
AxisMarks(position: .leading, values: [0, 25, 50, 75, 100]) { value in
AxisGridLine()
AxisValueLabel {
if let intValue = value.as(Int.self) {
Text("\(intValue)%")
.font(.caption2)
}
}
}
}
.padding(.vertical, 8)
.chartXAxis {
AxisMarks(values: .automatic(desiredCount: 5)) { _ in
AxisGridLine()
}
}
.drawingGroup()
}
}
#Preview {
MemoryLineChart(history: [])
.frame(height: 200)
.padding()
}

Fájl megtekintése

@@ -0,0 +1,100 @@
import SwiftUI
import Charts
struct DownloadLineChart: View {
let history: [NetworkHistoryPoint]
var maxRate: Double {
max(history.map(\.downloadRate).max() ?? 0, 1024)
}
var yScaleMax: Double {
maxRate * 1.1 // 10% margin at top
}
var body: some View {
Chart {
ForEach(history) { point in
LineMark(
x: .value("Time", point.timestamp),
y: .value("Download", point.downloadRate)
)
.foregroundStyle(.blue)
.lineStyle(StrokeStyle(lineWidth: 2))
}
}
.chartYScale(domain: 0...yScaleMax)
.chartYAxis {
AxisMarks(position: .leading) { value in
AxisGridLine()
AxisValueLabel {
if let rate = value.as(Double.self) {
Text(formatBytesPerSecond(rate))
.font(.caption2)
}
}
}
}
.chartXAxis {
AxisMarks(values: .automatic(desiredCount: 5)) { _ in
AxisGridLine()
}
}
.padding(.vertical, 8)
.drawingGroup()
}
}
struct UploadLineChart: View {
let history: [NetworkHistoryPoint]
var maxRate: Double {
max(history.map(\.uploadRate).max() ?? 0, 1024)
}
var yScaleMax: Double {
maxRate * 1.1 // 10% margin at top
}
var body: some View {
Chart {
ForEach(history) { point in
LineMark(
x: .value("Time", point.timestamp),
y: .value("Upload", point.uploadRate)
)
.foregroundStyle(.green)
.lineStyle(StrokeStyle(lineWidth: 2))
}
}
.chartYScale(domain: 0...yScaleMax)
.chartYAxis {
AxisMarks(position: .leading) { value in
AxisGridLine()
AxisValueLabel {
if let rate = value.as(Double.self) {
Text(formatBytesPerSecond(rate))
.font(.caption2)
}
}
}
}
.chartXAxis {
AxisMarks(values: .automatic(desiredCount: 5)) { _ in
AxisGridLine()
}
}
.padding(.vertical, 8)
.drawingGroup()
}
}
#Preview {
VStack {
DownloadLineChart(history: [])
.frame(height: 150)
UploadLineChart(history: [])
.frame(height: 150)
}
.padding()
}

Fájl megtekintése

@@ -0,0 +1,541 @@
import SwiftUI
import Charts
struct PerformanceView: View {
@State private var selectedTab = 0
var body: some View {
TabView(selection: $selectedTab) {
CPUTabView()
.tabItem {
Label("CPU", systemImage: "cpu")
}
.tag(0)
MemoryTabView()
.tabItem {
Label("Memory", systemImage: "memorychip")
}
.tag(1)
NetworkTabView()
.tabItem {
Label("Network", systemImage: "network")
}
.tag(2)
}
}
}
// MARK: - CPU Tab
struct CPUTabView: View {
@EnvironmentObject var monitor: SystemMonitor
private let coreColumns = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
ScrollView {
VStack(spacing: 16) {
// Overall CPU
GroupBox {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Total CPU Usage")
.font(.headline)
Spacer()
if let cpu = monitor.cpuInfo {
Text(String(format: "%.1f%%", cpu.globalUsage))
.font(.title2)
.fontWeight(.semibold)
.monospacedDigit()
}
}
CPULineChart(history: monitor.cpuHistory)
.frame(height: 120)
// Legend
HStack(spacing: 20) {
ChartLegendItem(color: .blue, label: "User")
ChartLegendItem(color: .red, label: "System")
ChartLegendItem(color: .gray, label: "Total")
}
.frame(maxWidth: .infinity)
}
.padding(.vertical, 8)
}
// Per-core charts
if let cpu = monitor.cpuInfo {
// P-Cores
if !cpu.pCoreUsages.isEmpty {
GroupBox {
VStack(alignment: .leading, spacing: 12) {
Text("Performance Cores")
.font(.headline)
LazyVGrid(columns: coreColumns, spacing: 12) {
ForEach(cpu.pCoreUsages) { core in
CoreLineChartView(
coreId: core.id,
coreType: "P",
usage: core.usage,
history: monitor.coreHistories[core.id] ?? [],
color: .orange
)
}
}
}
.padding(.vertical, 8)
}
}
// E-Cores
if !cpu.eCoreUsages.isEmpty {
GroupBox {
VStack(alignment: .leading, spacing: 12) {
Text("Efficiency Cores")
.font(.headline)
LazyVGrid(columns: coreColumns, spacing: 12) {
ForEach(cpu.eCoreUsages) { core in
CoreLineChartView(
coreId: core.id,
coreType: "E",
usage: core.usage,
history: monitor.coreHistories[core.id] ?? [],
color: .blue
)
}
}
}
.padding(.vertical, 8)
}
}
// All cores (for non-Apple Silicon)
if cpu.pCoreUsages.isEmpty && cpu.eCoreUsages.isEmpty {
GroupBox {
VStack(alignment: .leading, spacing: 12) {
Text("CPU Cores")
.font(.headline)
LazyVGrid(columns: coreColumns, spacing: 12) {
ForEach(cpu.coreUsages) { core in
CoreLineChartView(
coreId: core.id,
coreType: "",
usage: core.usage,
history: monitor.coreHistories[core.id] ?? [],
color: .blue
)
}
}
}
.padding(.vertical, 8)
}
}
}
}
.padding()
}
}
}
struct CoreLineChartView: View {
let coreId: Int
let coreType: String
let usage: Double
let history: [CoreHistoryPoint]
let color: Color
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(coreType.isEmpty ? "Core \(coreId)" : "\(coreType)\(coreId)")
.font(.caption)
.fontWeight(.medium)
Spacer()
Text(String(format: "%.0f%%", usage))
.font(.caption)
.monospacedDigit()
.foregroundColor(usageColor(usage))
}
// Line chart for this core
Chart {
ForEach(history) { point in
LineMark(
x: .value("Time", point.timestamp),
y: .value("Usage", point.usage)
)
.foregroundStyle(color)
.lineStyle(StrokeStyle(lineWidth: 1.5))
}
}
.chartYScale(domain: -5...105)
.chartXAxis(.hidden)
.chartYAxis(.hidden)
.frame(height: 50)
.drawingGroup()
}
.padding(8)
.background(Color(nsColor: .controlBackgroundColor))
.cornerRadius(6)
}
private func usageColor(_ usage: Double) -> Color {
if usage > 80 { return .red }
if usage > 50 { return .orange }
return .primary
}
}
// MARK: - Memory Tab
struct MemoryTabView: View {
@EnvironmentObject var monitor: SystemMonitor
var body: some View {
ScrollView {
VStack(spacing: 16) {
HStack(alignment: .top, spacing: 16) {
// Left column: Donut chart
GroupBox {
VStack(alignment: .leading, spacing: 12) {
Text("Memory Composition")
.font(.headline)
MemoryDonutView()
.frame(height: 200)
if let mem = monitor.memoryInfo {
MemoryLegendView(memory: mem)
}
}
.padding(.vertical, 8)
}
// Right column: Chart + Details
VStack(spacing: 16) {
// Memory history chart
GroupBox {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text("Memory Usage Over Time")
.font(.headline)
Spacer()
if let mem = monitor.memoryInfo {
Text(String(format: "%.1f%%", mem.usagePercentage))
.font(.title2)
.fontWeight(.semibold)
.monospacedDigit()
}
}
MemoryLineChart(history: monitor.memoryHistory)
.frame(height: 150)
if let mem = monitor.memoryInfo {
HStack {
Text("Used: \(formatBytes(mem.usedMemory))")
Spacer()
Text("Free: \(formatBytes(mem.freeMemory))")
Spacer()
Text("Total: \(formatBytes(mem.totalMemory))")
}
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
// Memory details
if let mem = monitor.memoryInfo {
GroupBox {
VStack(alignment: .leading, spacing: 12) {
Text("Memory Details")
.font(.headline)
HStack(spacing: 20) {
MemoryStatItem(label: "Swap Used", value: formatBytes(mem.swapUsed))
MemoryStatItem(label: "Swap Total", value: formatBytes(mem.swapTotal))
MemoryStatItem(label: "Pressure", value: mem.memoryPressure.rawValue, color: pressureColor(mem.memoryPressure))
Spacer()
}
}
.padding(.vertical, 8)
.frame(maxWidth: .infinity)
}
.frame(maxWidth: .infinity)
}
}
}
// GPU Memory
if let gpu = monitor.gpuInfo {
GroupBox {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("GPU", systemImage: "gpu")
.font(.headline)
Text(gpu.name)
.foregroundColor(.secondary)
Spacer()
}
HStack {
Text(gpu.memoryLabel + ":")
.foregroundColor(.secondary)
if gpu.vramUsed > 0 {
Text(formatBytes(gpu.vramUsed))
.monospacedDigit()
if let total = gpu.vramTotal {
Text("/ \(formatBytes(total))")
.foregroundColor(.secondary)
}
} else if gpu.isUnifiedMemory {
Text("Unified Memory Architecture")
.foregroundColor(.secondary)
}
Spacer()
if let util = gpu.utilizationPercent {
Text("GPU: \(String(format: "%.0f%%", util))")
.monospacedDigit()
}
}
if gpu.vramUsed > 0, let total = gpu.vramTotal {
ProgressView(value: Double(gpu.vramUsed), total: Double(total))
.tint(.purple)
}
}
.padding(.vertical, 8)
}
}
}
.padding()
}
}
private func pressureColor(_ pressure: MemoryPressure) -> Color {
switch pressure {
case .nominal: return .green
case .warning: return .yellow
case .critical: return .red
case .unknown: return .secondary
}
}
}
struct MemoryLegendView: View {
let memory: MemoryInfo
var body: some View {
VStack(spacing: 4) {
LegendRow(color: .blue, label: "App Memory", value: formatBytes(memory.appMemory))
LegendRow(color: .orange, label: "Wired", value: formatBytes(memory.wiredMemory))
LegendRow(color: .yellow, label: "Compressed", value: formatBytes(memory.compressedMemory))
LegendRow(color: .green.opacity(0.5), label: "Cached/Free", value: formatBytes(memory.cachedMemory + memory.freeMemory))
}
}
}
struct LegendRow: View {
let color: Color
let label: String
let value: String
var body: some View {
HStack {
Circle()
.fill(color)
.frame(width: 8, height: 8)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text(value)
.font(.caption)
.monospacedDigit()
}
}
}
struct MemoryStatItem: View {
let label: String
let value: String
var color: Color = .primary
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(label)
.font(.caption)
.foregroundColor(.secondary)
Text(value)
.font(.body)
.foregroundColor(color)
.monospacedDigit()
}
}
}
// MARK: - Network Tab
struct NetworkTabView: View {
@EnvironmentObject var monitor: SystemMonitor
var body: some View {
ScrollView {
VStack(spacing: 16) {
// Download chart
GroupBox {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Download", systemImage: "arrow.down.circle")
.font(.headline)
.foregroundColor(.blue)
Spacer()
if let net = monitor.networkInfo {
Text(formatBytesPerSecond(net.totalDownloadRate))
.font(.title2)
.fontWeight(.semibold)
.monospacedDigit()
}
}
DownloadLineChart(history: monitor.networkHistory)
.frame(height: 150)
}
.padding(.vertical, 8)
}
// Upload chart
GroupBox {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Upload", systemImage: "arrow.up.circle")
.font(.headline)
.foregroundColor(.green)
Spacer()
if let net = monitor.networkInfo {
Text(formatBytesPerSecond(net.totalUploadRate))
.font(.title2)
.fontWeight(.semibold)
.monospacedDigit()
}
}
UploadLineChart(history: monitor.networkHistory)
.frame(height: 150)
}
.padding(.vertical, 8)
}
// Network interfaces
GroupBox {
VStack(alignment: .leading, spacing: 12) {
Text("Network Interfaces")
.font(.headline)
if let net = monitor.networkInfo {
ForEach(net.interfaces.filter { $0.isActive }) { iface in
NetworkInterfaceRowView(interface: iface)
}
if net.interfaces.filter({ $0.isActive }).isEmpty {
Text("No active interfaces")
.foregroundColor(.secondary)
}
}
}
.padding(.vertical, 8)
}
// Totals
if let net = monitor.networkInfo {
GroupBox {
VStack(alignment: .leading, spacing: 8) {
Text("Session Totals")
.font(.headline)
HStack(spacing: 40) {
VStack(alignment: .leading) {
Text("Downloaded")
.font(.caption)
.foregroundColor(.secondary)
Text(formatBytes(net.totalDownloadBytes))
.monospacedDigit()
}
VStack(alignment: .leading) {
Text("Uploaded")
.font(.caption)
.foregroundColor(.secondary)
Text(formatBytes(net.totalUploadBytes))
.monospacedDigit()
}
}
}
.padding(.vertical, 8)
}
}
}
.padding()
}
}
}
struct NetworkInterfaceRowView: View {
let interface: NetworkInterface
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(interface.displayName)
.fontWeight(.medium)
Spacer()
Text(interface.name)
.font(.caption)
.foregroundColor(.secondary)
}
HStack {
HStack(spacing: 4) {
Image(systemName: "arrow.down")
.foregroundColor(.blue)
Text(formatBytesPerSecond(interface.downloadRate))
.monospacedDigit()
}
Spacer()
HStack(spacing: 4) {
Image(systemName: "arrow.up")
.foregroundColor(.green)
Text(formatBytesPerSecond(interface.uploadRate))
.monospacedDigit()
}
}
.font(.caption)
Divider()
}
}
}
#Preview {
PerformanceView()
.environmentObject(SystemMonitor.shared)
.frame(width: 900, height: 700)
}

Fájl megtekintése

@@ -0,0 +1,91 @@
import SwiftUI
struct DiskUsageView: View {
let volume: VolumeInfo
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: volumeIcon)
.foregroundColor(.secondary)
Text(volume.name)
.fontWeight(.medium)
Spacer()
Text(volume.mountPoint)
.font(.caption)
.foregroundColor(.secondary)
}
ProgressView(value: volume.usagePercentage, total: 100)
.tint(usageColor)
HStack {
Text("\(formatBytes(volume.usedSpace)) used")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text("\(formatBytes(volume.freeSpace)) free")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Text("\(formatBytes(volume.totalSpace)) total")
.font(.caption)
.foregroundColor(.secondary)
}
Divider()
}
}
private var volumeIcon: String {
if volume.isRemovable {
return "externaldrive"
} else if volume.mountPoint == "/" {
return "internaldrive.fill"
} else {
return "internaldrive"
}
}
private var usageColor: Color {
if volume.usagePercentage > 90 {
return .red
} else if volume.usagePercentage > 75 {
return .orange
} else {
return .blue
}
}
}
#Preview {
VStack {
DiskUsageView(volume: VolumeInfo(
name: "Macintosh HD",
mountPoint: "/",
totalSpace: 500 * 1024 * 1024 * 1024,
freeSpace: 150 * 1024 * 1024 * 1024,
fileSystem: "apfs",
isRemovable: false,
isInternal: true
))
DiskUsageView(volume: VolumeInfo(
name: "External Drive",
mountPoint: "/Volumes/External",
totalSpace: 1024 * 1024 * 1024 * 1024,
freeSpace: 800 * 1024 * 1024 * 1024,
fileSystem: "apfs",
isRemovable: true,
isInternal: false
))
}
.padding()
}

Fájl megtekintése

@@ -0,0 +1,272 @@
import SwiftUI
struct PowerStorageView: View {
@EnvironmentObject var monitor: SystemMonitor
var body: some View {
ScrollView {
VStack(spacing: 20) {
// System Status and GPU side by side
HStack(alignment: .top, spacing: 20) {
// System Status (left column)
GroupBox {
VStack(alignment: .leading, spacing: 10) {
Label("System Status", systemImage: "thermometer.medium")
.font(.headline)
HStack {
Text("macOS:")
Spacer()
Text(ProcessInfo.processInfo.operatingSystemVersionString)
.foregroundColor(.secondary)
}
HStack {
Text("Uptime:")
Spacer()
Text(formatUptime(ProcessInfo.processInfo.systemUptime))
.monospacedDigit()
}
HStack {
Text("Thermal State:")
Spacer()
ThermalStateView(state: monitor.thermalState)
}
if let cpu = monitor.cpuInfo {
HStack {
Text("CPU Usage:")
Spacer()
Text(String(format: "%.1f%%", cpu.globalUsage))
.monospacedDigit()
}
HStack {
Text("CPU Cores:")
Spacer()
Text("\(cpu.coreUsages.count)")
.monospacedDigit()
}
}
if let mem = monitor.memoryInfo {
HStack {
Text("Memory:")
Spacer()
Text("\(formatBytes(mem.usedMemory)) / \(formatBytes(mem.totalMemory))")
.monospacedDigit()
}
}
if monitor.isAppleSilicon {
HStack {
Text("Architecture:")
Spacer()
Text("Apple Silicon")
}
}
}
.padding(.vertical, 8)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
// GPU Info (right column)
GroupBox {
VStack(alignment: .leading, spacing: 10) {
if let gpu = monitor.gpuInfo {
HStack {
Label("GPU", systemImage: "cpu.fill")
.font(.headline)
Spacer()
}
HStack {
Text("Name:")
Spacer()
Text(gpu.name)
.foregroundColor(.secondary)
}
if let cores = gpu.coreCount {
HStack {
Text("GPU Cores:")
Spacer()
Text("\(cores)")
.monospacedDigit()
}
}
HStack {
Text("VRAM Used:")
Spacer()
Text(formatBytes(gpu.vramUsed))
.monospacedDigit()
}
if let total = gpu.vramTotal {
HStack {
Text("VRAM Total:")
Spacer()
Text(formatBytes(total))
.monospacedDigit()
}
ProgressView(value: Double(gpu.vramUsed), total: Double(total))
.tint(.purple)
}
if let utilization = gpu.utilizationPercent {
HStack {
Text("Utilization:")
Spacer()
Text(String(format: "%.1f%%", utilization))
.monospacedDigit()
}
}
} else {
Label("GPU", systemImage: "cpu.fill")
.font(.headline)
Text("Loading GPU info...")
.foregroundColor(.secondary)
}
Spacer(minLength: 0)
}
.padding(.vertical, 8)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.fixedSize(horizontal: false, vertical: true)
// Storage
GroupBox {
VStack(alignment: .leading, spacing: 12) {
Label("Storage", systemImage: "internaldrive")
.font(.headline)
if let diskInfo = monitor.diskInfo {
ForEach(diskInfo.volumes) { volume in
DiskUsageView(volume: volume)
}
} else {
Text("Loading storage information...")
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
// Network Interfaces
GroupBox {
VStack(alignment: .leading, spacing: 12) {
Label("Network Interfaces", systemImage: "network")
.font(.headline)
if let networkInfo = monitor.networkInfo {
ForEach(networkInfo.interfaces.filter { $0.isActive }) { iface in
NetworkInterfaceRow(interface: iface)
}
if networkInfo.interfaces.filter({ $0.isActive }).isEmpty {
Text("No active interfaces")
.foregroundColor(.secondary)
}
}
}
.padding(.vertical, 8)
}
}
.padding()
}
}
}
struct ThermalStateView: View {
let state: ProcessInfo.ThermalState
var body: some View {
HStack(spacing: 4) {
Image(systemName: stateIcon)
.foregroundColor(stateColor)
Text(stateText)
.foregroundColor(stateColor)
}
}
private var stateIcon: String {
switch state {
case .nominal: return "checkmark.circle.fill"
case .fair: return "exclamationmark.circle.fill"
case .serious: return "exclamationmark.triangle.fill"
case .critical: return "xmark.octagon.fill"
@unknown default: return "questionmark.circle"
}
}
private var stateColor: Color {
switch state {
case .nominal: return .green
case .fair: return .yellow
case .serious: return .orange
case .critical: return .red
@unknown default: return .secondary
}
}
private var stateText: String {
switch state {
case .nominal: return "Normal"
case .fair: return "Fair"
case .serious: return "Serious"
case .critical: return "Critical"
@unknown default: return "Unknown"
}
}
}
struct NetworkInterfaceRow: View {
let interface: NetworkInterface
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(interface.displayName)
.fontWeight(.medium)
Spacer()
Text(interface.name)
.font(.caption)
.foregroundColor(.secondary)
}
HStack {
HStack(spacing: 4) {
Image(systemName: "arrow.down")
.foregroundColor(.blue)
Text(formatBytesPerSecond(interface.downloadRate))
.monospacedDigit()
}
Spacer()
HStack(spacing: 4) {
Image(systemName: "arrow.up")
.foregroundColor(.green)
Text(formatBytesPerSecond(interface.uploadRate))
.monospacedDigit()
}
}
.font(.caption)
Divider()
}
}
}
#Preview {
PowerStorageView()
.environmentObject(SystemMonitor.shared)
.frame(width: 600, height: 700)
}

Fájl megtekintése

@@ -0,0 +1,110 @@
import SwiftUI
struct ProcessDetailView: View {
let process: ProcessItem
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// Header
HStack(spacing: 12) {
if let icon = process.icon {
Image(nsImage: icon)
.resizable()
.frame(width: 48, height: 48)
} else {
Image(systemName: "app.dashed")
.font(.system(size: 48))
}
VStack(alignment: .leading) {
Text(process.name)
.font(.title2)
.fontWeight(.semibold)
Text("PID: \(process.pid)")
.foregroundColor(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
HStack {
Image(systemName: process.state.symbol)
Text(process.state.rawValue)
}
.foregroundColor(stateColor(process.state))
}
}
Divider()
// Details grid
LazyVGrid(columns: [
GridItem(.flexible()),
GridItem(.flexible())
], alignment: .leading, spacing: 12) {
DetailRow(label: "CPU Usage", value: String(format: "%.1f%%", process.cpuUsage))
DetailRow(label: "Memory", value: formatBytes(process.memoryUsage))
DetailRow(label: "Threads", value: "\(process.threadCount)")
DetailRow(label: "User", value: process.user)
DetailRow(label: "Parent PID", value: "\(process.parentPid)")
if let startTime = process.startTime {
DetailRow(label: "Started", value: formatDate(startTime))
}
}
Spacer()
}
.padding()
.frame(minWidth: 300)
}
private func stateColor(_ state: ProcessState) -> Color {
switch state {
case .running: return .green
case .sleeping: return .secondary
case .stopped: return .orange
case .zombie: return .red
case .unknown: return .secondary
}
}
private func formatDate(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .medium
return formatter.string(from: date)
}
}
struct DetailRow: View {
let label: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 2) {
Text(label)
.font(.caption)
.foregroundColor(.secondary)
Text(value)
.font(.body)
.monospacedDigit()
}
}
}
#Preview {
ProcessDetailView(process: ProcessItem(
pid: 1234,
name: "Safari",
user: "ariel",
cpuUsage: 12.5,
memoryUsage: 512 * 1024 * 1024,
threadCount: 42,
state: .running,
icon: nil,
parentPid: 1,
startTime: Date()
))
}

Fájl megtekintése

@@ -0,0 +1,426 @@
import SwiftUI
enum ProcessSortColumn: String {
case name, pid, cpu, memory, threads, user, state
}
struct ProcessView: View {
@EnvironmentObject var monitor: SystemMonitor
@State private var searchText = ""
@State private var selectedProcess: Set<ProcessItem.ID> = []
@State private var sortColumn: ProcessSortColumn = .cpu
@State private var sortAscending: Bool = false
@State private var displayedProcesses: [ProcessItem] = []
// Keep sortOrder for Table binding compatibility
@State private var sortOrder: [KeyPathComparator<ProcessItem>] = [
.init(\.cpuUsage, order: .reverse)
]
// Confirmation dialogs
@State private var showTerminateConfirm = false
@State private var showForceKillConfirm = false
@State private var processToKill: ProcessItem?
// "Don't ask again" preferences
@AppStorage("skipTerminateConfirm") private var skipTerminateConfirm = false
@AppStorage("skipForceKillConfirm") private var skipForceKillConfirm = false
private func updateDisplayedProcesses() {
let filtered = monitor.processes.filter {
searchText.isEmpty ||
$0.name.localizedCaseInsensitiveContains(searchText) ||
String($0.pid).contains(searchText)
}
displayedProcesses = filtered.sorted { lhs, rhs in
let comparison: ComparisonResult
switch sortColumn {
case .name:
comparison = lhs.name.localizedCaseInsensitiveCompare(rhs.name)
case .pid:
comparison = lhs.pid < rhs.pid ? .orderedAscending : (lhs.pid > rhs.pid ? .orderedDescending : .orderedSame)
case .cpu:
comparison = lhs.cpuUsage < rhs.cpuUsage ? .orderedAscending : (lhs.cpuUsage > rhs.cpuUsage ? .orderedDescending : .orderedSame)
case .memory:
comparison = lhs.memoryUsage < rhs.memoryUsage ? .orderedAscending : (lhs.memoryUsage > rhs.memoryUsage ? .orderedDescending : .orderedSame)
case .threads:
comparison = lhs.threadCount < rhs.threadCount ? .orderedAscending : (lhs.threadCount > rhs.threadCount ? .orderedDescending : .orderedSame)
case .user:
comparison = lhs.user.localizedCaseInsensitiveCompare(rhs.user)
case .state:
comparison = lhs.state.rawValue.compare(rhs.state.rawValue)
}
// Primary sort
if comparison != .orderedSame {
return sortAscending ? comparison == .orderedAscending : comparison == .orderedDescending
}
// Tiebreaker: sort by PID for stability
return lhs.pid < rhs.pid
}
}
var selectedProcessItem: ProcessItem? {
guard let pid = selectedProcess.first else { return nil }
return monitor.processes.first { $0.pid == pid }
}
var body: some View {
VStack(spacing: 0) {
// Summary bar - separate view to avoid re-rendering table
ProcessSummaryBar(processCount: displayedProcesses.count, searchText: $searchText)
Divider()
// Process table
Table(displayedProcesses, selection: $selectedProcess, sortOrder: $sortOrder) {
TableColumn("") { process in
if let icon = process.icon {
Image(nsImage: icon)
.resizable()
.frame(width: 16, height: 16)
} else {
Image(systemName: "app.dashed")
.frame(width: 16, height: 16)
}
}
.width(24)
TableColumn("Name", value: \.name) { process in
Text(process.name)
.lineLimit(1)
}
.width(min: 150, ideal: 200)
TableColumn("PID", value: \.pid) { process in
Text("\(process.pid)")
.monospacedDigit()
}
.width(60)
TableColumn("CPU %", value: \.cpuUsage) { process in
Text(String(format: "%.1f%%", process.cpuUsage))
.monospacedDigit()
.foregroundColor(cpuColor(process.cpuUsage))
}
.width(70)
TableColumn("Memory", value: \.memoryUsage) { process in
Text(formatBytes(process.memoryUsage))
.monospacedDigit()
}
.width(80)
TableColumn("Threads", value: \.threadCount) { process in
Text("\(process.threadCount)")
.monospacedDigit()
}
.width(60)
TableColumn("User", value: \.user) { process in
Text(process.user)
.lineLimit(1)
}
.width(80)
TableColumn("State", value: \.state.rawValue) { process in
HStack(spacing: 4) {
Image(systemName: process.state.symbol)
.foregroundColor(stateColor(process.state))
Text(process.state.rawValue)
}
}
.width(90)
}
.contextMenu(forSelectionType: ProcessItem.ID.self) { selection in
if let pid = selection.first {
Button("Terminate (⌫)") {
initiateTerminate()
}
Button("Force Kill (⌘⌫)") {
initiateForceKill()
}
Divider()
Button("Suspend") {
monitor.suspendProcess(pid)
}
Button("Resume") {
monitor.resumeProcess(pid)
}
Divider()
Button("Copy PID") {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString("\(pid)", forType: .string)
}
}
} primaryAction: { selection in
// Double-click action
}
.onDeleteCommand {
initiateTerminate()
}
}
.onAppear {
updateDisplayedProcesses()
}
.onChange(of: monitor.processes) { _ in
updateDisplayedProcesses()
}
.onChange(of: searchText) { _ in
updateDisplayedProcesses()
}
.onChange(of: sortColumn) { _ in
updateDisplayedProcesses()
}
.onChange(of: sortAscending) { _ in
updateDisplayedProcesses()
}
.onChange(of: sortOrder) { newOrder in
guard let comparator = newOrder.first else { return }
// Detect which column and direction from the KeyPathComparator
let ascending = comparator.order == .forward
// Use string representation of keypath to determine column
let keyPathString = String(describing: comparator)
if keyPathString.contains("cpuUsage") {
sortColumn = .cpu
} else if keyPathString.contains("memoryUsage") {
sortColumn = .memory
} else if keyPathString.contains("name") {
sortColumn = .name
} else if keyPathString.contains("pid") {
sortColumn = .pid
} else if keyPathString.contains("threadCount") {
sortColumn = .threads
} else if keyPathString.contains("user") {
sortColumn = .user
} else if keyPathString.contains("state") {
sortColumn = .state
}
sortAscending = ascending
}
.background(
KeyboardShortcutHandler(
onCommandBackspace: { initiateForceKill() }
)
)
.sheet(isPresented: $showTerminateConfirm) {
if let process = processToKill {
ConfirmationSheet(
title: "Terminate Process",
message: "Are you sure you want to terminate \"\(process.name)\" (PID: \(process.pid))?",
actionTitle: "Terminate",
isDestructive: true,
skipPreferenceKey: "skipTerminateConfirm",
onConfirm: {
monitor.terminateProcess(process.pid)
},
onCancel: {}
)
}
}
.sheet(isPresented: $showForceKillConfirm) {
if let process = processToKill {
ConfirmationSheet(
title: "Force Kill Process",
message: "Are you sure you want to force kill \"\(process.name)\" (PID: \(process.pid))?\n\nThis will immediately terminate the process without allowing it to save data.",
actionTitle: "Force Kill",
isDestructive: true,
skipPreferenceKey: "skipForceKillConfirm",
onConfirm: {
monitor.forceKillProcess(process.pid)
},
onCancel: {}
)
}
}
}
private func initiateTerminate() {
guard let process = selectedProcessItem else { return }
processToKill = process
if skipTerminateConfirm {
monitor.terminateProcess(process.pid)
} else {
showTerminateConfirm = true
}
}
private func initiateForceKill() {
guard let process = selectedProcessItem else { return }
processToKill = process
if skipForceKillConfirm {
monitor.forceKillProcess(process.pid)
} else {
showForceKillConfirm = true
}
}
private func cpuColor(_ usage: Double) -> Color {
if usage > 80 {
return .red
} else if usage > 50 {
return .orange
} else if usage > 20 {
return .yellow
}
return .primary
}
private func stateColor(_ state: ProcessState) -> Color {
switch state {
case .running: return .green
case .sleeping: return .secondary
case .stopped: return .orange
case .zombie: return .red
case .unknown: return .secondary
}
}
}
struct ConfirmationSheet: View {
let title: String
let message: String
let actionTitle: String
let isDestructive: Bool
let skipPreferenceKey: String
let onConfirm: () -> Void
let onCancel: () -> Void
@Environment(\.dismiss) private var dismiss
@State private var dontAskAgain = false
@AppStorage private var skipConfirm: Bool
init(
title: String,
message: String,
actionTitle: String,
isDestructive: Bool,
skipPreferenceKey: String,
onConfirm: @escaping () -> Void,
onCancel: @escaping () -> Void
) {
self.title = title
self.message = message
self.actionTitle = actionTitle
self.isDestructive = isDestructive
self.skipPreferenceKey = skipPreferenceKey
self.onConfirm = onConfirm
self.onCancel = onCancel
self._skipConfirm = AppStorage(wrappedValue: false, skipPreferenceKey)
}
var body: some View {
VStack(spacing: 20) {
Image(systemName: isDestructive ? "exclamationmark.triangle.fill" : "questionmark.circle.fill")
.font(.system(size: 48))
.foregroundColor(isDestructive ? .orange : .blue)
Text(title)
.font(.headline)
Text(message)
.font(.body)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
.fixedSize(horizontal: false, vertical: true)
Toggle("Don't ask again", isOn: $dontAskAgain)
.toggleStyle(.checkbox)
HStack(spacing: 12) {
Button("Cancel") {
onCancel()
dismiss()
}
.keyboardShortcut(.cancelAction)
Button(actionTitle) {
if dontAskAgain {
skipConfirm = true
}
onConfirm()
dismiss()
}
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.tint(isDestructive ? .red : .accentColor)
}
}
.padding(24)
.frame(width: 350)
}
}
struct KeyboardShortcutHandler: NSViewRepresentable {
let onCommandBackspace: () -> Void
func makeNSView(context: Context) -> NSView {
let view = KeyCaptureView()
view.onCommandBackspace = onCommandBackspace
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
if let view = nsView as? KeyCaptureView {
view.onCommandBackspace = onCommandBackspace
}
}
class KeyCaptureView: NSView {
var onCommandBackspace: (() -> Void)?
override func performKeyEquivalent(with event: NSEvent) -> Bool {
// Backspace = keyCode 51
if event.keyCode == 51 && event.modifierFlags.contains(.command) {
onCommandBackspace?()
return true
}
return super.performKeyEquivalent(with: event)
}
}
}
struct ProcessSummaryBar: View {
@EnvironmentObject var monitor: SystemMonitor
let processCount: Int
@Binding var searchText: String
var body: some View {
HStack {
Text("\(processCount) processes")
.foregroundColor(.secondary)
Spacer()
if let cpu = monitor.cpuInfo {
Text("CPU: \(String(format: "%.1f%%", cpu.globalUsage))")
.monospacedDigit()
}
if let mem = monitor.memoryInfo {
Text("Memory: \(formatBytes(mem.usedMemory)) / \(formatBytes(mem.totalMemory))")
.monospacedDigit()
}
TextField("Search...", text: $searchText)
.textFieldStyle(.roundedBorder)
.frame(width: 150)
}
.padding(.horizontal)
.padding(.vertical, 8)
.background(Color(nsColor: .windowBackgroundColor))
}
}
#Preview {
ProcessView()
.environmentObject(SystemMonitor.shared)
}