添加温度显示(CPU/GPU独立模块,SMC传感器),拆分温度配置,去掉紧凑横条,添加图形/文字动画开关,数值动画优化

This commit is contained in:
JOJO 2026-05-06 10:37:58 +08:00
parent ee96706ab1
commit f749771135
7 changed files with 474 additions and 91 deletions

View File

@ -7,7 +7,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable {
case ring // case ring //
case text // case text //
case pie // case pie //
case compact // +
var id: String { rawValue } var id: String { rawValue }
@ -17,7 +16,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable {
case .ring: return "圆环进度" case .ring: return "圆环进度"
case .text: return "纯文本" case .text: return "纯文本"
case .pie: return "扇形圆盘" case .pie: return "扇形圆盘"
case .compact: return "紧凑横条"
} }
} }
@ -27,7 +25,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable {
case .ring: return "circle.dotted" case .ring: return "circle.dotted"
case .text: return "textformat" case .text: return "textformat"
case .pie: return "chart.pie" case .pie: return "chart.pie"
case .compact: return "rectangle.compress.vertical"
} }
} }
} }
@ -65,6 +62,12 @@ final class Configuration: ObservableObject {
@Published var swapStyle: GaugeDisplayStyle { @Published var swapStyle: GaugeDisplayStyle {
didSet { save("swapStyle", swapStyle.rawValue) } didSet { save("swapStyle", swapStyle.rawValue) }
} }
@Published var cpuTemperatureStyle: GaugeDisplayStyle {
didSet { save("cpuTemperatureStyle", cpuTemperatureStyle.rawValue) }
}
@Published var gpuTemperatureStyle: GaugeDisplayStyle {
didSet { save("gpuTemperatureStyle", gpuTemperatureStyle.rawValue) }
}
@Published var showCPU: Bool { @Published var showCPU: Bool {
didSet { save("showCPU", showCPU) } didSet { save("showCPU", showCPU) }
@ -78,6 +81,15 @@ final class Configuration: ObservableObject {
@Published var showSwap: Bool { @Published var showSwap: Bool {
didSet { save("showSwap", showSwap) } didSet { save("showSwap", showSwap) }
} }
@Published var showTemperature: Bool {
didSet { save("showTemperature", showTemperature) }
}
@Published var showCPUTemperature: Bool {
didSet { save("showCPUTemperature", showCPUTemperature) }
}
@Published var showGPUTemperature: Bool {
didSet { save("showGPUTemperature", showGPUTemperature) }
}
@Published var showTopProcesses: Bool { @Published var showTopProcesses: Bool {
didSet { save("showTopProcesses", showTopProcesses) } didSet { save("showTopProcesses", showTopProcesses) }
} }
@ -101,6 +113,13 @@ final class Configuration: ObservableObject {
didSet { save("windowHeight", windowHeight) } didSet { save("windowHeight", windowHeight) }
} }
@Published var gaugeAnimationEnabled: Bool {
didSet { save("gaugeAnimationEnabled", gaugeAnimationEnabled) }
}
@Published var textAnimationEnabled: Bool {
didSet { save("textAnimationEnabled", textAnimationEnabled) }
}
@Published var cpuPos: ModulePosition { @Published var cpuPos: ModulePosition {
didSet { save("cpuPos", cpuPos.rawValue) } didSet { save("cpuPos", cpuPos.rawValue) }
} }
@ -113,6 +132,15 @@ final class Configuration: ObservableObject {
@Published var swapPos: ModulePosition { @Published var swapPos: ModulePosition {
didSet { save("swapPos", swapPos.rawValue) } didSet { save("swapPos", swapPos.rawValue) }
} }
@Published var temperaturePos: ModulePosition {
didSet { save("temperaturePos", temperaturePos.rawValue) }
}
@Published var cpuTemperaturePos: ModulePosition {
didSet { save("cpuTemperaturePos", cpuTemperaturePos.rawValue) }
}
@Published var gpuTemperaturePos: ModulePosition {
didSet { save("gpuTemperaturePos", gpuTemperaturePos.rawValue) }
}
@Published var processesPos: ModulePosition { @Published var processesPos: ModulePosition {
didSet { save("processesPos", processesPos.rawValue) } didSet { save("processesPos", processesPos.rawValue) }
} }
@ -130,6 +158,15 @@ final class Configuration: ObservableObject {
@Published var swapRow: Int { @Published var swapRow: Int {
didSet { save("swapRow", swapRow) } didSet { save("swapRow", swapRow) }
} }
@Published var temperatureRow: Int {
didSet { save("temperatureRow", temperatureRow) }
}
@Published var cpuTemperatureRow: Int {
didSet { save("cpuTemperatureRow", cpuTemperatureRow) }
}
@Published var gpuTemperatureRow: Int {
didSet { save("gpuTemperatureRow", gpuTemperatureRow) }
}
@Published var processesRow: Int { @Published var processesRow: Int {
didSet { save("processesRow", processesRow) } didSet { save("processesRow", processesRow) }
} }
@ -141,11 +178,16 @@ final class Configuration: ObservableObject {
gpuStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "gpuStyle") ?? "ring") ?? .ring gpuStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "gpuStyle") ?? "ring") ?? .ring
memoryStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "memoryStyle") ?? "bar") ?? .bar memoryStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "memoryStyle") ?? "bar") ?? .bar
swapStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "swapStyle") ?? "bar") ?? .bar swapStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "swapStyle") ?? "bar") ?? .bar
cpuTemperatureStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "cpuTemperatureStyle") ?? "text") ?? .text
gpuTemperatureStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "gpuTemperatureStyle") ?? "text") ?? .text
showCPU = defaults.object(forKey: "showCPU") as? Bool ?? true showCPU = defaults.object(forKey: "showCPU") as? Bool ?? true
showGPU = defaults.object(forKey: "showGPU") as? Bool ?? true showGPU = defaults.object(forKey: "showGPU") as? Bool ?? true
showMemory = defaults.object(forKey: "showMemory") as? Bool ?? true showMemory = defaults.object(forKey: "showMemory") as? Bool ?? true
showSwap = defaults.object(forKey: "showSwap") as? Bool ?? true showSwap = defaults.object(forKey: "showSwap") as? Bool ?? true
showTemperature = defaults.object(forKey: "showTemperature") as? Bool ?? true
showCPUTemperature = defaults.object(forKey: "showCPUTemperature") as? Bool ?? true
showGPUTemperature = defaults.object(forKey: "showGPUTemperature") as? Bool ?? true
showTopProcesses = defaults.object(forKey: "showTopProcesses") as? Bool ?? true showTopProcesses = defaults.object(forKey: "showTopProcesses") as? Bool ?? true
processCount = defaults.object(forKey: "processCount") as? Int ?? 5 processCount = defaults.object(forKey: "processCount") as? Int ?? 5
@ -155,16 +197,25 @@ final class Configuration: ObservableObject {
windowWidth = defaults.object(forKey: "windowWidth") as? Double ?? 250 windowWidth = defaults.object(forKey: "windowWidth") as? Double ?? 250
windowHeight = defaults.object(forKey: "windowHeight") as? Double ?? 300 windowHeight = defaults.object(forKey: "windowHeight") as? Double ?? 300
gaugeAnimationEnabled = defaults.object(forKey: "gaugeAnimationEnabled") as? Bool ?? true
textAnimationEnabled = defaults.object(forKey: "textAnimationEnabled") as? Bool ?? true
cpuPos = ModulePosition(rawValue: defaults.string(forKey: "cpuPos") ?? "full") ?? .full cpuPos = ModulePosition(rawValue: defaults.string(forKey: "cpuPos") ?? "full") ?? .full
gpuPos = ModulePosition(rawValue: defaults.string(forKey: "gpuPos") ?? "full") ?? .full gpuPos = ModulePosition(rawValue: defaults.string(forKey: "gpuPos") ?? "full") ?? .full
memoryPos = ModulePosition(rawValue: defaults.string(forKey: "memoryPos") ?? "full") ?? .full memoryPos = ModulePosition(rawValue: defaults.string(forKey: "memoryPos") ?? "full") ?? .full
swapPos = ModulePosition(rawValue: defaults.string(forKey: "swapPos") ?? "full") ?? .full swapPos = ModulePosition(rawValue: defaults.string(forKey: "swapPos") ?? "full") ?? .full
temperaturePos = ModulePosition(rawValue: defaults.string(forKey: "temperaturePos") ?? "full") ?? .full
cpuTemperaturePos = ModulePosition(rawValue: defaults.string(forKey: "cpuTemperaturePos") ?? "left") ?? .left
gpuTemperaturePos = ModulePosition(rawValue: defaults.string(forKey: "gpuTemperaturePos") ?? "right") ?? .right
processesPos = ModulePosition(rawValue: defaults.string(forKey: "processesPos") ?? "full") ?? .full processesPos = ModulePosition(rawValue: defaults.string(forKey: "processesPos") ?? "full") ?? .full
cpuRow = defaults.object(forKey: "cpuRow") as? Int ?? 1 cpuRow = defaults.object(forKey: "cpuRow") as? Int ?? 1
gpuRow = defaults.object(forKey: "gpuRow") as? Int ?? 2 gpuRow = defaults.object(forKey: "gpuRow") as? Int ?? 2
memoryRow = defaults.object(forKey: "memoryRow") as? Int ?? 3 memoryRow = defaults.object(forKey: "memoryRow") as? Int ?? 3
swapRow = defaults.object(forKey: "swapRow") as? Int ?? 4 swapRow = defaults.object(forKey: "swapRow") as? Int ?? 4
temperatureRow = defaults.object(forKey: "temperatureRow") as? Int ?? 5
cpuTemperatureRow = defaults.object(forKey: "cpuTemperatureRow") as? Int ?? 5
gpuTemperatureRow = defaults.object(forKey: "gpuTemperatureRow") as? Int ?? 5
processesRow = defaults.object(forKey: "processesRow") as? Int ?? 5 processesRow = defaults.object(forKey: "processesRow") as? Int ?? 5
} }

