- 菜单栏:CPU/GPU/内存多样式、网速两行、温度、风扇 - 面板:概览仪表盘、进程、散热、防休眠,液态玻璃与浅色/深色主题 - 采集层:CPU/内存/网络/GPU/磁盘/电池/进程/SMC 温度与风扇,按界面分级采样 - root 辅助工具:风扇调速、合盖不睡眠,断开时自动恢复 - XcodeGen 工程、单元测试、截图模式
190 lines
5.9 KiB
Swift
190 lines
5.9 KiB
Swift
import Foundation
|
||
import IOKit.pwr_mgt
|
||
import Metrics
|
||
import Observation
|
||
|
||
@MainActor
|
||
@Observable
|
||
public final class KeepAwakeController {
|
||
public enum Mode: String, CaseIterable, Identifiable {
|
||
case display, system
|
||
|
||
public var id: String { rawValue }
|
||
|
||
var title: String {
|
||
switch self {
|
||
case .display: "屏幕保持常亮"
|
||
case .system: "仅系统不休眠"
|
||
}
|
||
}
|
||
|
||
var subtitle: String {
|
||
switch self {
|
||
case .display: "屏幕和系统都不会因闲置而关闭或休眠"
|
||
case .system: "屏幕可按设置关闭,下载、编译等后台任务继续运行"
|
||
}
|
||
}
|
||
|
||
/// IOPMLib 中的 CFSTR 宏无法直接导入 Swift,这里使用其字符串值
|
||
var assertionType: String {
|
||
switch self {
|
||
case .display: "PreventUserIdleDisplaySleep"
|
||
case .system: "PreventUserIdleSystemSleep"
|
||
}
|
||
}
|
||
}
|
||
|
||
public enum Duration: Int, CaseIterable, Identifiable {
|
||
case minutes15 = 15, hour1 = 60, hours2 = 120, hours5 = 300, unlimited = 0
|
||
|
||
public var id: Int { rawValue }
|
||
|
||
var title: String {
|
||
switch self {
|
||
case .minutes15: "15 分钟"
|
||
case .hour1: "1 小时"
|
||
case .hours2: "2 小时"
|
||
case .hours5: "5 小时"
|
||
case .unlimited: "不限"
|
||
}
|
||
}
|
||
}
|
||
|
||
public private(set) var isActive = false
|
||
public private(set) var mode: Mode = .display
|
||
public private(set) var duration: Duration = .unlimited
|
||
public private(set) var endDate: Date?
|
||
/// 用户是否希望合盖后继续运行
|
||
public private(set) var lidClosedRequested = false
|
||
/// 辅助工具是否已实际关闭系统睡眠
|
||
public private(set) var lidClosedActive = false
|
||
public private(set) var notice: String?
|
||
public private(set) var noticeIsError = false
|
||
|
||
@ObservationIgnored private var assertionID: IOPMAssertionID = 0
|
||
@ObservationIgnored private var expiryTask: Task<Void, Never>?
|
||
@ObservationIgnored private let helper: HelperClient
|
||
@ObservationIgnored private let settings: AppSettings
|
||
|
||
init(helper: HelperClient, settings: AppSettings) {
|
||
self.helper = helper
|
||
self.settings = settings
|
||
}
|
||
|
||
func setActive(_ active: Bool) async {
|
||
active ? await start() : await stop(reason: nil)
|
||
}
|
||
|
||
func setMode(_ newMode: Mode) {
|
||
mode = newMode
|
||
if isActive { takeAssertion() }
|
||
}
|
||
|
||
func setDuration(_ newDuration: Duration) {
|
||
duration = newDuration
|
||
if isActive { scheduleExpiry() }
|
||
}
|
||
|
||
func setLidClosed(_ requested: Bool) async {
|
||
lidClosedRequested = requested
|
||
guard isActive else { return }
|
||
await applyLidMode()
|
||
}
|
||
|
||
func start() async {
|
||
isActive = true
|
||
notice = nil
|
||
takeAssertion()
|
||
scheduleExpiry()
|
||
if lidClosedRequested { await applyLidMode() }
|
||
}
|
||
|
||
func stop(reason: String?) async {
|
||
isActive = false
|
||
endDate = nil
|
||
expiryTask?.cancel()
|
||
releaseAssertion()
|
||
if lidClosedActive {
|
||
if let error = await helper.setSleepDisabled(false) {
|
||
show("恢复系统睡眠失败:\(error)", isError: true)
|
||
return
|
||
}
|
||
lidClosedActive = false
|
||
}
|
||
if let reason { show(reason, isError: false) }
|
||
}
|
||
|
||
/// 连接恢复后重新下发合盖设置
|
||
func reapply() async {
|
||
guard isActive, lidClosedRequested else { return }
|
||
await applyLidMode()
|
||
}
|
||
|
||
/// 电池电量保护:使用电池供电且电量低于下限时关闭合盖运行
|
||
func evaluateBattery(_ battery: BatteryStatus?) {
|
||
guard lidClosedActive, let battery, !battery.isPluggedIn,
|
||
battery.level * 100 < Double(settings.lidModeBatteryFloor) else { return }
|
||
Task {
|
||
lidClosedRequested = false
|
||
await applyLidMode()
|
||
show("电量低于 \(settings.lidModeBatteryFloor)%,已关闭合盖运行", isError: false)
|
||
}
|
||
}
|
||
|
||
/// 退出应用时释放电源断言(合盖设置由 HelperClient 同步恢复)
|
||
func releaseForTermination() {
|
||
releaseAssertion()
|
||
}
|
||
|
||
private func applyLidMode() async {
|
||
let wanted = isActive && lidClosedRequested
|
||
guard wanted != lidClosedActive else { return }
|
||
if let error = await helper.setSleepDisabled(wanted) {
|
||
lidClosedRequested = lidClosedActive
|
||
show(error, isError: true)
|
||
return
|
||
}
|
||
lidClosedActive = wanted
|
||
}
|
||
|
||
private func takeAssertion() {
|
||
releaseAssertion()
|
||
var id: IOPMAssertionID = 0
|
||
let result = IOPMAssertionCreateWithName(mode.assertionType as CFString,
|
||
IOPMAssertionLevel(kIOPMAssertionLevelOn),
|
||
"OpenStats 防休眠" as CFString,
|
||
&id)
|
||
if result == kIOReturnSuccess {
|
||
assertionID = id
|
||
} else {
|
||
show("无法创建电源断言(\(result))", isError: true)
|
||
}
|
||
}
|
||
|
||
private func releaseAssertion() {
|
||
guard assertionID != 0 else { return }
|
||
IOPMAssertionRelease(assertionID)
|
||
assertionID = 0
|
||
}
|
||
|
||
private func scheduleExpiry() {
|
||
expiryTask?.cancel()
|
||
guard duration != .unlimited else {
|
||
endDate = nil
|
||
return
|
||
}
|
||
let end = Date().addingTimeInterval(TimeInterval(duration.rawValue * 60))
|
||
endDate = end
|
||
expiryTask = Task { [weak self] in
|
||
try? await Task.sleep(for: .seconds(end.timeIntervalSinceNow))
|
||
guard !Task.isCancelled else { return }
|
||
await self?.stop(reason: "已到设定时间,防休眠已关闭")
|
||
}
|
||
}
|
||
|
||
private func show(_ message: String, isError: Bool) {
|
||
notice = message
|
||
noticeIsError = isError
|
||
}
|
||
}
|