Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

1352 lines
46 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import AppKit
import SwiftUI
import MacToolsPluginKit
private enum SystemStatusHUDLayout {
static let outerPadding: CGFloat = 0
static let sectionSpacing: CGFloat = SystemStatusComponentLayout.dashboardSectionSpacing
static let metricSpacing: CGFloat = SystemStatusComponentLayout.cardSpacing
static let metricTileHeight: CGFloat = SystemStatusComponentLayout.dashboardMetricTileHeight
static let metricInternalSpacing: CGFloat = 3
static let metricTitleHeight: CGFloat = 20
static let metricValueHeight: CGFloat = 19
static let metricVisualHeight: CGFloat = 22
static let metricFootnoteHeight: CGFloat = 11
static let lowerTileHeight: CGFloat = SystemStatusComponentLayout.dashboardLowerTileHeight
static let processRowHeight: CGFloat = 17
static let processListSpacing: CGFloat = metricInternalSpacing
static let processLimit = 3
static let processListHeight = CGFloat(processLimit) * processRowHeight
+ CGFloat(max(processLimit - 1, 0)) * processListSpacing
static let chartDisplayInterval: TimeInterval = 30 * 60
static let chartSampleLimit = 120
}
struct SystemStatusDashboardView: View {
let snapshot: SystemStatusSnapshot
let visibleKinds: [SystemStatusMetricKind]
let localization: PluginLocalization
private var metricColumns: [GridItem] {
[
GridItem(.flexible(), spacing: SystemStatusHUDLayout.metricSpacing),
GridItem(.flexible(), spacing: SystemStatusHUDLayout.metricSpacing)
]
}
var body: some View {
Group {
if visibleRows.isEmpty {
emptyState
} else {
VStack(alignment: .leading, spacing: SystemStatusHUDLayout.sectionSpacing) {
ForEach(Array(visibleRows.enumerated()), id: \.offset) { _, row in
rowView(row)
}
}
}
}
.padding(SystemStatusHUDLayout.outerPadding)
.frame(
height: SystemStatusComponentLayout.contentHeight(for: visibleKinds),
alignment: .topLeading
)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
private var visibleRows: [SystemStatusComponentRow] {
SystemStatusComponentLayout.rows(for: visibleKinds)
}
@ViewBuilder
private func rowView(_ row: SystemStatusComponentRow) -> some View {
switch row {
case let .metrics(kinds):
metricRow(kinds)
case .topProcesses:
topProcessesSection
}
}
private func metricRow(_ kinds: [SystemStatusMetricKind]) -> some View {
let visibleHistory = chartHistory
return LazyVGrid(columns: metricColumns, spacing: SystemStatusHUDLayout.metricSpacing) {
ForEach(kinds) { kind in
metricTile(kind, history: visibleHistory)
}
}
.frame(height: SystemStatusComponentLayout.dashboardMetricTileHeight, alignment: .top)
}
@ViewBuilder
private func metricTile(_ kind: SystemStatusMetricKind, history: [SystemStatusHistoryPoint]) -> some View {
switch kind {
case .cpu:
cpuTile(history: history)
case .gpu:
gpuTile(history: history)
case .network:
networkTile(history: history)
case .disk:
diskTile(history: history)
case .memory:
memoryTile(history: history)
case .battery:
batteryTile(history: history)
case .topProcesses:
EmptyView()
}
}
private var emptyState: some View {
VStack(spacing: 8) {
Image(systemName: "slider.horizontal.3")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(SystemStatusHUDPalette.textSecondary)
Text(localization.string("component.empty.title", defaultValue: "未选择显示内容"))
.font(SystemStatusHUDFont.sans(11, .semibold))
.foregroundStyle(SystemStatusHUDPalette.textSecondary)
}
.frame(maxWidth: .infinity, minHeight: SystemStatusComponentLayout.emptyContentHeight)
.background(SystemStatusHUDCardBackground())
}
private func cpuTile(history: [SystemStatusHistoryPoint]) -> some View {
let value = percentParts(snapshot.cpu.usage)
return SystemStatusHUDValueTile(
eyebrow: "CPU",
glyph: "cpu",
accent: SystemStatusHUDPalette.green,
chartColor: SystemStatusHUDPalette.green,
value: value.value,
unit: value.unit,
chip: temperatureChip(snapshot.cpu.temperatureCelsius),
values: percentHistory(\.cpuUsage, fallback: snapshot.cpu.usage, history: history),
chartStyle: .bars,
footnote: cpuFootnote
)
}
private func gpuTile(history: [SystemStatusHistoryPoint]) -> some View {
let value = snapshot.gpu.isAvailable
? percentParts(snapshot.gpu.usage)
: (value: "—", unit: "")
return SystemStatusHUDValueTile(
eyebrow: "GPU",
glyph: "cpu.fill",
accent: SystemStatusHUDPalette.gpu,
chartColor: SystemStatusHUDPalette.gpu,
value: value.value,
unit: value.unit,
chip: temperatureChip(snapshot.gpu.temperatureCelsius),
values: percentHistory(\.gpuUsage, fallback: snapshot.gpu.usage, history: history),
chartStyle: .bars,
footnote: gpuFootnote
)
}
private func memoryTile(history: [SystemStatusHistoryPoint]) -> some View {
let value = percentParts(snapshot.memory.usage)
return SystemStatusHUDValueTile(
eyebrow: SystemStatusMetricKind.memory.title(localization: localization),
glyph: "memorychip",
accent: SystemStatusHUDPalette.amber,
chartColor: SystemStatusHUDPalette.amber,
value: value.value,
unit: value.unit,
chip: memoryChip,
values: percentHistory(\.memoryUsage, fallback: snapshot.memory.usage, history: history),
chartStyle: .area,
footnote: memoryFootnote
)
}
private func diskTile(history: [SystemStatusHistoryPoint]) -> some View {
let rate = rateParts(totalDiskBytesPerSecond)
let free = bytesParts(diskFreeBytes)
return SystemStatusHUDDiskTile(
title: SystemStatusMetricKind.disk.title(localization: localization),
freeValue: free.value,
freeUnit: diskAvailableUnit(free.unit),
totalText: SystemStatusFormatter.bytes(snapshot.disk.totalBytes),
readText: SystemStatusFormatter.speed(snapshot.disk.readBytesPerSecond),
writeText: SystemStatusFormatter.speed(snapshot.disk.writeBytesPerSecond),
readValues: diskReadHistory(history),
writeValues: diskWriteHistory(history),
readColor: SystemStatusHUDPalette.diskRead,
writeColor: SystemStatusHUDPalette.diskWrite
)
.accessibilityElement(children: .combine)
.accessibilityLabel(
localization.format(
"accessibility.diskFormat",
defaultValue: "磁盘 %@%@%@",
rate.value,
rate.unit,
diskFootnote
)
)
}
private func networkTile(history: [SystemStatusHistoryPoint]) -> some View {
let rate = rateParts(totalNetworkBytesPerSecond)
return SystemStatusHUDMetricTile(
title: SystemStatusMetricKind.network.title(localization: localization),
glyph: "network",
accent: SystemStatusHUDPalette.network,
value: rate.value,
unit: rate.unit,
chip: networkChip,
footnote: nil,
footer: {
SystemStatusHUDRateFooter(
firstLabel: "↓",
firstText: SystemStatusFormatter.speed(snapshot.network.downloadBytesPerSecond),
firstColor: SystemStatusHUDPalette.networkDownload,
secondLabel: "↑",
secondText: SystemStatusFormatter.speed(snapshot.network.uploadBytesPerSecond),
secondColor: SystemStatusHUDPalette.networkUpload
)
}
) {
SystemStatusHUDRateChart(
firstValues: networkDownloadHistory(history),
secondValues: networkUploadHistory(history),
firstColor: SystemStatusHUDPalette.networkDownload,
secondColor: SystemStatusHUDPalette.networkUpload
)
}
.accessibilityElement(children: .combine)
.accessibilityLabel(
localization.format(
"accessibility.networkFormat",
defaultValue: "网络 %@%@%@",
rate.value,
rate.unit,
networkFootnote
)
)
}
private func batteryTile(history: [SystemStatusHistoryPoint]) -> some View {
let color = batteryColor
let value = snapshot.battery.isAvailable
? percentParts(snapshot.battery.level)
: (value: "—", unit: "")
return SystemStatusHUDValueTile(
eyebrow: SystemStatusMetricKind.battery.title(localization: localization),
glyph: "battery.100",
accent: color,
chartColor: color,
value: value.value,
unit: value.unit,
chip: temperatureChip(snapshot.battery.temperatureCelsius),
values: percentHistory(\.batteryLevel, fallback: snapshot.battery.level, history: history),
chartStyle: .area,
footnote: batteryFootnote
)
.accessibilityElement(children: .combine)
.accessibilityLabel(
localization.format(
"accessibility.batteryFormat",
defaultValue: "电量 %@%@",
SystemStatusFormatter.percent(snapshot.battery.level),
batteryFootnote
)
)
}
private var topProcessesSection: some View {
VStack(alignment: .leading, spacing: SystemStatusHUDLayout.metricInternalSpacing) {
SystemStatusHUDEyebrow(
text: SystemStatusMetricKind.topProcesses.title(localization: localization),
glyph: "list.bullet",
color: SystemStatusHUDPalette.textSecondary
)
.frame(height: SystemStatusHUDLayout.metricTitleHeight, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading)
if snapshot.topProcesses.isEmpty {
Text(localization.string("topProcesses.collecting", defaultValue: "采集中…"))
.font(SystemStatusHUDFont.mono(10))
.foregroundStyle(SystemStatusHUDPalette.textSecondary)
.frame(
maxWidth: .infinity,
minHeight: SystemStatusHUDLayout.processListHeight,
alignment: .center
)
} else {
VStack(spacing: SystemStatusHUDLayout.processListSpacing) {
ForEach(Array(snapshot.topProcesses.prefix(SystemStatusHUDLayout.processLimit))) { process in
SystemStatusHUDProcessRow(process: process, localization: localization)
}
}
.frame(
height: SystemStatusHUDLayout.processListHeight,
alignment: .topLeading
)
}
}
.padding(SystemStatusComponentLayout.cardContentPadding)
.frame(height: SystemStatusHUDLayout.lowerTileHeight, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(SystemStatusHUDCardBackground())
}
private var diskFreeBytes: UInt64? {
guard let totalBytes = snapshot.disk.totalBytes, let usedBytes = snapshot.disk.usedBytes else {
return nil
}
return totalBytes >= usedBytes ? totalBytes - usedBytes : 0
}
private var totalNetworkBytesPerSecond: UInt64? {
guard snapshot.network.downloadBytesPerSecond != nil || snapshot.network.uploadBytesPerSecond != nil else {
return nil
}
return (snapshot.network.downloadBytesPerSecond ?? 0) + (snapshot.network.uploadBytesPerSecond ?? 0)
}
private var totalDiskBytesPerSecond: UInt64? {
guard snapshot.disk.readBytesPerSecond != nil || snapshot.disk.writeBytesPerSecond != nil else {
return nil
}
return (snapshot.disk.readBytesPerSecond ?? 0) + (snapshot.disk.writeBytesPerSecond ?? 0)
}
private var memoryChip: (text: String, color: Color)? {
(SystemStatusFormatter.bytes(snapshot.memory.totalBytes), SystemStatusHUDPalette.badgeText)
}
private var networkChip: (text: String, color: Color)? {
guard let name = snapshot.network.interfaceName, !name.isEmpty else {
return nil
}
return (name, SystemStatusHUDPalette.badgeText)
}
private var cpuFootnote: String {
let powerText = localization.format(
"metric.powerFormat",
defaultValue: "功率 %@",
SystemStatusFormatter.power(snapshot.cpu.systemPowerWatts)
)
guard let load = snapshot.cpu.loadAverage1Minute else {
return localization.format(
"cpu.footnote.loadUnavailableFormat",
defaultValue: "负载 — · %@",
powerText
)
}
return localization.format(
"cpu.footnote.loadFormat",
defaultValue: "负载 %.2f · %@",
load,
powerText
)
}
private var gpuFootnote: String {
if let name = snapshot.gpu.name, !name.isEmpty {
return name
}
return snapshot.gpu.isAvailable
? localization.string("gpu.footnote.graphicsLoad", defaultValue: "图形负载")
: localization.string("metric.unavailable", defaultValue: "不可用")
}
private var memoryFootnote: String {
localization.format(
"memory.footnote.usedSwapFormat",
defaultValue: "已用 %@ · 交换 %@",
SystemStatusFormatter.bytes(snapshot.memory.usedBytes),
SystemStatusFormatter.bytes(snapshot.memory.swapUsedBytes)
)
}
private var networkFootnote: String {
"↓ \(SystemStatusFormatter.speed(snapshot.network.downloadBytesPerSecond))\(SystemStatusFormatter.speed(snapshot.network.uploadBytesPerSecond))"
}
private var diskFootnote: String {
"R \(SystemStatusFormatter.speed(snapshot.disk.readBytesPerSecond)) W \(SystemStatusFormatter.speed(snapshot.disk.writeBytesPerSecond))"
}
private var batteryFootnote: String {
guard snapshot.battery.isAvailable else {
return snapshot.battery.state.title(localization: localization)
}
var parts: [String] = []
if let batteryPowerText = batteryPowerText {
parts.append(batteryPowerText)
}
if let batteryHealthText {
parts.append(batteryHealthText)
}
if let cycleCount = snapshot.battery.cycleCount {
parts.append(
localization.format(
"battery.cyclesFormat",
defaultValue: "%d 次循环",
cycleCount
)
)
}
return parts.isEmpty ? snapshot.battery.state.title(localization: localization) : parts.joined(separator: " · ")
}
private var batteryPowerText: String? {
guard let batteryPowerWatts = snapshot.battery.batteryPowerWatts else {
return nil
}
let absoluteWatts = abs(batteryPowerWatts)
guard absoluteWatts >= 0.1 else {
return nil
}
if batteryPowerWatts < 0 {
return localization.format(
"battery.chargePowerFormat",
defaultValue: "充电 %@",
SystemStatusFormatter.power(absoluteWatts)
)
}
return localization.format(
"battery.dischargePowerFormat",
defaultValue: "放电 %@",
SystemStatusFormatter.power(absoluteWatts)
)
}
private var batteryHealthText: String? {
guard let healthPercent = snapshot.battery.healthPercent else {
return nil
}
return localization.format("battery.healthFormat", defaultValue: "健康度 %d%%", healthPercent)
}
private func diskAvailableUnit(_ unit: String) -> String {
guard !unit.isEmpty else {
return localization.string("disk.availableSuffix", defaultValue: "可用")
}
return localization.format("disk.availableUnitFormat", defaultValue: "%@可用", unit)
}
private var batteryColor: Color {
guard snapshot.battery.isAvailable else {
return SystemStatusHUDPalette.textTertiary
}
if snapshot.battery.state == .charging || snapshot.battery.state == .charged || snapshot.battery.state == .acPower {
return SystemStatusHUDPalette.green
}
let level = snapshot.battery.level ?? 0
if level < 0.15 {
return SystemStatusHUDPalette.red
}
if level < 0.35 {
return SystemStatusHUDPalette.gold
}
return SystemStatusHUDPalette.amber
}
private func temperatureChip(_ temperature: Double?) -> (text: String, color: Color)? {
guard let temperature, temperature > 0 else {
return nil
}
return (SystemStatusFormatter.temperature(temperature), SystemStatusHUDPalette.badgeText)
}
private func percentParts(_ value: Double?) -> (value: String, unit: String) {
guard let value else {
return ("—", "")
}
return (format(value * 100, fractionDigits: 0), "%")
}
private func rateParts(_ bytesPerSecond: UInt64?) -> (value: String, unit: String) {
guard let bytesPerSecond else {
return ("—", "")
}
let units = ["B/s", "KB/s", "MB/s", "GB/s"]
var value = Double(bytesPerSecond)
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 func bytesParts(_ bytes: UInt64?) -> (value: String, unit: String) {
guard let bytes else {
return ("—", "")
}
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 func percentHistory(
_ keyPath: KeyPath<SystemStatusHistoryPoint, Double?>,
fallback: Double?,
history: [SystemStatusHistoryPoint]
) -> [Double] {
let values = downsample(history.compactMap { point in
point[keyPath: keyPath].map { min(max($0 * 100, 0), 100) }
})
guard values.count <= 1, let fallback else {
return values
}
let value = min(max(fallback * 100, 0), 100)
return [value, value]
}
private func networkDownloadHistory(_ history: [SystemStatusHistoryPoint]) -> [Double] {
SystemStatusHUDDualLineChart.downsamplePeaks(
history.map { Double($0.networkDownloadBytesPerSecond ?? 0) },
limit: SystemStatusHUDLayout.chartSampleLimit
)
}
private func networkUploadHistory(_ history: [SystemStatusHistoryPoint]) -> [Double] {
SystemStatusHUDDualLineChart.downsamplePeaks(
history.map { Double($0.networkUploadBytesPerSecond ?? 0) },
limit: SystemStatusHUDLayout.chartSampleLimit
)
}
private func diskReadHistory(_ history: [SystemStatusHistoryPoint]) -> [Double] {
SystemStatusHUDDualLineChart.downsamplePeaks(
history.map { Double($0.diskReadBytesPerSecond ?? 0) },
limit: SystemStatusHUDLayout.chartSampleLimit
)
}
private func diskWriteHistory(_ history: [SystemStatusHistoryPoint]) -> [Double] {
SystemStatusHUDDualLineChart.downsamplePeaks(
history.map { Double($0.diskWriteBytesPerSecond ?? 0) },
limit: SystemStatusHUDLayout.chartSampleLimit
)
}
private var chartHistory: [SystemStatusHistoryPoint] {
guard let latestTimestamp = snapshot.history.last?.timestamp else {
return []
}
let cutoff = latestTimestamp - SystemStatusHUDLayout.chartDisplayInterval
return snapshot.history.filter { point in
point.timestamp >= cutoff && point.timestamp <= latestTimestamp
}
}
private func downsample(_ values: [Double], limit: Int = SystemStatusHUDLayout.chartSampleLimit) -> [Double] {
guard values.count > limit else {
return values
}
let stride = Double(values.count - 1) / Double(limit - 1)
return (0..<limit).map { index in
values[Int((Double(index) * stride).rounded())]
}
}
private func format(_ value: Double, fractionDigits: Int) -> String {
if fractionDigits == 0 {
return String(Int(value.rounded()))
}
return String(format: "%.\(fractionDigits)f", value)
}
}
private struct SystemStatusHUDEyebrow: View {
let text: String
let glyph: String
let color: Color
var body: some View {
HStack(spacing: 4) {
Image(systemName: glyph)
.font(.system(size: 9.5, weight: .semibold))
.foregroundStyle(color)
.frame(width: 11, height: 11)
Text(text)
.font(SystemStatusHUDFont.sans(10, .semibold))
.foregroundStyle(SystemStatusHUDPalette.textSecondary)
.lineLimit(1)
.minimumScaleFactor(0.7)
.layoutPriority(0)
}
}
}
private struct SystemStatusHUDChip: View {
let text: String
let color: Color
var body: some View {
Text(text)
.font(SystemStatusHUDFont.mono(9, .medium))
.foregroundStyle(color)
.lineLimit(1)
.truncationMode(.tail)
.minimumScaleFactor(0.8)
.padding(.horizontal, 7)
.frame(height: 18, alignment: .center)
.background(
Capsule(style: .continuous)
.fill(SystemStatusHUDPalette.chipFill)
)
.fixedSize(horizontal: true, vertical: true)
}
}
private struct SystemStatusHUDValueTile: View {
let eyebrow: String
let glyph: String
let accent: Color
let chartColor: Color
let value: String
var unit: String = ""
var chip: (text: String, color: Color)? = nil
let values: [Double]
var chartStyle: SystemStatusHUDMiniChart.Style = .area
var footnote: String? = nil
var body: some View {
SystemStatusHUDMetricTile(
title: eyebrow,
glyph: glyph,
accent: accent,
value: value,
unit: unit,
chip: chip,
footnote: footnote
) {
SystemStatusHUDMiniChart(values: values, color: chartColor, style: chartStyle)
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(eyebrow) \(value)\(unit)")
}
}
private struct SystemStatusHUDMetricTile<Visual: View, Footer: View>: View {
let title: String
let glyph: String
let accent: Color
let value: String
let unit: String
let chip: (text: String, color: Color)?
let footnote: String?
@ViewBuilder let footer: Footer
@ViewBuilder let visual: Visual
init(
title: String,
glyph: String,
accent: Color,
value: String,
unit: String,
chip: (text: String, color: Color)?,
footnote: String?,
@ViewBuilder footer: () -> Footer,
@ViewBuilder visual: () -> Visual
) {
self.title = title
self.glyph = glyph
self.accent = accent
self.value = value
self.unit = unit
self.chip = chip
self.footnote = footnote
self.footer = footer()
self.visual = visual()
}
}
private extension SystemStatusHUDMetricTile where Footer == EmptyView {
init(
title: String,
glyph: String,
accent: Color,
value: String,
unit: String,
chip: (text: String, color: Color)?,
footnote: String?,
@ViewBuilder visual: () -> Visual
) {
self.init(
title: title,
glyph: glyph,
accent: accent,
value: value,
unit: unit,
chip: chip,
footnote: footnote,
footer: { EmptyView() },
visual: visual
)
}
}
private extension SystemStatusHUDMetricTile {
var body: some View {
VStack(alignment: .leading, spacing: SystemStatusHUDLayout.metricInternalSpacing) {
HStack(spacing: 4) {
SystemStatusHUDEyebrow(text: title, glyph: glyph, color: accent)
Spacer(minLength: 2)
if let chip {
SystemStatusHUDChip(text: chip.text, color: chip.color)
.layoutPriority(1)
}
}
.frame(height: SystemStatusHUDLayout.metricTitleHeight, alignment: .leading)
.frame(maxWidth: .infinity, alignment: .leading)
HStack(alignment: .firstTextBaseline, spacing: 2) {
Text(value)
.font(SystemStatusHUDFont.mono(15, .semibold))
.foregroundStyle(SystemStatusHUDPalette.textPrimary)
.monospacedDigit()
.lineLimit(1)
.minimumScaleFactor(0.58)
if !unit.isEmpty {
Text(unit)
.font(SystemStatusHUDFont.mono(9))
.foregroundStyle(SystemStatusHUDPalette.textSecondary)
.lineLimit(1)
.minimumScaleFactor(0.7)
}
Spacer(minLength: 0)
}
.frame(height: SystemStatusHUDLayout.metricValueHeight, alignment: .leading)
visual
.frame(height: SystemStatusHUDLayout.metricVisualHeight)
.frame(maxWidth: .infinity)
if Footer.self != EmptyView.self {
footer
.frame(height: SystemStatusHUDLayout.metricFootnoteHeight, alignment: .leading)
} else {
Text(footnote ?? "")
.font(SystemStatusHUDFont.mono(8.5))
.foregroundStyle(SystemStatusHUDPalette.textTertiary)
.lineLimit(1)
.truncationMode(.middle)
.minimumScaleFactor(0.6)
.frame(height: SystemStatusHUDLayout.metricFootnoteHeight, alignment: .leading)
}
}
.padding(SystemStatusComponentLayout.cardContentPadding)
.frame(height: SystemStatusHUDLayout.metricTileHeight, alignment: .topLeading)
.frame(maxWidth: .infinity, alignment: .topLeading)
.background(SystemStatusHUDCardBackground())
}
}
private struct SystemStatusHUDDiskTile: View {
let title: String
let freeValue: String
let freeUnit: String
let totalText: String
let readText: String
let writeText: String
let readValues: [Double]
let writeValues: [Double]
let readColor: Color
let writeColor: Color
var body: some View {
SystemStatusHUDMetricTile(
title: title,
glyph: "internaldrive",
accent: readColor,
value: freeValue,
unit: freeUnit,
chip: (totalText, SystemStatusHUDPalette.badgeText),
footnote: nil,
footer: {
SystemStatusHUDRateFooter(
firstLabel: "R",
firstText: readText,
firstColor: readColor,
secondLabel: "W",
secondText: writeText,
secondColor: writeColor
)
}
) {
SystemStatusHUDRateChart(
firstValues: readValues,
secondValues: writeValues,
firstColor: readColor,
secondColor: writeColor
)
}
}
}
private struct SystemStatusHUDProcessRow: View {
let process: SystemStatusTopProcess
let localization: PluginLocalization
@State private var cachedIcon: NSImage?
@State private var cachedIconKey: String?
private var iconKey: String {
"\(process.pid)|\(process.displayName)|\(process.command)"
}
var body: some View {
HStack(spacing: 8) {
if let icon = cachedIcon {
Image(nsImage: icon)
.resizable()
.interpolation(.high)
.frame(width: 15, height: 15)
.clipShape(RoundedRectangle(cornerRadius: 3, style: .continuous))
} else {
ZStack {
RoundedRectangle(cornerRadius: 3, style: .continuous)
.fill(SystemStatusHUDPalette.chipFill)
Image(systemName: "app.dashed")
.font(.system(size: 9, weight: .medium))
.foregroundStyle(SystemStatusHUDPalette.textTertiary)
}
.frame(width: 15, height: 15)
}
Text(process.displayName)
.font(SystemStatusHUDFont.sans(11))
.foregroundStyle(SystemStatusHUDPalette.textPrimary)
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 6)
HStack(spacing: 10) {
processMetricText(SystemStatusFormatter.wholePercent(process.cpuPercent, fractionDigits: 1))
.frame(width: 46, alignment: .trailing)
processMetricText(processMemoryText)
.frame(width: 52, alignment: .trailing)
}
.frame(alignment: .trailing)
}
.frame(height: SystemStatusHUDLayout.processRowHeight)
.accessibilityElement(children: .combine)
.accessibilityLabel(
localization.format(
"accessibility.processFormat",
defaultValue: "%@ CPU %@,内存 %@",
process.displayName,
SystemStatusFormatter.wholePercent(process.cpuPercent, fractionDigits: 1),
processMemoryText
)
)
.onAppear(perform: resolveIconIfNeeded)
.onChange(of: iconKey) {
resolveIconIfNeeded()
}
}
private func processMetricText(_ text: String) -> some View {
Text(text)
.font(SystemStatusHUDFont.mono(10))
.foregroundStyle(SystemStatusHUDPalette.textSecondary)
.monospacedDigit()
.lineLimit(1)
.minimumScaleFactor(0.75)
}
private func resolveIconIfNeeded() {
guard cachedIconKey != iconKey else {
return
}
cachedIconKey = iconKey
cachedIcon = Self.processIcon(for: process)
}
private static func processIcon(for process: SystemStatusTopProcess) -> NSImage? {
if let icon = NSRunningApplication(processIdentifier: pid_t(process.pid))?.icon {
return icon
}
if let appPath = appBundlePath(in: process.command) {
return NSWorkspace.shared.icon(forFile: appPath)
}
let commandPath = executablePath(from: process.command)
if let icon = runningApplicationIcon(matching: commandPath, process: process) {
return icon
}
guard
commandPath.hasPrefix("/"),
FileManager.default.fileExists(atPath: commandPath)
else {
return nil
}
return NSWorkspace.shared.icon(forFile: commandPath)
}
private static func runningApplicationIcon(matching commandPath: String, process: SystemStatusTopProcess) -> NSImage? {
let candidates = [
process.displayName.trimmingCharacters(in: .whitespacesAndNewlines),
URL(fileURLWithPath: commandPath).lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines)
].filter { !$0.isEmpty }
for application in NSWorkspace.shared.runningApplications {
guard let icon = application.icon else {
continue
}
let applicationNames = [
application.localizedName,
application.executableURL?.lastPathComponent
].compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
if applicationNames.contains(where: { applicationName in
candidates.contains(where: { candidate in
candidate == applicationName || candidate.hasPrefix("\(applicationName) ")
})
}) {
return icon
}
}
return nil
}
private static func executablePath(from command: String) -> String {
let trimmedCommand = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard trimmedCommand.hasPrefix("/") else {
return trimmedCommand
}
if let appRange = trimmedCommand.range(of: ".app/") {
return String(trimmedCommand[..<trimmedCommand.index(before: appRange.upperBound)])
}
if let whitespaceIndex = trimmedCommand.firstIndex(where: { $0 == " " || $0 == "\t" }) {
return String(trimmedCommand[..<whitespaceIndex])
}
return trimmedCommand
}
private static func appBundlePath(in command: String) -> String? {
let trimmedCommand = command.trimmingCharacters(in: .whitespacesAndNewlines)
guard
trimmedCommand.hasPrefix("/"),
let appRange = trimmedCommand.range(of: ".app", options: [.caseInsensitive])
else {
return nil
}
return String(trimmedCommand[..<appRange.upperBound])
}
private var processMemoryText: String {
if let memoryBytes = process.memoryBytes, memoryBytes > 0 {
return SystemStatusFormatter.bytes(memoryBytes)
}
return SystemStatusFormatter.wholePercent(process.memoryPercent, fractionDigits: 1)
}
}
private struct SystemStatusHUDRateFooter: View {
private enum Layout {
static let itemSpacing: CGFloat = 6
static let itemWidth: CGFloat = 61
static let labelWidth: CGFloat = 9
static let valueWidth: CGFloat = 50
}
let firstLabel: String
let firstText: String
let firstColor: Color
let secondLabel: String
let secondText: String
let secondColor: Color
var body: some View {
HStack(spacing: Layout.itemSpacing) {
rateItem(label: firstLabel, text: firstText, color: firstColor)
rateItem(label: secondLabel, text: secondText, color: secondColor)
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
private func rateItem(label: String, text: String, color: Color) -> some View {
HStack(spacing: 2) {
Text(label)
.font(SystemStatusHUDFont.mono(8.5, .semibold))
.foregroundStyle(color)
.lineLimit(1)
.frame(width: Layout.labelWidth, alignment: .leading)
Text(text)
.font(SystemStatusHUDFont.mono(8.5))
.foregroundStyle(SystemStatusHUDPalette.textTertiary)
.monospacedDigit()
.lineLimit(1)
.minimumScaleFactor(0.65)
.frame(width: Layout.valueWidth, alignment: .leading)
}
.frame(width: Layout.itemWidth, alignment: .leading)
}
}
private struct SystemStatusHUDMiniChart: View {
enum Style {
case area
case bars
}
let values: [Double]
let color: Color
var style: Style = .area
private var samples: [Double] {
values.count > 120 ? Array(values.suffix(120)) : values
}
var body: some View {
GeometryReader { proxy in
let width = proxy.size.width
let height = proxy.size.height
let bounds = bounds()
let denominator = max(bounds.high - bounds.low, 0.0001)
if samples.count < 2 {
baseline(width: width, height: height)
} else {
switch style {
case .area:
area(width: width, height: height, low: bounds.low, denominator: denominator)
case .bars:
bars(width: width, height: height, low: bounds.low, denominator: denominator)
}
}
}
}
private func y(_ value: Double, height: CGFloat, low: Double, denominator: Double) -> CGFloat {
(1.0 - CGFloat((value - low) / denominator)) * height
}
private func baseline(width: CGFloat, height: CGFloat) -> some View {
Path { path in
path.move(to: CGPoint(x: 0, y: height - 1))
path.addLine(to: CGPoint(x: width, y: height - 1))
}
.stroke(color.opacity(0.25), lineWidth: 1)
}
private func area(width: CGFloat, height: CGFloat, low: Double, denominator: Double) -> some View {
let points = samples.enumerated().map { index, value in
CGPoint(
x: width * CGFloat(index) / CGFloat(samples.count - 1),
y: y(value, height: height, low: low, denominator: denominator)
)
}
return ZStack {
Path { path in
guard let first = points.first, let last = points.last else {
return
}
path.move(to: CGPoint(x: first.x, y: height))
path.addLine(to: first)
for point in points.dropFirst() {
path.addLine(to: point)
}
path.addLine(to: CGPoint(x: last.x, y: height))
path.closeSubpath()
}
.fill(
LinearGradient(
colors: [color.opacity(0.30), color.opacity(0.02)],
startPoint: .top,
endPoint: .bottom
)
)
Path { path in
guard let first = points.first else {
return
}
path.move(to: first)
for point in points.dropFirst() {
path.addLine(to: point)
}
}
.stroke(color, style: StrokeStyle(lineWidth: 1.5, lineCap: .round, lineJoin: .round))
}
}
private func bars(width: CGFloat, height: CGFloat, low: Double, denominator: Double) -> some View {
let count = max(samples.count, 1)
let slot = width / CGFloat(count)
let barWidth = max(1.5, slot * 0.62)
return Path { path in
for (index, value) in samples.enumerated() {
let barHeight = max(1.5, CGFloat((value - low) / denominator) * height)
let x = CGFloat(index) * slot + (slot - barWidth) / 2
path.addRoundedRect(
in: CGRect(x: x, y: height - barHeight, width: barWidth, height: barHeight),
cornerSize: CGSize(width: 1, height: 1),
style: .continuous
)
}
}
.fill(color.opacity(0.85))
}
private func bounds() -> (low: Double, high: Double) {
let low = min(samples.min() ?? 0, 0)
let high = samples.max() ?? 1
if high - low < 0.001 {
return (low, high + 1)
}
return (low, high)
}
}
enum SystemStatusHUDDualLineChart {
static func downsamplePeaks(
_ values: [Double],
limit: Int
) -> [Double] {
guard limit > 0 else {
return []
}
guard values.count > limit else {
return values.map { max($0, 0) }
}
guard limit > 1 else {
return [max(values.max() ?? 0, 0)]
}
let bucketSize = Double(values.count) / Double(limit)
return (0..<limit).map { index in
let start = Int((Double(index) * bucketSize).rounded(.down))
let proposedEnd = Int((Double(index + 1) * bucketSize).rounded(.down))
let end = min(values.count, max(start + 1, proposedEnd))
return max(values[start..<end].max() ?? 0, 0)
}
}
static func points(
values: [Double],
width: CGFloat,
height: CGFloat,
maximumValue: Double? = nil
) -> [CGPoint] {
let samples = values.map { max($0, 0) }
guard !samples.isEmpty else {
return []
}
let maximumValue = max(maximumValue ?? (samples.max() ?? 0), 0.0001)
return samples.enumerated().map { index, sample in
let x = width * CGFloat(index) / CGFloat(max(samples.count - 1, 1))
let ratio = scaledRatio(value: sample, maximumValue: maximumValue)
let y = (1 - ratio) * height
return CGPoint(x: x, y: y)
}
}
static func scaledRatio(value: Double, maximumValue: Double) -> CGFloat {
let normalized = min(max(value / max(maximumValue, 0.0001), 0), 1)
return CGFloat(sqrt(normalized))
}
}
private struct SystemStatusHUDRateChart: View {
let firstValues: [Double]
let secondValues: [Double]
let firstColor: Color
let secondColor: Color
var body: some View {
GeometryReader { proxy in
let width = proxy.size.width
let height = proxy.size.height
let firstSeries = firstValues.map { max($0, 0) }
let secondSeries = secondValues.map { max($0, 0) }
let firstMaximum = firstSeries.max() ?? 0
let secondMaximum = secondSeries.max() ?? 0
if max(firstMaximum, secondMaximum) <= 0 || (firstSeries.count < 2 && secondSeries.count < 2) {
Color.clear
} else {
ZStack {
series(
values: firstSeries,
width: width,
height: height,
maximumValue: firstMaximum,
color: firstColor
)
series(
values: secondSeries,
width: width,
height: height,
maximumValue: secondMaximum,
color: secondColor
)
}
}
}
}
@ViewBuilder
private func series(
values: [Double],
width: CGFloat,
height: CGFloat,
maximumValue: Double,
color: Color
) -> some View {
if values.count >= 2 {
let points = SystemStatusHUDDualLineChart.points(
values: values,
width: width,
height: height,
maximumValue: maximumValue
)
ZStack {
Path { path in
guard let first = points.first, let last = points.last else {
return
}
path.move(to: CGPoint(x: first.x, y: height))
path.addLine(to: first)
for point in points.dropFirst() {
path.addLine(to: point)
}
path.addLine(to: CGPoint(x: last.x, y: height))
path.closeSubpath()
}
.fill(
LinearGradient(
colors: [color.opacity(0.30), color.opacity(0.02)],
startPoint: .top,
endPoint: .bottom
)
)
Path { path in
guard let first = points.first else {
return
}
path.move(to: first)
for point in points.dropFirst() {
path.addLine(to: point)
}
}
.stroke(color, style: StrokeStyle(lineWidth: 1.2, lineCap: .round, lineJoin: .round))
}
}
}
}
private struct SystemStatusHUDCardBackground: View {
var body: some View {
RoundedRectangle(cornerRadius: 10, style: .continuous)
.fill(SystemStatusHUDPalette.cardFill)
}
}
private enum SystemStatusHUDPalette {
static let textPrimary = Color.primary
static let textSecondary = Color.secondary
static let textTertiary = Color.secondary.opacity(0.72)
static let cardFill = Color.primary.opacity(0.045)
static let chipFill = Color.primary.opacity(0.06)
static let badgeText = textSecondary
static let green = Color(nsColor: .systemGreen)
static let gold = Color(nsColor: .systemYellow)
static let amber = Color(nsColor: .systemOrange)
static let orange = Color(nsColor: .systemOrange)
static let blue = Color(nsColor: .systemBlue)
static let red = Color(nsColor: .systemRed)
static let gpu = Color(red: 0.949, green: 0.537, blue: 0.306)
static let network = Color(red: 0.180, green: 0.690, blue: 0.659)
static let networkDownload = Color(red: 0.204, green: 0.596, blue: 0.859)
static let networkUpload = Color(red: 0.941, green: 0.467, blue: 0.294)
static let diskRead = Color(red: 0.275, green: 0.588, blue: 0.941)
static let diskWrite = Color(red: 0.608, green: 0.431, blue: 0.902)
}
private enum SystemStatusHUDFont {
static func mono(_ size: CGFloat, _ weight: Font.Weight = .regular) -> Font {
.system(size: size, weight: weight, design: .monospaced)
}
static func sans(_ size: CGFloat, _ weight: Font.Weight = .regular) -> Font {
.system(size: size, weight: weight)
}
}