添加温度显示(CPU/GPU独立模块,SMC传感器),拆分温度配置,去掉紧凑横条,添加图形/文字动画开关,数值动画优化
This commit is contained in:
parent
ee96706ab1
commit
f749771135
@ -7,7 +7,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable {
|
||||
case ring // 圆环进度
|
||||
case text // 纯文本百分比
|
||||
case pie // 扇形小圆盘
|
||||
case compact // 紧凑横条 + 文字
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
@ -17,7 +16,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable {
|
||||
case .ring: return "圆环进度"
|
||||
case .text: return "纯文本"
|
||||
case .pie: return "扇形圆盘"
|
||||
case .compact: return "紧凑横条"
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +25,6 @@ enum GaugeDisplayStyle: String, CaseIterable, Identifiable, Codable {
|
||||
case .ring: return "circle.dotted"
|
||||
case .text: return "textformat"
|
||||
case .pie: return "chart.pie"
|
||||
case .compact: return "rectangle.compress.vertical"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -65,6 +62,12 @@ final class Configuration: ObservableObject {
|
||||
@Published var swapStyle: GaugeDisplayStyle {
|
||||
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 {
|
||||
didSet { save("showCPU", showCPU) }
|
||||
@ -78,6 +81,15 @@ final class Configuration: ObservableObject {
|
||||
@Published var showSwap: Bool {
|
||||
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 {
|
||||
didSet { save("showTopProcesses", showTopProcesses) }
|
||||
}
|
||||
@ -101,6 +113,13 @@ final class Configuration: ObservableObject {
|
||||
didSet { save("windowHeight", windowHeight) }
|
||||
}
|
||||
|
||||
@Published var gaugeAnimationEnabled: Bool {
|
||||
didSet { save("gaugeAnimationEnabled", gaugeAnimationEnabled) }
|
||||
}
|
||||
@Published var textAnimationEnabled: Bool {
|
||||
didSet { save("textAnimationEnabled", textAnimationEnabled) }
|
||||
}
|
||||
|
||||
@Published var cpuPos: ModulePosition {
|
||||
didSet { save("cpuPos", cpuPos.rawValue) }
|
||||
}
|
||||
@ -113,6 +132,15 @@ final class Configuration: ObservableObject {
|
||||
@Published var swapPos: ModulePosition {
|
||||
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 {
|
||||
didSet { save("processesPos", processesPos.rawValue) }
|
||||
}
|
||||
@ -130,6 +158,15 @@ final class Configuration: ObservableObject {
|
||||
@Published var swapRow: Int {
|
||||
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 {
|
||||
didSet { save("processesRow", processesRow) }
|
||||
}
|
||||
@ -141,11 +178,16 @@ final class Configuration: ObservableObject {
|
||||
gpuStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "gpuStyle") ?? "ring") ?? .ring
|
||||
memoryStyle = GaugeDisplayStyle(rawValue: defaults.string(forKey: "memoryStyle") ?? "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
|
||||
showGPU = defaults.object(forKey: "showGPU") as? Bool ?? true
|
||||
showMemory = defaults.object(forKey: "showMemory") 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
|
||||
|
||||
processCount = defaults.object(forKey: "processCount") as? Int ?? 5
|
||||
@ -155,16 +197,25 @@ final class Configuration: ObservableObject {
|
||||
windowWidth = defaults.object(forKey: "windowWidth") as? Double ?? 250
|
||||
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
|
||||
gpuPos = ModulePosition(rawValue: defaults.string(forKey: "gpuPos") ?? "full") ?? .full
|
||||
memoryPos = ModulePosition(rawValue: defaults.string(forKey: "memoryPos") ?? "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
|
||||
|
||||
cpuRow = defaults.object(forKey: "cpuRow") as? Int ?? 1
|
||||
gpuRow = defaults.object(forKey: "gpuRow") as? Int ?? 2
|
||||
memoryRow = defaults.object(forKey: "memoryRow") as? Int ?? 3
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@ -88,7 +88,9 @@ struct ContentView: View {
|
||||
title: "CPU", icon: "cpu",
|
||||
value: monitor.info.cpuUsage, unit: "%",
|
||||
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",
|
||||
value: monitor.info.gpuUsage ?? 0, unit: "%",
|
||||
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",
|
||||
value: monitor.info.memoryUsagePercent, unit: "%",
|
||||
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",
|
||||
value: monitor.info.swapUsagePercent, unit: "%",
|
||||
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 {
|
||||
VStack(spacing: 12) {
|
||||
ProcessListView(
|
||||
@ -162,6 +204,14 @@ struct ContentView: View {
|
||||
items.append(Item(name: "Swap", row: config.swapRow, position: config.swapPos,
|
||||
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 {
|
||||
items.append(Item(name: "进程", row: config.processesRow, position: config.processesPos,
|
||||
view: AnyView(processesSection)))
|
||||
|
||||
259
Sources/FloatingMonitor/SMCSensor.swift
Normal file
259
Sources/FloatingMonitor/SMCSensor.swift
Normal 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 // 子 selector(5=读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))
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,9 @@ struct SystemInfo {
|
||||
var swapIns: UInt64
|
||||
var swapOuts: UInt64
|
||||
|
||||
var cpuTemperature: Double? // °C
|
||||
var gpuTemperature: Double? // °C
|
||||
|
||||
var topCPUProcesses: [ProcessInfo]
|
||||
var topMemoryProcesses: [ProcessInfo]
|
||||
|
||||
@ -44,6 +47,7 @@ final class SystemMonitor: ObservableObject {
|
||||
memoryTotal: 0, memoryUsed: 0, memoryUsagePercent: 0,
|
||||
swapTotal: 0, swapUsed: 0, swapUsagePercent: 0,
|
||||
swapIns: 0, swapOuts: 0,
|
||||
cpuTemperature: nil, gpuTemperature: nil,
|
||||
topCPUProcesses: [], topMemoryProcesses: []
|
||||
)
|
||||
|
||||
@ -90,6 +94,7 @@ final class SystemMonitor: ObservableObject {
|
||||
let gpu = fetchGPU()
|
||||
let mem = fetchMemory()
|
||||
let swap = fetchSwap()
|
||||
let temp = fetchTemperature()
|
||||
let procs = fetchTopProcesses(count: topN)
|
||||
|
||||
info = SystemInfo(
|
||||
@ -106,6 +111,8 @@ final class SystemMonitor: ObservableObject {
|
||||
swapUsagePercent: swap.percent,
|
||||
swapIns: swap.ins,
|
||||
swapOuts: swap.outs,
|
||||
cpuTemperature: temp.cpu,
|
||||
gpuTemperature: temp.gpu,
|
||||
topCPUProcesses: procs.cpu,
|
||||
topMemoryProcesses: procs.mem
|
||||
)
|
||||
@ -266,6 +273,14 @@ final class SystemMonitor: ObservableObject {
|
||||
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: - 进程列表
|
||||
|
||||
private func fetchTopProcesses(count: Int) -> (cpu: [SystemInfo.ProcessInfo], mem: [SystemInfo.ProcessInfo]) {
|
||||
|
||||
@ -5,23 +5,46 @@ import SwiftUI
|
||||
struct StyledGauge: View {
|
||||
let title: String
|
||||
let icon: String
|
||||
let value: Double // 0-100
|
||||
let unit: String // "%" / "GB"
|
||||
let detail: String // 副标题(如 "3.2 / 16 GB")
|
||||
let value: Double
|
||||
let unit: String
|
||||
let detail: String
|
||||
let style: GaugeDisplayStyle
|
||||
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 {
|
||||
if value < 40 { return .green }
|
||||
if value < 70 { return .yellow }
|
||||
if value < 85 { return .orange }
|
||||
if progress < 0.4 { return .green }
|
||||
if progress < 0.7 { return .yellow }
|
||||
if progress < 0.85 { return .orange }
|
||||
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 {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
// 标题行
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: fontSize))
|
||||
@ -32,18 +55,11 @@ struct StyledGauge: View {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// 根据样式渲染
|
||||
switch style {
|
||||
case .bar:
|
||||
barView
|
||||
case .ring:
|
||||
ringView
|
||||
case .text:
|
||||
textView
|
||||
case .pie:
|
||||
pieView
|
||||
case .compact:
|
||||
compactView
|
||||
case .bar: barView
|
||||
case .ring: ringView
|
||||
case .text: textView
|
||||
case .pie: pieView
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -58,29 +74,26 @@ struct StyledGauge: View {
|
||||
.fill(Color.white.opacity(0.12))
|
||||
.frame(height: 8)
|
||||
Capsule()
|
||||
.fill(
|
||||
LinearGradient(
|
||||
.fill(LinearGradient(
|
||||
colors: [gaugeColor.opacity(0.8), gaugeColor],
|
||||
startPoint: .leading,
|
||||
endPoint: .trailing
|
||||
)
|
||||
)
|
||||
.frame(width: max(geo.size.width * CGFloat(value / 100), 4), height: 8)
|
||||
.animation(.easeOut(duration: 0.2), value: value)
|
||||
startPoint: .leading, endPoint: .trailing))
|
||||
.frame(width: max(geo.size.width * CGFloat(progress), 4), height: 8)
|
||||
.animation(graphicAnim, value: value)
|
||||
}
|
||||
}
|
||||
.frame(height: 8)
|
||||
|
||||
HStack {
|
||||
Text(String(format: "%.1f%@", value, unit))
|
||||
animatedNumber(Text(String(format: "%.1f%@", value, unit))
|
||||
.font(.system(size: fontSize + 2, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(.primary)
|
||||
.foregroundColor(.primary))
|
||||
Spacer()
|
||||
Text(detail)
|
||||
.font(.system(size: fontSize - 2))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.animation(textAnim, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,33 +105,32 @@ struct StyledGauge: View {
|
||||
Circle()
|
||||
.stroke(Color.white.opacity(0.12), lineWidth: 5)
|
||||
.frame(width: 48, height: 48)
|
||||
.animation(.easeOut(duration: 0.2), value: value)
|
||||
Circle()
|
||||
.trim(from: 0, to: CGFloat(value / 100))
|
||||
.stroke(
|
||||
AngularGradient(
|
||||
.trim(from: 0, to: CGFloat(progress))
|
||||
.stroke(AngularGradient(
|
||||
colors: [gaugeColor.opacity(0.7), gaugeColor, gaugeColor.opacity(0.9)],
|
||||
center: .center
|
||||
),
|
||||
style: StrokeStyle(lineWidth: 5, lineCap: .round)
|
||||
)
|
||||
center: .center),
|
||||
style: StrokeStyle(lineWidth: 5, lineCap: .round))
|
||||
.rotationEffect(.degrees(-90))
|
||||
.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))
|
||||
.foregroundColor(.primary)
|
||||
.foregroundColor(.primary))
|
||||
}
|
||||
.animation(textAnim, value: value)
|
||||
|
||||
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))
|
||||
.foregroundColor(.primary)
|
||||
.foregroundColor(.primary))
|
||||
Text(detail)
|
||||
.font(.system(size: fontSize - 2))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.animation(textAnim, value: value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,10 +138,10 @@ struct StyledGauge: View {
|
||||
|
||||
private var textView: some View {
|
||||
HStack {
|
||||
Text(String(format: "%.1f%@", value, unit))
|
||||
animatedNumber(Text(String(format: "%.1f%@", value, unit))
|
||||
.font(.system(size: fontSize + 6, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(gaugeColor)
|
||||
.animation(.easeOut(duration: 0.2), value: value)
|
||||
.foregroundColor(gaugeColor))
|
||||
.animation(textAnim, value: value)
|
||||
Spacer()
|
||||
Text(detail)
|
||||
.font(.system(size: fontSize - 2))
|
||||
@ -147,56 +159,28 @@ struct StyledGauge: View {
|
||||
.fill(Color.white.opacity(0.05))
|
||||
.frame(width: 44, height: 44)
|
||||
|
||||
PieSlice(endAngle: .degrees(360 * value / 100))
|
||||
PieSlice(endAngle: .degrees(360 * progress))
|
||||
.fill(gaugeColor.opacity(0.85))
|
||||
.frame(width: 42, height: 42)
|
||||
.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))
|
||||
.foregroundColor(.white)
|
||||
.foregroundColor(.white))
|
||||
}
|
||||
.animation(textAnim, value: value)
|
||||
|
||||
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))
|
||||
.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()
|
||||
.foregroundColor(.primary))
|
||||
Text(detail)
|
||||
.font(.system(size: fontSize - 2))
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.animation(textAnim, value: value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +56,7 @@ struct SettingsView: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.bottom, 12)
|
||||
}
|
||||
.frame(width: 340, height: 640)
|
||||
.frame(width: 340, height: 680)
|
||||
.background(.ultraThinMaterial)
|
||||
}
|
||||
|
||||
@ -73,6 +73,8 @@ struct SettingsView: View {
|
||||
toggleRow("GPU", binding: $config.showGPU, icon: "display")
|
||||
toggleRow("内存", binding: $config.showMemory, icon: "memorychip")
|
||||
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")
|
||||
}
|
||||
}
|
||||
@ -105,7 +107,9 @@ struct SettingsView: View {
|
||||
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("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)
|
||||
}
|
||||
}
|
||||
@ -280,6 +284,26 @@ struct SettingsView: View {
|
||||
.foregroundColor(.secondary)
|
||||
.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.
Loading…
Reference in New Issue
Block a user