852 lines
25 KiB
Swift
852 lines
25 KiB
Swift
import Foundation
|
|
import MacToolsPluginKit
|
|
|
|
enum SystemStatusMetricKind: String, CaseIterable, Codable, Equatable, Hashable, Identifiable, Sendable {
|
|
case cpu
|
|
case gpu
|
|
case memory
|
|
case disk
|
|
case battery
|
|
case network
|
|
case topProcesses
|
|
|
|
var id: String { rawValue }
|
|
|
|
func title(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
|
|
switch self {
|
|
case .cpu:
|
|
return "CPU"
|
|
case .gpu:
|
|
return "GPU"
|
|
case .memory:
|
|
return localization.string("metric.memory", defaultValue: "内存")
|
|
case .disk:
|
|
return localization.string("metric.disk", defaultValue: "磁盘")
|
|
case .battery:
|
|
return localization.string("metric.battery", defaultValue: "电量")
|
|
case .network:
|
|
return localization.string("metric.network", defaultValue: "网络")
|
|
case .topProcesses:
|
|
return localization.string("metric.topProcesses", defaultValue: "进程")
|
|
}
|
|
}
|
|
|
|
var symbolName: String {
|
|
switch self {
|
|
case .cpu:
|
|
return "cpu"
|
|
case .gpu:
|
|
return "display"
|
|
case .memory:
|
|
return "memorychip"
|
|
case .disk:
|
|
return "internaldrive"
|
|
case .battery:
|
|
return "battery.75percent"
|
|
case .network:
|
|
return "wifi"
|
|
case .topProcesses:
|
|
return "list.bullet.rectangle"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SystemStatusGridPosition: Equatable, Sendable {
|
|
let row: Int
|
|
let column: Int
|
|
}
|
|
|
|
enum SystemStatusComponentRow: Equatable, Sendable {
|
|
case metrics([SystemStatusMetricKind])
|
|
case topProcesses
|
|
}
|
|
|
|
enum SystemStatusComponentLayout {
|
|
static let cardCornerRadius = PluginComponentPanelLayoutMetrics.cardCornerRadius
|
|
static let cardSpacing: CGFloat = 6
|
|
static let cardContentPadding: CGFloat = 8
|
|
static let columns = 2
|
|
static let metricRows = 3
|
|
|
|
static let dashboardSectionSpacing: CGFloat = cardSpacing
|
|
static let dashboardMetricTileHeight: CGFloat = 99
|
|
static let dashboardLowerTileHeight: CGFloat = 96
|
|
static let dashboardMetricGridHeight = CGFloat(metricRows) * dashboardMetricTileHeight
|
|
+ CGFloat(max(metricRows - 1, 0)) * cardSpacing
|
|
static let emptyContentHeight = dashboardLowerTileHeight
|
|
static let dashboardContentHeight = contentHeight(for: defaultPanelMetricKinds)
|
|
|
|
static let defaultPanelMetricKinds: [SystemStatusMetricKind] = [
|
|
.cpu,
|
|
.gpu,
|
|
.network,
|
|
.disk,
|
|
.memory,
|
|
.battery,
|
|
.topProcesses
|
|
]
|
|
|
|
static let defaultMenuBarMetricKinds: [SystemStatusMetricKind] = [
|
|
.cpu,
|
|
.gpu,
|
|
.network,
|
|
.disk,
|
|
.memory,
|
|
.battery
|
|
]
|
|
|
|
static func position(for metric: SystemStatusMetricKind) -> SystemStatusGridPosition? {
|
|
let metricKinds = defaultPanelMetricKinds.filter { $0 != .topProcesses }
|
|
guard let index = metricKinds.firstIndex(of: metric) else {
|
|
return nil
|
|
}
|
|
|
|
return SystemStatusGridPosition(
|
|
row: index / columns,
|
|
column: index % columns
|
|
)
|
|
}
|
|
|
|
static func rows(for kinds: [SystemStatusMetricKind]) -> [SystemStatusComponentRow] {
|
|
var rows: [SystemStatusComponentRow] = []
|
|
var pendingMetrics: [SystemStatusMetricKind] = []
|
|
|
|
func flushPendingMetrics() {
|
|
guard !pendingMetrics.isEmpty else {
|
|
return
|
|
}
|
|
|
|
rows.append(.metrics(pendingMetrics))
|
|
pendingMetrics.removeAll(keepingCapacity: true)
|
|
}
|
|
|
|
for kind in kinds {
|
|
if kind == .topProcesses {
|
|
flushPendingMetrics()
|
|
rows.append(.topProcesses)
|
|
continue
|
|
}
|
|
|
|
pendingMetrics.append(kind)
|
|
if pendingMetrics.count == columns {
|
|
flushPendingMetrics()
|
|
}
|
|
}
|
|
|
|
flushPendingMetrics()
|
|
return rows
|
|
}
|
|
|
|
static func contentHeight(for kinds: [SystemStatusMetricKind]) -> CGFloat {
|
|
let rows = rows(for: kinds)
|
|
guard !rows.isEmpty else {
|
|
return emptyContentHeight
|
|
}
|
|
|
|
let rowHeight = rows.reduce(CGFloat(0)) { partialResult, row in
|
|
switch row {
|
|
case .metrics:
|
|
return partialResult + dashboardMetricTileHeight
|
|
case .topProcesses:
|
|
return partialResult + dashboardLowerTileHeight
|
|
}
|
|
}
|
|
let spacing = CGFloat(max(rows.count - 1, 0)) * dashboardSectionSpacing
|
|
return rowHeight + spacing
|
|
}
|
|
}
|
|
|
|
struct SystemStatusSnapshot: Equatable, Sendable {
|
|
var cpu: SystemStatusCPUSnapshot
|
|
var gpu: SystemStatusGPUSnapshot
|
|
var memory: SystemStatusMemorySnapshot
|
|
var disk: SystemStatusDiskSnapshot
|
|
var battery: SystemStatusBatterySnapshot
|
|
var network: SystemStatusNetworkSnapshot
|
|
var topProcesses: [SystemStatusTopProcess]
|
|
var hardware: SystemStatusHardwareSnapshot
|
|
var history: [SystemStatusHistoryPoint]
|
|
|
|
static let empty = SystemStatusSnapshot(
|
|
cpu: .empty,
|
|
gpu: .empty,
|
|
memory: .empty,
|
|
disk: .empty,
|
|
battery: .empty,
|
|
network: .empty,
|
|
topProcesses: [],
|
|
hardware: .empty,
|
|
history: []
|
|
)
|
|
}
|
|
|
|
struct SystemStatusFastSample: Equatable, Sendable {
|
|
let cpu: SystemStatusCPUSnapshot
|
|
let memory: SystemStatusMemorySnapshot
|
|
let network: SystemStatusNetworkSnapshot
|
|
let disk: SystemStatusDiskSnapshot
|
|
}
|
|
|
|
struct SystemStatusSlowSample: Equatable, Sendable {
|
|
let disk: SystemStatusDiskSnapshot
|
|
let battery: SystemStatusBatterySnapshot
|
|
let gpu: SystemStatusGPUSnapshot
|
|
let hardware: SystemStatusHardwareSnapshot
|
|
}
|
|
|
|
struct SystemStatusCPUSnapshot: Equatable, Sendable {
|
|
let usage: Double?
|
|
let loadAverage1Minute: Double?
|
|
let temperatureCelsius: Double?
|
|
let systemPowerWatts: Double?
|
|
let isCollecting: Bool
|
|
|
|
static let empty = SystemStatusCPUSnapshot(
|
|
usage: nil,
|
|
loadAverage1Minute: nil,
|
|
temperatureCelsius: nil,
|
|
systemPowerWatts: nil,
|
|
isCollecting: true
|
|
)
|
|
}
|
|
|
|
struct SystemStatusGPUSnapshot: Equatable, Sendable {
|
|
let usage: Double?
|
|
let name: String?
|
|
let temperatureCelsius: Double?
|
|
let isAvailable: Bool
|
|
let isCollecting: Bool
|
|
|
|
static let empty = SystemStatusGPUSnapshot(
|
|
usage: nil,
|
|
name: nil,
|
|
temperatureCelsius: nil,
|
|
isAvailable: false,
|
|
isCollecting: true
|
|
)
|
|
}
|
|
|
|
struct SystemStatusMemorySnapshot: Equatable, Sendable {
|
|
let usedBytes: UInt64?
|
|
let totalBytes: UInt64?
|
|
let swapUsedBytes: UInt64?
|
|
let swapTotalBytes: UInt64?
|
|
|
|
var usage: Double? {
|
|
guard let usedBytes, let totalBytes, totalBytes > 0 else {
|
|
return nil
|
|
}
|
|
|
|
return min(max(Double(usedBytes) / Double(totalBytes), 0), 1)
|
|
}
|
|
|
|
static let empty = SystemStatusMemorySnapshot(
|
|
usedBytes: nil,
|
|
totalBytes: nil,
|
|
swapUsedBytes: nil,
|
|
swapTotalBytes: nil
|
|
)
|
|
}
|
|
|
|
struct SystemStatusDiskSnapshot: Equatable, Sendable {
|
|
let usedBytes: UInt64?
|
|
let totalBytes: UInt64?
|
|
let readBytesPerSecond: UInt64?
|
|
let writeBytesPerSecond: UInt64?
|
|
|
|
var usage: Double? {
|
|
guard let usedBytes, let totalBytes, totalBytes > 0 else {
|
|
return nil
|
|
}
|
|
|
|
return min(max(Double(usedBytes) / Double(totalBytes), 0), 1)
|
|
}
|
|
|
|
static let empty = SystemStatusDiskSnapshot(
|
|
usedBytes: nil,
|
|
totalBytes: nil,
|
|
readBytesPerSecond: nil,
|
|
writeBytesPerSecond: nil
|
|
)
|
|
|
|
func replacingActivity(from disk: SystemStatusDiskSnapshot) -> SystemStatusDiskSnapshot {
|
|
SystemStatusDiskSnapshot(
|
|
usedBytes: usedBytes,
|
|
totalBytes: totalBytes,
|
|
readBytesPerSecond: disk.readBytesPerSecond,
|
|
writeBytesPerSecond: disk.writeBytesPerSecond
|
|
)
|
|
}
|
|
|
|
func replacingCapacity(from disk: SystemStatusDiskSnapshot) -> SystemStatusDiskSnapshot {
|
|
SystemStatusDiskSnapshot(
|
|
usedBytes: disk.usedBytes,
|
|
totalBytes: disk.totalBytes,
|
|
readBytesPerSecond: readBytesPerSecond,
|
|
writeBytesPerSecond: writeBytesPerSecond
|
|
)
|
|
}
|
|
}
|
|
|
|
enum SystemStatusBatteryState: Equatable, Sendable {
|
|
case charging
|
|
case charged
|
|
case unplugged
|
|
case acPower
|
|
case unavailable
|
|
case unknown
|
|
|
|
func title(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
|
|
switch self {
|
|
case .charging:
|
|
return localization.string("battery.state.charging", defaultValue: "充电中")
|
|
case .charged:
|
|
return localization.string("battery.state.charged", defaultValue: "已充满")
|
|
case .unplugged:
|
|
return localization.string("battery.state.unplugged", defaultValue: "使用电池")
|
|
case .acPower:
|
|
return localization.string("battery.state.acPower", defaultValue: "外接电源")
|
|
case .unavailable:
|
|
return localization.string("battery.state.unavailable", defaultValue: "无电池")
|
|
case .unknown:
|
|
return localization.string("battery.state.unknown", defaultValue: "未知")
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SystemStatusBatterySnapshot: Equatable, Sendable {
|
|
let isAvailable: Bool
|
|
let level: Double?
|
|
let state: SystemStatusBatteryState
|
|
let timeRemainingMinutes: Int?
|
|
let adapterWatts: Int?
|
|
let batteryPowerWatts: Double?
|
|
let temperatureCelsius: Double?
|
|
let healthPercent: Int?
|
|
let cycleCount: Int?
|
|
|
|
static let empty = SystemStatusBatterySnapshot(
|
|
isAvailable: false,
|
|
level: nil,
|
|
state: .unknown,
|
|
timeRemainingMinutes: nil,
|
|
adapterWatts: nil,
|
|
batteryPowerWatts: nil,
|
|
temperatureCelsius: nil,
|
|
healthPercent: nil,
|
|
cycleCount: nil
|
|
)
|
|
}
|
|
|
|
struct SystemStatusNetworkSnapshot: Equatable, Sendable {
|
|
let interfaceName: String?
|
|
let ipAddress: String?
|
|
let publicIPAddress: String?
|
|
let downloadBytesPerSecond: UInt64?
|
|
let uploadBytesPerSecond: UInt64?
|
|
let isConnected: Bool
|
|
let isCollecting: Bool
|
|
|
|
static let empty = SystemStatusNetworkSnapshot(
|
|
interfaceName: nil,
|
|
ipAddress: nil,
|
|
publicIPAddress: nil,
|
|
downloadBytesPerSecond: nil,
|
|
uploadBytesPerSecond: nil,
|
|
isConnected: false,
|
|
isCollecting: true
|
|
)
|
|
|
|
func replacingPublicIPAddress(_ publicIPAddress: String?) -> SystemStatusNetworkSnapshot {
|
|
SystemStatusNetworkSnapshot(
|
|
interfaceName: interfaceName,
|
|
ipAddress: ipAddress,
|
|
publicIPAddress: publicIPAddress,
|
|
downloadBytesPerSecond: downloadBytesPerSecond,
|
|
uploadBytesPerSecond: uploadBytesPerSecond,
|
|
isConnected: isConnected,
|
|
isCollecting: isCollecting
|
|
)
|
|
}
|
|
}
|
|
|
|
struct SystemStatusTopProcess: Identifiable, Equatable, Sendable {
|
|
let pid: Int
|
|
let displayName: String
|
|
let command: String
|
|
let cpuPercent: Double
|
|
let memoryPercent: Double
|
|
let memoryBytes: UInt64?
|
|
|
|
var id: Int { pid }
|
|
|
|
func replacingDisplayName(_ displayName: String) -> SystemStatusTopProcess {
|
|
SystemStatusTopProcess(
|
|
pid: pid,
|
|
displayName: displayName,
|
|
command: command,
|
|
cpuPercent: cpuPercent,
|
|
memoryPercent: memoryPercent,
|
|
memoryBytes: memoryBytes
|
|
)
|
|
}
|
|
}
|
|
|
|
struct SystemStatusHardwareSnapshot: Equatable, Sendable {
|
|
let modelName: String?
|
|
let chipName: String?
|
|
let macOSVersion: String
|
|
let uptimeSeconds: TimeInterval?
|
|
let totalMemoryBytes: UInt64?
|
|
|
|
static let empty = SystemStatusHardwareSnapshot(
|
|
modelName: nil,
|
|
chipName: nil,
|
|
macOSVersion: ProcessInfo.processInfo.operatingSystemVersionString,
|
|
uptimeSeconds: nil,
|
|
totalMemoryBytes: nil
|
|
)
|
|
|
|
func replacingUptime(_ uptimeSeconds: TimeInterval?) -> SystemStatusHardwareSnapshot {
|
|
SystemStatusHardwareSnapshot(
|
|
modelName: modelName,
|
|
chipName: chipName,
|
|
macOSVersion: macOSVersion,
|
|
uptimeSeconds: uptimeSeconds,
|
|
totalMemoryBytes: totalMemoryBytes
|
|
)
|
|
}
|
|
}
|
|
|
|
struct SystemStatusHistoryPoint: Codable, Equatable, Sendable {
|
|
let timestamp: TimeInterval
|
|
let cpuUsage: Double?
|
|
let gpuUsage: Double?
|
|
let memoryUsage: Double?
|
|
let diskUsage: Double?
|
|
let diskReadBytesPerSecond: UInt64?
|
|
let diskWriteBytesPerSecond: UInt64?
|
|
let networkDownloadBytesPerSecond: UInt64?
|
|
let networkUploadBytesPerSecond: UInt64?
|
|
let batteryLevel: Double?
|
|
|
|
init(timestamp: TimeInterval, snapshot: SystemStatusSnapshot) {
|
|
self.timestamp = timestamp
|
|
self.cpuUsage = snapshot.cpu.usage
|
|
self.gpuUsage = snapshot.gpu.usage
|
|
self.memoryUsage = snapshot.memory.usage
|
|
self.diskUsage = snapshot.disk.usage
|
|
self.diskReadBytesPerSecond = snapshot.disk.readBytesPerSecond
|
|
self.diskWriteBytesPerSecond = snapshot.disk.writeBytesPerSecond
|
|
self.networkDownloadBytesPerSecond = snapshot.network.downloadBytesPerSecond
|
|
self.networkUploadBytesPerSecond = snapshot.network.uploadBytesPerSecond
|
|
self.batteryLevel = snapshot.battery.level
|
|
}
|
|
|
|
init(
|
|
timestamp: TimeInterval,
|
|
cpuUsage: Double? = nil,
|
|
gpuUsage: Double? = nil,
|
|
memoryUsage: Double? = nil,
|
|
diskUsage: Double? = nil,
|
|
diskReadBytesPerSecond: UInt64? = nil,
|
|
diskWriteBytesPerSecond: UInt64? = nil,
|
|
networkDownloadBytesPerSecond: UInt64? = nil,
|
|
networkUploadBytesPerSecond: UInt64? = nil,
|
|
batteryLevel: Double? = nil
|
|
) {
|
|
self.timestamp = timestamp
|
|
self.cpuUsage = cpuUsage
|
|
self.gpuUsage = gpuUsage
|
|
self.memoryUsage = memoryUsage
|
|
self.diskUsage = diskUsage
|
|
self.diskReadBytesPerSecond = diskReadBytesPerSecond
|
|
self.diskWriteBytesPerSecond = diskWriteBytesPerSecond
|
|
self.networkDownloadBytesPerSecond = networkDownloadBytesPerSecond
|
|
self.networkUploadBytesPerSecond = networkUploadBytesPerSecond
|
|
self.batteryLevel = batteryLevel
|
|
}
|
|
}
|
|
|
|
struct SystemStatusHistoryDocument: Codable, Equatable, Sendable {
|
|
let schemaVersion: Int
|
|
var samples: [SystemStatusHistoryPoint]
|
|
}
|
|
|
|
struct SystemStatusCPUTicks: Equatable, Sendable {
|
|
let user: UInt64
|
|
let system: UInt64
|
|
let idle: UInt64
|
|
let nice: UInt64
|
|
}
|
|
|
|
enum SystemStatusCPUUsageCalculator {
|
|
static func usage(current: SystemStatusCPUTicks, previous: SystemStatusCPUTicks) -> Double? {
|
|
let user = positiveDelta(current.user, previous.user)
|
|
let system = positiveDelta(current.system, previous.system)
|
|
let idle = positiveDelta(current.idle, previous.idle)
|
|
let nice = positiveDelta(current.nice, previous.nice)
|
|
let active = user + system + nice
|
|
let total = active + idle
|
|
|
|
guard total > 0 else {
|
|
return nil
|
|
}
|
|
|
|
return min(max(Double(active) / Double(total), 0), 1)
|
|
}
|
|
|
|
private static func positiveDelta(_ current: UInt64, _ previous: UInt64) -> UInt64 {
|
|
current >= previous ? current - previous : 0
|
|
}
|
|
}
|
|
|
|
enum SystemStatusPowerNormalizer {
|
|
static func energyJoules(from value: Double, unit: String) -> Double? {
|
|
switch unit {
|
|
case "mJ":
|
|
return value / 1_000
|
|
case "uJ":
|
|
return value / 1_000_000
|
|
case "nJ":
|
|
return value / 1_000_000_000
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
enum SystemStatusBatteryPowerNormalizer {
|
|
static func telemetryWatts(fromRawMilliwatts rawValue: Any?) -> Double? {
|
|
telemetryWatts(fromMilliwatts: signedNumberValue(rawValue))
|
|
}
|
|
|
|
static func telemetryWatts(fromMilliwatts milliwatts: Double?) -> Double? {
|
|
guard let milliwatts, milliwatts.isFinite else {
|
|
return nil
|
|
}
|
|
|
|
let signedMilliwatts = twosComplementSignedValueIfNeeded(milliwatts)
|
|
return validBatteryWatts(signedMilliwatts / 1_000)
|
|
}
|
|
|
|
static func derivedWatts(voltageMillivolts: Double?, amperageMilliamps: Double?) -> Double? {
|
|
guard
|
|
let voltageMillivolts,
|
|
let amperageMilliamps,
|
|
voltageMillivolts > 0,
|
|
amperageMilliamps != 0
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
return validBatteryWatts(-(voltageMillivolts * amperageMilliamps) / 1_000_000)
|
|
}
|
|
|
|
private static func signedNumberValue(_ rawValue: Any?) -> Double? {
|
|
if let intValue = rawValue as? Int {
|
|
return Double(intValue)
|
|
}
|
|
if let int64Value = rawValue as? Int64 {
|
|
return Double(int64Value)
|
|
}
|
|
if let uint64Value = rawValue as? UInt64 {
|
|
return signedDoubleValue(fromUnsigned: uint64Value)
|
|
}
|
|
if let doubleValue = rawValue as? Double {
|
|
return doubleValue
|
|
}
|
|
if let numberValue = rawValue as? NSNumber {
|
|
if numberValue.doubleValue > Double(Int64.max) {
|
|
return signedDoubleValue(fromUnsigned: numberValue.uint64Value)
|
|
}
|
|
return numberValue.doubleValue
|
|
}
|
|
if let stringValue = rawValue as? String {
|
|
if let int64Value = Int64(stringValue) {
|
|
return Double(int64Value)
|
|
}
|
|
if let uint64Value = UInt64(stringValue) {
|
|
return signedDoubleValue(fromUnsigned: uint64Value)
|
|
}
|
|
return Double(stringValue)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private static func signedDoubleValue(fromUnsigned value: UInt64) -> Double {
|
|
guard value > UInt64(Int64.max) else {
|
|
return Double(value)
|
|
}
|
|
|
|
let magnitude = ~value &+ 1
|
|
return -Double(magnitude)
|
|
}
|
|
|
|
private static func twosComplementSignedValueIfNeeded(_ value: Double) -> Double {
|
|
guard value > Double(Int64.max) else {
|
|
return value
|
|
}
|
|
|
|
return value - pow(2, 64)
|
|
}
|
|
|
|
private static func validBatteryWatts(_ watts: Double) -> Double? {
|
|
guard watts.isFinite, watts > -200, watts < 200 else {
|
|
return nil
|
|
}
|
|
|
|
return watts
|
|
}
|
|
}
|
|
|
|
struct SystemStatusPowerCalculator {
|
|
static func watts(
|
|
current: SystemStatusPowerEnergySample,
|
|
previous: SystemStatusPowerEnergySample
|
|
) -> Double? {
|
|
let elapsedSeconds = current.date.timeIntervalSince(previous.date)
|
|
guard elapsedSeconds > 0, current.joules >= previous.joules else {
|
|
return nil
|
|
}
|
|
|
|
let watts = (current.joules - previous.joules) / elapsedSeconds
|
|
guard watts >= 0, watts < 1_000 else {
|
|
return nil
|
|
}
|
|
|
|
return watts
|
|
}
|
|
}
|
|
|
|
struct SystemStatusNetworkCounter: Equatable, Sendable {
|
|
let key: String
|
|
let displayName: String
|
|
let receivedBytes: UInt64
|
|
let sentBytes: UInt64
|
|
let ipAddress: String?
|
|
let isUp: Bool
|
|
|
|
func replacingCounters(from counter: SystemStatusNetworkCounter?) -> SystemStatusNetworkCounter {
|
|
guard let counter else {
|
|
return self
|
|
}
|
|
|
|
return SystemStatusNetworkCounter(
|
|
key: counter.key,
|
|
displayName: displayName,
|
|
receivedBytes: counter.receivedBytes,
|
|
sentBytes: counter.sentBytes,
|
|
ipAddress: ipAddress,
|
|
isUp: isUp
|
|
)
|
|
}
|
|
}
|
|
|
|
struct SystemStatusNetworkRate: Equatable, Sendable {
|
|
let downloadBytesPerSecond: UInt64
|
|
let uploadBytesPerSecond: UInt64
|
|
}
|
|
|
|
enum SystemStatusNetworkRateCalculator {
|
|
private static let maximumBytesPerSecond: UInt64 = 2_000_000_000
|
|
|
|
static func rate(
|
|
current: SystemStatusNetworkCounter,
|
|
previous: SystemStatusNetworkCounter,
|
|
elapsedSeconds: TimeInterval
|
|
) -> SystemStatusNetworkRate? {
|
|
guard elapsedSeconds > 0 else {
|
|
return nil
|
|
}
|
|
|
|
let receivedDelta = positiveDelta(current.receivedBytes, previous.receivedBytes)
|
|
let sentDelta = positiveDelta(current.sentBytes, previous.sentBytes)
|
|
|
|
return SystemStatusNetworkRate(
|
|
downloadBytesPerSecond: clampedBytesPerSecond(receivedDelta, elapsedSeconds: elapsedSeconds),
|
|
uploadBytesPerSecond: clampedBytesPerSecond(sentDelta, elapsedSeconds: elapsedSeconds)
|
|
)
|
|
}
|
|
|
|
private static func positiveDelta(_ current: UInt64, _ previous: UInt64) -> UInt64 {
|
|
current >= previous ? current - previous : 0
|
|
}
|
|
|
|
private static func clampedBytesPerSecond(_ delta: UInt64, elapsedSeconds: TimeInterval) -> UInt64 {
|
|
let bytesPerSecond = UInt64(Double(delta) / elapsedSeconds)
|
|
guard bytesPerSecond <= maximumBytesPerSecond else {
|
|
return 0
|
|
}
|
|
|
|
return bytesPerSecond
|
|
}
|
|
}
|
|
|
|
struct SystemStatusDiskIOCounter: Equatable, Sendable {
|
|
let readBytes: UInt64
|
|
let writeBytes: UInt64
|
|
}
|
|
|
|
struct SystemStatusDiskIORate: Equatable, Sendable {
|
|
let readBytesPerSecond: UInt64
|
|
let writeBytesPerSecond: UInt64
|
|
}
|
|
|
|
enum SystemStatusDiskIORateCalculator {
|
|
private static let maximumBytesPerSecond: UInt64 = 10_000_000_000
|
|
|
|
static func rate(
|
|
current: SystemStatusDiskIOCounter,
|
|
previous: SystemStatusDiskIOCounter,
|
|
elapsedSeconds: TimeInterval
|
|
) -> SystemStatusDiskIORate? {
|
|
guard elapsedSeconds > 0 else {
|
|
return nil
|
|
}
|
|
|
|
let readDelta = positiveDelta(current.readBytes, previous.readBytes)
|
|
let writeDelta = positiveDelta(current.writeBytes, previous.writeBytes)
|
|
|
|
return SystemStatusDiskIORate(
|
|
readBytesPerSecond: clampedBytesPerSecond(readDelta, elapsedSeconds: elapsedSeconds),
|
|
writeBytesPerSecond: clampedBytesPerSecond(writeDelta, elapsedSeconds: elapsedSeconds)
|
|
)
|
|
}
|
|
|
|
private static func positiveDelta(_ current: UInt64, _ previous: UInt64) -> UInt64 {
|
|
current >= previous ? current - previous : 0
|
|
}
|
|
|
|
private static func clampedBytesPerSecond(_ delta: UInt64, elapsedSeconds: TimeInterval) -> UInt64 {
|
|
let bytesPerSecond = UInt64(Double(delta) / elapsedSeconds)
|
|
guard bytesPerSecond <= maximumBytesPerSecond else {
|
|
return 0
|
|
}
|
|
|
|
return bytesPerSecond
|
|
}
|
|
}
|
|
|
|
enum SystemStatusFormatter {
|
|
static func percent(_ value: Double?, fractionDigits: Int = 0) -> String {
|
|
guard let value else {
|
|
return "—"
|
|
}
|
|
|
|
return numericPercent(value * 100, fractionDigits: fractionDigits)
|
|
}
|
|
|
|
static func wholePercent(_ value: Double?, fractionDigits: Int = 0) -> String {
|
|
guard let value else {
|
|
return "—"
|
|
}
|
|
|
|
return numericPercent(value, fractionDigits: fractionDigits)
|
|
}
|
|
|
|
static func bytes(_ bytes: UInt64?) -> String {
|
|
guard let bytes else {
|
|
return "—"
|
|
}
|
|
|
|
return scaledBytes(bytes)
|
|
}
|
|
|
|
static func speed(_ bytesPerSecond: UInt64?) -> String {
|
|
guard let bytesPerSecond else {
|
|
return "—"
|
|
}
|
|
|
|
return "\(scaledBytes(bytesPerSecond))/s"
|
|
}
|
|
|
|
static func temperature(_ celsius: Double?) -> String {
|
|
guard let celsius else {
|
|
return "—°C"
|
|
}
|
|
|
|
return "\(format(celsius, fractionDigits: 0))°C"
|
|
}
|
|
|
|
static func power(_ watts: Double?) -> String {
|
|
guard let watts else {
|
|
return "—W"
|
|
}
|
|
|
|
let fractionDigits = watts < 10 ? 1 : 0
|
|
return "\(format(watts, fractionDigits: fractionDigits))W"
|
|
}
|
|
|
|
static func rpm(_ rpm: Double?) -> String {
|
|
guard let rpm else {
|
|
return "—"
|
|
}
|
|
|
|
return "\(format(rpm, fractionDigits: 0)) RPM"
|
|
}
|
|
|
|
static func timeRemaining(
|
|
minutes: Int?,
|
|
localization: PluginLocalization = PluginLocalization(bundle: .main)
|
|
) -> String {
|
|
guard let minutes, minutes >= 0 else {
|
|
return localization.string("battery.timeRemaining.estimating", defaultValue: "估算中")
|
|
}
|
|
|
|
if minutes < 60 {
|
|
return "\(minutes)m"
|
|
}
|
|
|
|
let hours = minutes / 60
|
|
let remainingMinutes = minutes % 60
|
|
guard remainingMinutes > 0 else {
|
|
return "\(hours)h"
|
|
}
|
|
|
|
return "\(hours)h \(remainingMinutes)m"
|
|
}
|
|
|
|
static func uptime(_ seconds: TimeInterval?) -> String {
|
|
guard let seconds, seconds >= 0 else {
|
|
return "—"
|
|
}
|
|
|
|
let totalHours = Int(seconds / 3_600)
|
|
let days = totalHours / 24
|
|
let hours = totalHours % 24
|
|
if days > 0 {
|
|
return "\(days)d \(hours)h"
|
|
}
|
|
|
|
return "\(max(totalHours, 0))h"
|
|
}
|
|
|
|
private static func numericPercent(_ value: Double, fractionDigits: Int) -> String {
|
|
let clampedFractionDigits = max(fractionDigits, 0)
|
|
return "\(format(value, fractionDigits: clampedFractionDigits))%"
|
|
}
|
|
|
|
private static func scaledBytes(_ bytes: UInt64) -> String {
|
|
let units = ["B", "KB", "MB", "GB", "TB"]
|
|
var value = Double(bytes)
|
|
var unitIndex = 0
|
|
|
|
while value >= 1024, unitIndex < units.count - 1 {
|
|
value /= 1024
|
|
unitIndex += 1
|
|
}
|
|
|
|
let fractionDigits = unitIndex == 0 || value >= 100 ? 0 : 1
|
|
return "\(format(value, fractionDigits: fractionDigits)) \(units[unitIndex])"
|
|
}
|
|
|
|
private static func format(_ value: Double, fractionDigits: Int) -> String {
|
|
if fractionDigits == 0 {
|
|
return String(Int(value.rounded()))
|
|
}
|
|
|
|
return String(format: "%.\(fractionDigits)f", value)
|
|
}
|
|
}
|