diff --git a/PowerBar/AppState.swift b/PowerBar/AppState.swift index e69de29..3b0c108 100644 --- a/PowerBar/AppState.swift +++ b/PowerBar/AppState.swift @@ -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? + + // 获取 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.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() + } +} diff --git a/PowerBar/PowerBarApp.swift b/PowerBar/PowerBarApp.swift index a17a7c7..8839be6 100644 --- a/PowerBar/PowerBarApp.swift +++ b/PowerBar/PowerBarApp.swift @@ -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) } }