feat: 实现状态栏应用和防睡眠功能
添加 AppState 类管理应用状态和防睡眠逻辑 将主应用改为状态栏应用并添加交互菜单 使用 IOKit 检测系统空闲时间并控制 caffeinate 进程
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import IOKit.pwr_mgt
|
||||
|
||||
// MARK: - App State Manager
|
||||
class AppState: ObservableObject {
|
||||
// MARK: - Published Properties (UI 会观察这些属性)
|
||||
|
||||
// 功能的总开关
|
||||
@Published var isFunctionEnabled: Bool = false
|
||||
|
||||
// 用户当前是否空闲
|
||||
@Published var isUserIdle: Bool = false
|
||||
|
||||
// 内部属性
|
||||
private var monitorTimer: Timer?
|
||||
private var caffeinateProcess: Process?
|
||||
|
||||
// 配置参数
|
||||
private let idleThreshold: TimeInterval = 300 // 5分钟无操作视为空闲
|
||||
private let checkInterval: TimeInterval = 10 // 每10秒检查一次
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// 切换主功能的开关
|
||||
func toggleFunction() {
|
||||
isFunctionEnabled.toggle()
|
||||
if isFunctionEnabled {
|
||||
// 开启功能,立即开始一轮检查,并设置定时器
|
||||
performCheck()
|
||||
startMonitoringTimer()
|
||||
} else {
|
||||
// 关闭功能,停止一切
|
||||
stopMonitoringTimer()
|
||||
stopCaffeinate()
|
||||
updateUserIdleStatus(false) // 重置状态
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Monitoring Logic
|
||||
|
||||
private func startMonitoringTimer() {
|
||||
guard monitorTimer == nil else { return }
|
||||
print("开始监控...")
|
||||
// 使用 weak self 避免循环引用
|
||||
monitorTimer = Timer.scheduledTimer(withTimeInterval: checkInterval, repeats: true) { [weak self] _ in
|
||||
self?.performCheck()
|
||||
}
|
||||
}
|
||||
|
||||
private func stopMonitoringTimer() {
|
||||
monitorTimer?.invalidate()
|
||||
monitorTimer = nil
|
||||
print("监控已停止。")
|
||||
}
|
||||
|
||||
private func performCheck() {
|
||||
// 1. 获取系统空闲时间
|
||||
let idleTime = getSystemIdleTime()
|
||||
|
||||
// 2. 判断是否空闲
|
||||
let isIdleNow = (idleTime >= idleThreshold)
|
||||
|
||||
// 3. 如果用户状态发生了变化,则采取行动
|
||||
if isIdleNow != isUserIdle {
|
||||
updateUserIdleStatus(isIdleNow)
|
||||
|
||||
if isIdleNow {
|
||||
// 从“活动中”变为“空闲”
|
||||
stopCaffeinate()
|
||||
} else {
|
||||
// 从“空闲”变为“活动中”
|
||||
startCaffeinate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User Idle Detection
|
||||
|
||||
/// 使用 IOKit 获取系统空闲时间 (键盘、鼠标、触控板无操作)
|
||||
/// - Returns: 空闲时间(秒)
|
||||
private func getSystemIdleTime() -> TimeInterval {
|
||||
// 这是一个更可靠、更原生的方法
|
||||
var onPort: mach_port_t = 0
|
||||
var prop: Unmanaged<CFTypeRef>?
|
||||
|
||||
// 获取 IO Master Port
|
||||
let kr = IOMainPort(0, &onPort)
|
||||
guard kr == KERN_SUCCESS else { return 0 }
|
||||
|
||||
// 查找电源管理服务
|
||||
let powerService = IOServiceGetMatchingService(onPort, IOServiceMatching("IOPMPowerSource"))
|
||||
guard powerService != 0 else { return 0 }
|
||||
|
||||
// 尝试获取空闲时间属性
|
||||
let result = IORegistryEntryCreateCFProperty(powerService, "HIDIdleTime" as CFString, kCFAllocatorDefault, 0)
|
||||
IOObjectRelease(powerService)
|
||||
|
||||
guard let data = result?.takeRetainedValue() as? Data else { return 0 }
|
||||
|
||||
// 数据通常是大端序的 UInt64
|
||||
var idleTime: UInt64 = 0
|
||||
(data as NSData).getBytes(&idleTime, length: MemoryLayout<UInt64>.size)
|
||||
|
||||
return Double(idleTime) / 1_000_000_000.0 // 纳秒转秒
|
||||
}
|
||||
|
||||
// MARK: - Caffeinate Management
|
||||
|
||||
private func startCaffeinate() {
|
||||
guard caffeinateProcess == nil else { return }
|
||||
|
||||
print("用户活动,启动 caffeinate 阻止睡眠...")
|
||||
caffeinateProcess = Process()
|
||||
caffeinateProcess?.executableURL = URL(fileURLWithPath: "/usr/bin/caffeinate")
|
||||
caffeinateProcess?.arguments = ["-d"] // -d 指示系统不要关闭显示屏
|
||||
|
||||
do {
|
||||
try caffeinateProcess?.run()
|
||||
} catch {
|
||||
print("启动 caffeinate 失败: \(error)")
|
||||
caffeinateProcess = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func stopCaffeinate() {
|
||||
guard caffeinateProcess != nil else { return }
|
||||
|
||||
print("用户空闲或功能关闭,停止 caffeinate.")
|
||||
caffeinateProcess?.terminate()
|
||||
caffeinateProcess = nil
|
||||
}
|
||||
|
||||
// MARK: - UI State Update
|
||||
|
||||
/// 在主线程更新用户空闲状态
|
||||
private func updateUserIdleStatus(_ isIdle: Bool) {
|
||||
DispatchQueue.main.async {
|
||||
self.isUserIdle = isIdle
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Deinitializer
|
||||
|
||||
deinit {
|
||||
// 确保 App 退出时清理资源
|
||||
stopMonitoringTimer()
|
||||
stopCaffeinate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,82 @@
|
||||
//
|
||||
// PowerBarApp.swift
|
||||
// PowerBar
|
||||
//
|
||||
// Created by Leo on 2025/12/27.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct PowerBarApp: App {
|
||||
// 创建一个状态对象来管理 App 的核心逻辑
|
||||
@StateObject private var appState = AppState()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
// 使用 MenuBarExtra 创建一个状态栏应用
|
||||
// 这里的字符串 "PowerBar" 是 fallback,如果系统不支持菜单栏标题,则会显示它
|
||||
// 系统会自动处理标题和图标的显示
|
||||
MenuBarExtra {
|
||||
// 这是点击状态栏图标后弹出的菜单视图
|
||||
MenuContent()
|
||||
.environmentObject(appState)
|
||||
} label: {
|
||||
// 这是状态栏上显示的图标和标题
|
||||
// 我们会根据 appState 的状态动态改变它
|
||||
Label {
|
||||
Text("PowerBar")
|
||||
} icon: {
|
||||
Image(systemName: appState.isFunctionEnabled ? "sun.max.fill" : "sun.max")
|
||||
.foregroundColor(appState.isFunctionEnabled ? .green : .gray)
|
||||
}
|
||||
}
|
||||
// 移除 setting,因为我们不需要设置窗口
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单内容视图
|
||||
struct MenuContent: View {
|
||||
@EnvironmentObject var appState: AppState
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
// 主要操作按钮:切换功能
|
||||
Button(action: {
|
||||
// 点击按钮,切换功能的开关状态
|
||||
appState.toggleFunction()
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: appState.isFunctionEnabled ? "stop.circle.fill" : "play.circle.fill")
|
||||
.foregroundColor(appState.isFunctionEnabled ? .red : .green)
|
||||
.frame(width: 20)
|
||||
Text(appState.isFunctionEnabled ? "关闭常亮" : "开启常亮")
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
// 状态显示
|
||||
HStack {
|
||||
Text("当前状态:")
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
if appState.isFunctionEnabled {
|
||||
Text(appState.isUserIdle ? "空闲" : "活动中")
|
||||
.foregroundColor(appState.isUserIdle ? .orange : .green)
|
||||
|
||||
// 附加显示 caffeinate 状态
|
||||
if !appState.isUserIdle {
|
||||
Text(" (保护中)")
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
} else {
|
||||
Text("已关闭")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
|
||||
Divider()
|
||||
|
||||
// 退出应用
|
||||
Button("退出 PowerBar") {
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
.frame(minWidth: 220)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user