View File

@ -88,7 +88,9 @@ struct ContentView: View {
title: "CPU", icon: "cpu", title: "CPU", icon: "cpu",
value: monitor.info.cpuUsage, unit: "%", value: monitor.info.cpuUsage, unit: "%",
detail: "\(monitor.info.cpuCores) 核心 · \(monitor.info.cpuModel)", detail: "\(monitor.info.cpuCores) 核心 · \(monitor.info.cpuModel)",
style: config.cpuStyle, fontSize: config.fontSize style: config.cpuStyle, fontSize: config.fontSize,
animateGraphic: config.gaugeAnimationEnabled,
animateText: config.textAnimationEnabled
) )
} }
@ -97,7 +99,9 @@ struct ContentView: View {
title: "GPU", icon: "display", title: "GPU", icon: "display",
value: monitor.info.gpuUsage ?? 0, unit: "%", value: monitor.info.gpuUsage ?? 0, unit: "%",
detail: monitor.info.gpuModel + (monitor.info.gpuUsage == nil ? " (N/A)" : ""), detail: monitor.info.gpuModel + (monitor.info.gpuUsage == nil ? " (N/A)" : ""),
style: config.gpuStyle, fontSize: config.fontSize style: config.gpuStyle, fontSize: config.fontSize,
animateGraphic: config.gaugeAnimationEnabled,
animateText: config.textAnimationEnabled
) )
} }
@ -106,7 +110,9 @@ struct ContentView: View {
title: "内存", icon: "memorychip", title: "内存", icon: "memorychip",
value: monitor.info.memoryUsagePercent, unit: "%", value: monitor.info.memoryUsagePercent, unit: "%",
detail: formatBytes(monitor.info.memoryUsed) + " / " + formatBytes(monitor.info.memoryTotal), detail: formatBytes(monitor.info.memoryUsed) + " / " + formatBytes(monitor.info.memoryTotal),
style: config.memoryStyle, fontSize: config.fontSize style: config.memoryStyle, fontSize: config.fontSize,
animateGraphic: config.gaugeAnimationEnabled,
animateText: config.textAnimationEnabled
) )
} }
@ -115,10 +121,46 @@ struct ContentView: View {
title: "Swap", icon: "arrow.left.arrow.right", title: "Swap", icon: "arrow.left.arrow.right",
value: monitor.info.swapUsagePercent, unit: "%", value: monitor.info.swapUsagePercent, unit: "%",
detail: formatBytes(monitor.info.swapUsed) + " / " + formatBytes(monitor.info.swapTotal), detail: formatBytes(monitor.info.swapUsed) + " / " + formatBytes(monitor.info.swapTotal),
style: config.swapStyle, fontSize: config.fontSize style: config.swapStyle, fontSize: config.fontSize,
animateGraphic: config.gaugeAnimationEnabled,
animateText: config.textAnimationEnabled
) )
} }
private var cpuTemperatureGauge: some View {
let temp = monitor.info.cpuTemperature
let hasData = temp != nil
let v = temp ?? 0
return StyledGauge(
title: "CPU 温度", icon: "thermometer.medium",
value: v, unit: "°C",
detail: hasData ? "" : "N/A",
style: config.cpuTemperatureStyle, fontSize: config.fontSize,
maxValue: 120,
animateGraphic: config.gaugeAnimationEnabled,
animateText: config.textAnimationEnabled
)
.opacity(hasData ? 1.0 : 0.5)
}
private var gpuTemperatureGauge: some View {
let temp = monitor.info.gpuTemperature
let hasData = temp != nil
let v = temp ?? 0
return StyledGauge(
title: "GPU 温度", icon: "thermometer.low",
value: v, unit: "°C",
detail: hasData ? "" : "N/A",
style: config.gpuTemperatureStyle, fontSize: config.fontSize,
maxValue: 120,
animateGraphic: config.gaugeAnimationEnabled,
animateText: config.textAnimationEnabled
)
.opacity(hasData ? 1.0 : 0.5)
}
private var processesSection: some View { private var processesSection: some View {
VStack(spacing: 12) { VStack(spacing: 12) {
ProcessListView( ProcessListView(
@ -162,6 +204,14 @@ struct ContentView: View {
items.append(Item(name: "Swap", row: config.swapRow, position: config.swapPos, items.append(Item(name: "Swap", row: config.swapRow, position: config.swapPos,
view: AnyView(swapGauge))) view: AnyView(swapGauge)))
} }
if config.showCPUTemperature {
items.append(Item(name: "CPU温度", row: config.cpuTemperatureRow, position: config.cpuTemperaturePos,
view: AnyView(cpuTemperatureGauge)))
}
if config.showGPUTemperature {
items.append(Item(name: "GPU温度", row: config.gpuTemperatureRow, position: config.gpuTemperaturePos,
view: AnyView(gpuTemperatureGauge)))
}
if config.showTopProcesses { if config.showTopProcesses {
items.append(Item(name: "进程", row: config.processesRow, position: config.processesPos, items.append(Item(name: "进程", row: config.processesRow, position: config.processesPos,
view: AnyView(processesSection))) view: AnyView(processesSection)))

View File

@ -0,0 +1,259 @@
import Foundation
import IOKit
// MARK: - SMC AppleSMC + IOKit CPU / GPU
// SMC AppleSMC kext SMCParamStruct
private struct SMCVersion {
var major: UInt8 = 0
var minor: UInt8 = 0
var build: UInt8 = 0
var reserved: UInt8 = 0
var release: UInt16 = 0
}
private struct SMCPLimitData {
var version: UInt16 = 0
var length: UInt16 = 0
var cpuPLimit: UInt32 = 0
var gpuPLimit: UInt32 = 0
var memPLimit: UInt32 = 0
}
private struct SMCKeyInfoData {
var dataSize: UInt32 = 0
var dataType: UInt32 = 0
var dataAttributes: UInt8 = 0
}
private typealias SMCBytes = (
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8,
UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8
)
private struct SMCParamStruct {
var key: UInt32 = 0
var vers = SMCVersion()
var pLimitData = SMCPLimitData()
var keyInfo = SMCKeyInfoData()
var padding: UInt16 = 0 //
var result: UInt8 = 0
var status: UInt8 = 0
var data8: UInt8 = 0 // selector5=key, 9=key, 8=key
var data32: UInt32 = 0
var bytes: SMCBytes = (
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0
)
}
// SMC
private let kSMCHandleYPCEvent: UInt32 = 2
private let kSMCReadKey: UInt8 = 5
private let kSMCGetKeyInfo: UInt8 = 9
private let kSMCGetKeyFromIndex: UInt8 = 8
private let kSMCSuccess: UInt8 = 0
/// actor SMC
final class SMCSensor {
static let shared = SMCSensor()
private var conn: io_connect_t = 0
private var cpuKeys: [String] = []
private var gpuKeys: [String] = []
private var scanned = false
private init() {}
deinit {
if conn != 0 { IOServiceClose(conn) }
}
// MARK: -
func readCPUTemperature() -> Double? {
return readMax(keys: cpuKeys)
}
func readGPUTemperature() -> Double? {
return readMax(keys: gpuKeys)
}
// MARK: -
private func readMax(keys: [String]) -> Double? {
guard openSMC() else { return nil }
if !scanned { scanKeys() }
var maxTemp: Double? = nil
for key in keys {
if let temp = readKey(key) {
maxTemp = max(maxTemp ?? temp, temp)
}
}
return maxTemp
}
private func openSMC() -> Bool {
if conn != 0 { return true }
let service = IOServiceGetMatchingService(
kIOMainPortDefault,
IOServiceMatching("AppleSMC")
)
guard service != 0 else { return false }
let status = IOServiceOpen(service, mach_task_self_, 0, &conn)
IOObjectRelease(service)
return status == KERN_SUCCESS && conn != 0
}
// MARK: - key
private func scanKeys() {
scanned = true
guard conn != 0 else { return }
// key
let totalKeys = readKeyCount()
guard totalKeys > 0 else { return }
var cpu: [String] = []
var gpu: [String] = []
for i in 0..<totalKeys {
guard let keyName = keyAtIndex(i),
let (dataType, _) = keyInfo(keyName) else { continue }
// sp78 key
let isTempType = (dataType == "flt " || dataType == "sp78")
guard isTempType else { continue }
if keyName.hasPrefix("Tp") {
cpu.append(keyName)
} else if keyName.hasPrefix("Tg") {
gpu.append(keyName)
}
}
if !cpu.isEmpty { cpuKeys = cpu }
if !gpu.isEmpty { gpuKeys = gpu }
}
// MARK: - SMC
private func readKeyCount() -> Int {
var input = SMCParamStruct()
input.key = fourCharCode("#KEY")
input.keyInfo.dataSize = 4
input.keyInfo.dataType = fourCharCode("ui32")
input.data8 = kSMCReadKey
guard let output = callSMC(&input),
output.result == kSMCSuccess else { return 0 }
let raw = UInt32(output.bytes.0) << 24
| UInt32(output.bytes.1) << 16
| UInt32(output.bytes.2) << 8
| UInt32(output.bytes.3)
return Int(raw)
}
private func keyAtIndex(_ index: Int) -> String? {
var input = SMCParamStruct()
input.data8 = kSMCGetKeyFromIndex
input.data32 = UInt32(index)
guard let output = callSMC(&input),
output.result == kSMCSuccess else { return nil }
return charCodeToString(output.key)
}
/// key (dataType, dataSize)
private func keyInfo(_ keyName: String) -> (String, UInt32)? {
var input = SMCParamStruct()
input.key = fourCharCode(keyName)
input.data8 = kSMCGetKeyInfo
guard let output = callSMC(&input),
output.result == kSMCSuccess else { return nil }
return (charCodeToString(output.keyInfo.dataType),
output.keyInfo.dataSize)
}
/// key flt / sp78
private func readKey(_ keyName: String) -> Double? {
guard let (dataType, dataSize) = keyInfo(keyName) else { return nil }
var input = SMCParamStruct()
input.key = fourCharCode(keyName)
input.keyInfo.dataSize = dataSize
input.keyInfo.dataType = fourCharCode(dataType)
input.data8 = kSMCReadKey
guard let output = callSMC(&input),
output.result == kSMCSuccess else { return nil }
switch dataType {
case "flt ":
// 32-bit IEEE 754 float
let raw = UInt32(output.bytes.0)
| (UInt32(output.bytes.1) << 8)
| (UInt32(output.bytes.2) << 16)
| (UInt32(output.bytes.3) << 24)
return Double(Float(bitPattern: raw))
case "sp78":
// 16-bit signed fixed-point
let sign = (output.bytes.0 & 0x80) == 0 ? 1.0 : -1.0
return sign * (Double(output.bytes.0 & 0x7F) + Double(output.bytes.1) / 256.0)
default:
return nil
}
}
/// SMCParamStruct AppleSMC
private func callSMC(_ input: inout SMCParamStruct) -> SMCParamStruct? {
var output = SMCParamStruct()
let size = MemoryLayout<SMCParamStruct>.stride // 80
var outputSize = size
let result = IOConnectCallStructMethod(
conn,
kSMCHandleYPCEvent,
&input, size,
&output, &outputSize
)
guard result == KERN_SUCCESS else { return nil }
return output
}
// MARK: -
private func fourCharCode(_ string: String) -> UInt32 {
var result: UInt32 = 0
for char in string.utf8 {
result = (result << 8) | UInt32(char)
}
return result
}
private func charCodeToString(_ code: UInt32) -> String {
let c1 = UInt8((code >> 24) & 0xFF)
let c2 = UInt8((code >> 16) & 0xFF)
let c3 = UInt8((code >> 8) & 0xFF)
let c4 = UInt8(code & 0xFF)
return String(UnicodeScalar(c1))
+ String(UnicodeScalar(c2))
+ String(UnicodeScalar(c3))
+ String(UnicodeScalar(c4))
}
}

View File

@ -22,6 +22,9 @@ struct SystemInfo {
var swapIns: UInt64 var swapIns: UInt64
var swapOuts: UInt64 var swapOuts: UInt64
var cpuTemperature: Double? // °C
var gpuTemperature: Double? // °C
var topCPUProcesses: [ProcessInfo] var topCPUProcesses: [ProcessInfo]
var topMemoryProcesses: [ProcessInfo] var topMemoryProcesses: [ProcessInfo]
@ -44,6 +47,7 @@ final class SystemMonitor: ObservableObject {
memoryTotal: 0, memoryUsed: 0, memoryUsagePercent: 0, memoryTotal: 0, memoryUsed: 0, memoryUsagePercent: 0,
swapTotal: 0, swapUsed: 0, swapUsagePercent: 0, swapTotal: 0, swapUsed: 0, swapUsagePercent: 0,
swapIns: 0, swapOuts: 0, swapIns: 0, swapOuts: 0,
cpuTemperature: nil, gpuTemperature: nil,
topCPUProcesses: [], topMemoryProcesses: [] topCPUProcesses: [], topMemoryProcesses: []
) )
@ -90,6 +94,7 @@ final class SystemMonitor: ObservableObject {
let gpu = fetchGPU() let gpu = fetchGPU()
let mem = fetchMemory() let mem = fetchMemory()
let swap = fetchSwap() let swap = fetchSwap()
let temp = fetchTemperature()
let procs = fetchTopProcesses(count: topN) let procs = fetchTopProcesses(count: topN)
info = SystemInfo( info = SystemInfo(
@ -106,6 +111,8 @@ final class SystemMonitor: ObservableObject {
swapUsagePercent: swap.percent, swapUsagePercent: swap.percent,
swapIns: swap.ins, swapIns: swap.ins,
swapOuts: swap.outs, swapOuts: swap.outs,
cpuTemperature: temp.cpu,
gpuTemperature: temp.gpu,
topCPUProcesses: procs.cpu, topCPUProcesses: procs.cpu,
topMemoryProcesses: procs.mem topMemoryProcesses: procs.mem
) )
@ -266,6 +273,14 @@ final class SystemMonitor: ObservableObject {
return (0, 0, 0, 0, 0) return (0, 0, 0, 0, 0)
} }
// MARK: - (SMC)
private func fetchTemperature() -> (cpu: Double?, gpu: Double?) {
let cpu = SMCSensor.shared.readCPUTemperature()
let gpu = SMCSensor.shared.readGPUTemperature()
return (cpu, gpu)
}
// MARK: - // MARK: -
private func fetchTopProcesses(count: Int) -> (cpu: [SystemInfo.ProcessInfo], mem: [SystemInfo.ProcessInfo]) { private func fetchTopProcesses(count: Int) -> (cpu: [SystemInfo.ProcessInfo], mem: [SystemInfo.ProcessInfo]) {

View File

@ -5,23 +5,46 @@ import SwiftUI
struct StyledGauge: View { struct StyledGauge: View {
let title: String let title: String
let icon: String let icon: String
let value: Double // 0-100 let value: Double
let unit: String // "%" / "GB" let unit: String
let detail: String // "3.2 / 16 GB" let detail: String
let style: GaugeDisplayStyle let style: GaugeDisplayStyle
let fontSize: Double let fontSize: Double
var maxValue: Double = 100
var animateGraphic: Bool = true
var animateText: Bool = true
private var progress: Double {
maxValue > 0 ? min(value / maxValue, 1.0) : 0
}
//
private var gaugeColor: Color { private var gaugeColor: Color {
if value < 40 { return .green } if progress < 0.4 { return .green }
if value < 70 { return .yellow } if progress < 0.7 { return .yellow }
if value < 85 { return .orange } if progress < 0.85 { return .orange }
return .red return .red
} }
/// contentTransition
@ViewBuilder
private func animatedNumber(_ text: Text) -> some View {
if animateText {
text.contentTransition(.numericText(value: value))
} else {
text
}
}
private var graphicAnim: Animation? {
animateGraphic ? .easeOut(duration: 0.2) : nil
}
private var textAnim: Animation? {
animateText ? .easeOut(duration: 0.3) : nil
}
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
//
HStack(spacing: 6) { HStack(spacing: 6) {
Image(systemName: icon) Image(systemName: icon)
.font(.system(size: fontSize)) .font(.system(size: fontSize))
@ -32,18 +55,11 @@ struct StyledGauge: View {
Spacer() Spacer()
} }
//
switch style { switch style {
case .bar: case .bar: barView
barView case .ring: ringView
case .ring: case .text: textView
ringView case .pie: pieView
case .text:
textView
case .pie:
pieView
case .compact:
compactView
} }
} }
} }
@ -58,29 +74,26 @@ struct StyledGauge: View {
.fill(Color.white.opacity(0.12)) .fill(Color.white.opacity(0.12))
.frame(height: 8) .frame(height: 8)
Capsule() Capsule()
.fill( .fill(LinearGradient(
LinearGradient(
colors: [gaugeColor.opacity(0.8), gaugeColor], colors: [gaugeColor.opacity(0.8), gaugeColor],
startPoint: .leading, startPoint: .leading, endPoint: .trailing))
endPoint: .trailing .frame(width: max(geo.size.width * CGFloat(progress), 4), height: 8)
) .animation(graphicAnim, value: value)
)
.frame(width: max(geo.size.width * CGFloat(value / 100), 4), height: 8)
.animation(.easeOut(duration: 0.2), value: value)
} }
} }
.frame(height: 8) .frame(height: 8)
HStack { HStack {
Text(String(format: "%.1f%@", value, unit)) animatedNumber(Text(String(format: "%.1f%@", value, unit))
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced)) .font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
.foregroundColor(.primary) .foregroundColor(.primary))
Spacer() Spacer()
Text(detail) Text(detail)
.font(.system(size: fontSize - 2)) .font(.system(size: fontSize - 2))
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
} }
.animation(textAnim, value: value)
} }
} }
@ -92,33 +105,32 @@ struct StyledGauge: View {
Circle() Circle()
.stroke(Color.white.opacity(0.12), lineWidth: 5) .stroke(Color.white.opacity(0.12), lineWidth: 5)
.frame(width: 48, height: 48) .frame(width: 48, height: 48)
.animation(.easeOut(duration: 0.2), value: value)
Circle() Circle()
.trim(from: 0, to: CGFloat(value / 100)) .trim(from: 0, to: CGFloat(progress))
.stroke( .stroke(AngularGradient(
AngularGradient(
colors: [gaugeColor.opacity(0.7), gaugeColor, gaugeColor.opacity(0.9)], colors: [gaugeColor.opacity(0.7), gaugeColor, gaugeColor.opacity(0.9)],
center: .center center: .center),
), style: StrokeStyle(lineWidth: 5, lineCap: .round))
style: StrokeStyle(lineWidth: 5, lineCap: .round)
)
.rotationEffect(.degrees(-90)) .rotationEffect(.degrees(-90))
.frame(width: 48, height: 48) .frame(width: 48, height: 48)
.animation(graphicAnim, value: value)
Text(String(format: "%.0f", value)) animatedNumber(Text(String(format: "%.0f", value))
.font(.system(size: fontSize - 1, weight: .bold, design: .monospaced)) .font(.system(size: fontSize - 1, weight: .bold, design: .monospaced))
.foregroundColor(.primary) .foregroundColor(.primary))
} }
.animation(textAnim, value: value)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(String(format: "%.1f%@", value, unit)) animatedNumber(Text(String(format: "%.1f%@", value, unit))
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced)) .font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
.foregroundColor(.primary) .foregroundColor(.primary))
Text(detail) Text(detail)
.font(.system(size: fontSize - 2)) .font(.system(size: fontSize - 2))
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
} }
.animation(textAnim, value: value)
} }
} }
@ -126,10 +138,10 @@ struct StyledGauge: View {
private var textView: some View { private var textView: some View {
HStack { HStack {
Text(String(format: "%.1f%@", value, unit)) animatedNumber(Text(String(format: "%.1f%@", value, unit))
.font(.system(size: fontSize + 6, weight: .bold, design: .monospaced)) .font(.system(size: fontSize + 6, weight: .bold, design: .monospaced))
.foregroundColor(gaugeColor) .foregroundColor(gaugeColor))
.animation(.easeOut(duration: 0.2), value: value) .animation(textAnim, value: value)
Spacer() Spacer()
Text(detail) Text(detail)
.font(.system(size: fontSize - 2)) .font(.system(size: fontSize - 2))
@ -147,56 +159,28 @@ struct StyledGauge: View {
.fill(Color.white.opacity(0.05)) .fill(Color.white.opacity(0.05))
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
PieSlice(endAngle: .degrees(360 * value / 100)) PieSlice(endAngle: .degrees(360 * progress))
.fill(gaugeColor.opacity(0.85)) .fill(gaugeColor.opacity(0.85))
.frame(width: 42, height: 42) .frame(width: 42, height: 42)
.clipShape(Circle()) .clipShape(Circle())
.animation(.easeOut(duration: 0.2), value: value) .animation(graphicAnim, value: value)
Text(String(format: "%.0f", value)) animatedNumber(Text(String(format: "%.0f", value))
.font(.system(size: fontSize - 2, weight: .bold, design: .monospaced)) .font(.system(size: fontSize - 2, weight: .bold, design: .monospaced))
.foregroundColor(.white) .foregroundColor(.white))
} }
.animation(textAnim, value: value)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(String(format: "%.1f%@", value, unit)) animatedNumber(Text(String(format: "%.1f%@", value, unit))
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced)) .font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
.foregroundColor(.primary) .foregroundColor(.primary))
Text(detail)
.font(.system(size: fontSize - 2))
.foregroundColor(.secondary)
.lineLimit(1)
}
}
}
// MARK: -
private var compactView: some View {
VStack(spacing: 4) {
GeometryReader { geo in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4)
.fill(Color.white.opacity(0.1))
.frame(height: 14)
RoundedRectangle(cornerRadius: 4)
.fill(gaugeColor)
.frame(width: max(geo.size.width * CGFloat(value / 100), 3), height: 14)
.animation(.easeOut(duration: 0.2), value: value)
}
}
.frame(height: 14)
HStack {
Text(String(format: "%.1f%@", value, unit))
.font(.system(size: fontSize, weight: .bold, design: .monospaced))
.foregroundColor(.primary)
Spacer()
Text(detail) Text(detail)
.font(.system(size: fontSize - 2)) .font(.system(size: fontSize - 2))
.foregroundColor(.secondary) .foregroundColor(.secondary)
.lineLimit(1) .lineLimit(1)
} }
.animation(textAnim, value: value)
} }
} }
} }

