import AppKit import Foundation final class AppDelegate: NSObject, NSApplicationDelegate { private var statusItem: NSStatusItem! private let sleepManager = SleepManager() private var timer: Timer? private var expireTimer: Timer? private var expireDate: Date? private var batteryAwareDisable = false private lazy var defaults = UserDefaults.standard func applicationDidFinishLaunching(_ notification: Notification) { NSApp.setActivationPolicy(.accessory) defaults.register(defaults: [ "activateOnLaunch": true, "preventDisplaySleep": true, "preventSystemSleep": true ]) loadPreferences() setupStatusItem() startTickingTimer() if defaults.bool(forKey: "activateOnLaunch") { activateIndefinitely() } } func applicationWillTerminate(_ notification: Notification) { sleepManager.deactivate() savePreferences() } // MARK: - Preferences private func loadPreferences() { if defaults.object(forKey: "preventDisplaySleep") != nil { sleepManager.preventDisplaySleep = defaults.bool(forKey: "preventDisplaySleep") } if defaults.object(forKey: "preventSystemSleep") != nil { sleepManager.preventSystemSleep = defaults.bool(forKey: "preventSystemSleep") } batteryAwareDisable = defaults.bool(forKey: "batteryAwareDisable") } private func savePreferences() { defaults.set(sleepManager.preventDisplaySleep, forKey: "preventDisplaySleep") defaults.set(sleepManager.preventSystemSleep, forKey: "preventSystemSleep") defaults.set(batteryAwareDisable, forKey: "batteryAwareDisable") } // MARK: - Status Item private func setupStatusItem() { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) updateStatusIcon() rebuildMenu() } private func updateStatusIcon() { guard let button = statusItem.button else { return } button.image = Self.makeIcon(active: sleepManager.isActive) } private static let activeIcon: NSImage = loadIcon(active: true) private static let inactiveIcon: NSImage = loadIcon(active: false) private static func makeIcon(active: Bool) -> NSImage { active ? activeIcon : inactiveIcon } private static func loadIcon(active: Bool) -> NSImage { let url = Bundle.main.url(forResource: "samjokoo", withExtension: "pdf") ?? URL(fileURLWithPath: "/Users/charleskwon/Applications/KPopBird.app/Contents/Resources/samjokoo.pdf") guard let base = NSImage(contentsOf: url) else { return NSImage(systemSymbolName: "bird.fill", accessibilityDescription: "KPopBird") ?? NSImage() } let target = NSSize(width: 20, height: 18) let rendered = NSImage(size: target, flipped: false) { _ in let rect = NSRect(origin: .zero, size: target) if active { base.draw(in: rect) } else { base.draw(in: rect, from: .zero, operation: .sourceOver, fraction: 0.45) } return true } rendered.isTemplate = true return rendered } private func rebuildMenu() { let menu = NSMenu() let statusTitle: String if sleepManager.isActive { if let expire = expireDate { let remaining = Int(expire.timeIntervalSinceNow) statusTitle = "활성: \(formatDuration(remaining)) 남음" } else if let started = sleepManager.activatedAt { let elapsed = Int(Date().timeIntervalSince(started)) statusTitle = "활성: \(formatDuration(elapsed)) 경과" } else { statusTitle = "활성" } } else { statusTitle = "비활성" } let statusItemMenu = NSMenuItem(title: statusTitle, action: nil, keyEquivalent: "") statusItemMenu.isEnabled = false menu.addItem(statusItemMenu) menu.addItem(.separator()) let toggleTitle = sleepManager.isActive ? "비활성화" : "활성화 (무제한)" let toggleItem = NSMenuItem(title: toggleTitle, action: #selector(toggle), keyEquivalent: "t") toggleItem.target = self menu.addItem(toggleItem) let timerMenu = NSMenu() let durations: [(String, TimeInterval)] = [ ("15분", 15 * 60), ("30분", 30 * 60), ("1시간", 60 * 60), ("2시간", 2 * 60 * 60), ("4시간", 4 * 60 * 60) ] for (title, duration) in durations { let item = NSMenuItem(title: title, action: #selector(activateTimer(_:)), keyEquivalent: "") item.target = self item.representedObject = duration timerMenu.addItem(item) } let timerRoot = NSMenuItem(title: "타이머로 활성화", action: nil, keyEquivalent: "") timerRoot.submenu = timerMenu menu.addItem(timerRoot) menu.addItem(.separator()) let displayItem = NSMenuItem(title: "디스플레이 절전 방지", action: #selector(toggleDisplay), keyEquivalent: "") displayItem.target = self displayItem.state = sleepManager.preventDisplaySleep ? .on : .off menu.addItem(displayItem) let systemItem = NSMenuItem(title: "시스템 절전 방지", action: #selector(toggleSystem), keyEquivalent: "") systemItem.target = self systemItem.state = sleepManager.preventSystemSleep ? .on : .off menu.addItem(systemItem) let batteryItem = NSMenuItem(title: "배터리 사용 시 자동 해제", action: #selector(toggleBatteryAware), keyEquivalent: "") batteryItem.target = self batteryItem.state = batteryAwareDisable ? .on : .off menu.addItem(batteryItem) let launchItem = NSMenuItem(title: "로그인 시 자동 활성화", action: #selector(toggleActivateOnLaunch), keyEquivalent: "") launchItem.target = self launchItem.state = defaults.bool(forKey: "activateOnLaunch") ? .on : .off menu.addItem(launchItem) menu.addItem(.separator()) let powerInfo = SleepManager.isOnACPower() ? "전원 어댑터 연결됨" : "배터리 사용 중" let powerItem = NSMenuItem(title: powerInfo, action: nil, keyEquivalent: "") powerItem.isEnabled = false menu.addItem(powerItem) menu.addItem(.separator()) let quitItem = NSMenuItem(title: "KPopBird 종료", action: #selector(quit), keyEquivalent: "q") quitItem.target = self menu.addItem(quitItem) statusItem.menu = menu } // MARK: - Actions @objc private func toggle() { if sleepManager.isActive { deactivate() } else { activateIndefinitely() } } @objc private func activateTimer(_ sender: NSMenuItem) { guard let duration = sender.representedObject as? TimeInterval else { return } activate(for: duration) } @objc private func toggleDisplay() { sleepManager.preventDisplaySleep.toggle() savePreferences() if sleepManager.isActive { activateIndefinitely() } rebuildMenu() } @objc private func toggleSystem() { sleepManager.preventSystemSleep.toggle() savePreferences() if sleepManager.isActive { activateIndefinitely() } rebuildMenu() } @objc private func toggleBatteryAware() { batteryAwareDisable.toggle() savePreferences() rebuildMenu() } @objc private func toggleActivateOnLaunch() { let current = defaults.bool(forKey: "activateOnLaunch") defaults.set(!current, forKey: "activateOnLaunch") rebuildMenu() } @objc private func quit() { NSApp.terminate(nil) } // MARK: - Activation private func activateIndefinitely() { expireTimer?.invalidate() expireTimer = nil expireDate = nil sleepManager.activate() updateStatusIcon() rebuildMenu() } private func activate(for duration: TimeInterval) { sleepManager.activate() expireDate = Date().addingTimeInterval(duration) expireTimer?.invalidate() expireTimer = Timer.scheduledTimer(withTimeInterval: duration, repeats: false) { [weak self] _ in self?.deactivate() } updateStatusIcon() rebuildMenu() } private func deactivate() { sleepManager.deactivate() expireTimer?.invalidate() expireTimer = nil expireDate = nil updateStatusIcon() rebuildMenu() } // MARK: - Ticking private func startTickingTimer() { timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in guard let self = self else { return } if self.batteryAwareDisable, !SleepManager.isOnACPower(), self.sleepManager.isActive { self.deactivate() } self.rebuildMenu() } } private func formatDuration(_ seconds: Int) -> String { let s = max(0, seconds) let h = s / 3600 let m = (s % 3600) / 60 let sec = s % 60 if h > 0 { return String(format: "%d시간 %d분", h, m) } if m > 0 { return String(format: "%d분 %d초", m, sec) } return String(format: "%d초", sec) } }