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
|
import SwiftUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct PowerBarApp: App {
|
struct PowerBarApp: App {
|
||||||
|
// 创建一个状态对象来管理 App 的核心逻辑
|
||||||
|
@StateObject private var appState = AppState()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
// 使用 MenuBarExtra 创建一个状态栏应用
|
||||||
ContentView()
|
// 这里的字符串 "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