View File

@ -56,7 +56,7 @@ struct SettingsView: View {
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.bottom, 12) .padding(.bottom, 12)
} }
.frame(width: 340, height: 640) .frame(width: 340, height: 680)
.background(.ultraThinMaterial) .background(.ultraThinMaterial)
} }
@ -73,6 +73,8 @@ struct SettingsView: View {
toggleRow("GPU", binding: $config.showGPU, icon: "display") toggleRow("GPU", binding: $config.showGPU, icon: "display")
toggleRow("内存", binding: $config.showMemory, icon: "memorychip") toggleRow("内存", binding: $config.showMemory, icon: "memorychip")
toggleRow("Swap", binding: $config.showSwap, icon: "arrow.left.arrow.right") toggleRow("Swap", binding: $config.showSwap, icon: "arrow.left.arrow.right")
toggleRow("CPU 温度", binding: $config.showCPUTemperature, icon: "thermometer.medium")
toggleRow("GPU 温度", binding: $config.showGPUTemperature, icon: "thermometer.low")
toggleRow("进程列表", binding: $config.showTopProcesses, icon: "list.bullet") toggleRow("进程列表", binding: $config.showTopProcesses, icon: "list.bullet")
} }
} }
@ -105,7 +107,9 @@ struct SettingsView: View {
styleRow("GPU", style: $config.gpuStyle, row: $config.gpuRow, pos: $config.gpuPos, icon: "display") styleRow("GPU", style: $config.gpuStyle, row: $config.gpuRow, pos: $config.gpuPos, icon: "display")
styleRow("内存", style: $config.memoryStyle, row: $config.memoryRow, pos: $config.memoryPos, icon: "memorychip") styleRow("内存", style: $config.memoryStyle, row: $config.memoryRow, pos: $config.memoryPos, icon: "memorychip")
styleRow("Swap", style: $config.swapStyle, row: $config.swapRow, pos: $config.swapPos, icon: "arrow.left.arrow.right") styleRow("Swap", style: $config.swapStyle, row: $config.swapRow, pos: $config.swapPos, icon: "arrow.left.arrow.right")
styleRow("进程", style: .constant(.compact), row: $config.processesRow, pos: $config.processesPos, styleRow("CPU温度", style: $config.cpuTemperatureStyle, row: $config.cpuTemperatureRow, pos: $config.cpuTemperaturePos, icon: "thermometer.medium")
styleRow("GPU温度", style: $config.gpuTemperatureStyle, row: $config.gpuTemperatureRow, pos: $config.gpuTemperaturePos, icon: "thermometer.low")
styleRow("进程", style: .constant(.bar), row: $config.processesRow, pos: $config.processesPos,
icon: "list.bullet", styleDisabled: true) icon: "list.bullet", styleDisabled: true)
} }
} }
@ -280,6 +284,26 @@ struct SettingsView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
.frame(width: 38, alignment: .trailing) .frame(width: 38, alignment: .trailing)
} }
HStack {
Text("图形动画")
.font(.system(size: 12))
Spacer()
Toggle("", isOn: $config.gaugeAnimationEnabled)
.toggleStyle(.switch)
.scaleEffect(0.7)
.frame(width: 38)
}
HStack {
Text("文字动画")
.font(.system(size: 12))
Spacer()
Toggle("", isOn: $config.textAnimationEnabled)
.toggleStyle(.switch)
.scaleEffect(0.7)
.frame(width: 38)
}
} }
} }

Binary file not shown.