import AppKit class AppDelegate: NSObject, NSApplicationDelegate { // Configuration var isDebugMode: Bool = false var workTimerDuration: Int { return isDebugMode ? 60 : 1500 // Debug: 1 min, Normal: 25 min } var countdownDuration: Int = 300 // 5 minutes of escalating flashes // Status bar var statusItem: NSStatusItem! var statusMenuItem: NSMenuItem! var debugMenuItem: NSMenuItem! var launchAtLoginMenuItem: NSMenuItem! // Overlay windows var overlayWindows: [NSWindow] = [] var countdownWindows: [NSWindow] = [] var countdownLabels: [NSTextField] = [] // Timers var countdownTimer: Timer? var breakTimer: Timer? // State var remainingSeconds: Int = 0 var isSolid = false var isPaused = false var currentSuggestion = "" var breakRemainingSeconds: Int = 300 var breakCountdownLabels: [NSTextField] = [] func applicationDidFinishLaunching(_ notification: Notification) { remainingSeconds = workTimerDuration setupStatusBar() setupWindows() startCountdown() NotificationManager.shared.requestPermission() NotificationManager.shared.postCycleStarted(durationMinutes: workTimerDuration / 60) } func startCountdown() { updateStatusBar() countdownTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in guard let self = self else { timer.invalidate(); return } if self.isPaused { return } self.remainingSeconds -= 1 self.updateStatusBar() if self.remainingSeconds <= self.countdownDuration && self.remainingSeconds > 0 { let s = self.remainingSeconds if s > 60 { // 5:00 to 3:00 -> every 60 seconds, 3:00 to 1:00 -> every 30 seconds let shouldBlink: Bool if s > 180 { shouldBlink = (s % 60 == 0) } else { shouldBlink = (s % 30 == 0) } if shouldBlink { self.triggerBlink() } } else if s > 5 { // Last minute: same as debug mode escalation let shouldBlink: Bool if s > 30 { shouldBlink = (s % 10 == 0) } else if s > 10 { shouldBlink = (s % 5 == 0) } else { shouldBlink = (s % 2 == 0) } if shouldBlink { self.triggerBlink() } } else { if !self.isSolid { self.makeSolid() } } if s <= 10 { self.showCountdownPills() self.updateCountdownLabels() } } if self.remainingSeconds <= 0 { self.showFullOverlay() } } } @objc func togglePause() { isPaused.toggle() updateStatusBar() } @objc func pauseFromOverlay() { isPaused = true breakTimer?.invalidate() updateStatusBar() resetOverlayToGradient() breakCountdownLabels.removeAll() } @objc func toggleDebugMode() { isDebugMode.toggle() debugMenuItem.state = isDebugMode ? .on : .off countdownTimer?.invalidate() breakTimer?.invalidate() breakCountdownLabels.removeAll() remainingSeconds = workTimerDuration isSolid = false isPaused = false resetOverlayToGradient() hideCountdownPills() for window in countdownWindows { window.orderFrontRegardless() } startCountdown() } @objc func toggleLaunchAtLogin() { if LaunchAgent.isInstalled() { LaunchAgent.uninstall() launchAtLoginMenuItem.state = .off } else { LaunchAgent.install() launchAtLoginMenuItem.state = .on } } @objc func closePommedoro() { countdownTimer?.invalidate() breakTimer?.invalidate() for window in overlayWindows { window.orderOut(nil) } for window in countdownWindows { window.orderOut(nil) } NSApp.terminate(nil) } @objc func resumePommedoro() { isPaused = false remainingSeconds = workTimerDuration isSolid = false breakTimer?.invalidate() breakCountdownLabels.removeAll() resetOverlayToGradient() hideCountdownPills() for window in countdownWindows { window.orderFrontRegardless() } startCountdown() NotificationManager.shared.postCycleStarted(durationMinutes: workTimerDuration / 60) } @objc func dismissSuggestion() { SuggestionManager.shared.dismiss(suggestion: currentSuggestion) resumePommedoro() } func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return false } }