general:made logging consistent

This commit is contained in:
Michael Freno
2026-01-16 09:07:53 -05:00
parent ea3478dfb9
commit dce626e9c2
6 changed files with 159 additions and 110 deletions

View File

@@ -8,7 +8,6 @@
import AppKit import AppKit
import Combine import Combine
import SwiftUI import SwiftUI
import os.log
@MainActor @MainActor
class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject { class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
@@ -20,43 +19,36 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
private var hasStartedTimers = false private var hasStartedTimers = false
private var isSettingsWindowOpen = false private var isSettingsWindowOpen = false
private var isOnboardingWindowOpen = false private var isOnboardingWindowOpen = false
// Logging manager
private let logger = LoggingManager.shared
// Convenience accessor for settings // Convenience accessor for settings
private var settingsManager: any SettingsProviding { private var settingsManager: any SettingsProviding {
serviceContainer.settingsManager serviceContainer.settingsManager
} }
override init() { override init() {
self.serviceContainer = ServiceContainer.shared self.serviceContainer = ServiceContainer.shared
self.windowManager = WindowManager.shared self.windowManager = WindowManager.shared
super.init() super.init()
// Setup window close observers // Setup window close observers
setupWindowCloseObservers() setupWindowCloseObservers()
} }
/// Initializer for testing with injectable dependencies /// Initializer for testing with injectable dependencies
init(serviceContainer: ServiceContainer, windowManager: WindowManaging) { init(serviceContainer: ServiceContainer, windowManager: WindowManaging) {
self.serviceContainer = serviceContainer self.serviceContainer = serviceContainer
self.windowManager = windowManager self.windowManager = windowManager
super.init() super.init()
} }
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
// Set activation policy to hide dock icon // Set activation policy to hide dock icon
NSApplication.shared.setActivationPolicy(.accessory) NSApplication.shared.setActivationPolicy(.accessory)
// Initialize logging
logger.configureLogging()
logger.appLogger.info("🚀 Application did finish launching")
// Get timer engine from service container logInfo("🚀 Application did finish launching")
timerEngine = serviceContainer.timerEngine timerEngine = serviceContainer.timerEngine
// Setup smart mode services through container
serviceContainer.setupSmartModeServices() serviceContainer.setupSmartModeServices()
// Check if onboarding needs to be shown automatically // Check if onboarding needs to be shown automatically
@@ -87,7 +79,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
.removeDuplicates() .removeDuplicates()
.sink { [weak self] smartMode in .sink { [weak self] smartMode in
guard let self = self else { return } guard let self = self else { return }
self.serviceContainer.idleService?.updateThreshold(minutes: smartMode.idleThresholdMinutes) self.serviceContainer.idleService?.updateThreshold(
minutes: smartMode.idleThresholdMinutes)
self.serviceContainer.usageTrackingService?.updateResetThreshold( self.serviceContainer.usageTrackingService?.updateResetThreshold(
minutes: smartMode.usageResetAfterMinutes) minutes: smartMode.usageResetAfterMinutes)
@@ -110,7 +103,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
private func startTimers() { private func startTimers() {
guard !hasStartedTimers else { return } guard !hasStartedTimers else { return }
hasStartedTimers = true hasStartedTimers = true
logger.appLogger.info("Starting timers") logInfo("Starting timers")
timerEngine?.start() timerEngine?.start()
observeReminderEvents() observeReminderEvents()
} }
@@ -128,13 +121,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
// Also observe smart mode settings // Also observe smart mode settings
observeSmartModeSettings() observeSmartModeSettings()
} }
func applicationWillTerminate(_ notification: Notification) { func applicationWillTerminate(_ notification: Notification) {
logger.appLogger.info(" applicationWill terminate") logInfo(" applicationWill terminate")
settingsManager.saveImmediately() settingsManager.saveImmediately()
timerEngine?.stop() timerEngine?.stop()
} }
@@ -156,13 +149,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
} }
@objc private func systemWillSleep() { @objc private func systemWillSleep() {
logger.systemLogger.info("System will sleep") logInfo("System will sleep")
timerEngine?.handleSystemSleep() timerEngine?.handleSystemSleep()
settingsManager.saveImmediately() settingsManager.saveImmediately()
} }
@objc private func systemDidWake() { @objc private func systemDidWake() {
logger.systemLogger.info("System did wake") logInfo("System did wake")
timerEngine?.handleSystemWake() timerEngine?.handleSystemWake()
} }
@@ -185,21 +178,21 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
self?.timerEngine?.dismissReminder() self?.timerEngine?.dismissReminder()
} }
windowManager.showReminderWindow(view, windowType: .overlay) windowManager.showReminderWindow(view, windowType: .overlay)
case .blinkTriggered: case .blinkTriggered:
let sizePercentage = settingsManager.settings.subtleReminderSize.percentage let sizePercentage = settingsManager.settings.subtleReminderSize.percentage
let view = BlinkReminderView(sizePercentage: sizePercentage) { [weak self] in let view = BlinkReminderView(sizePercentage: sizePercentage) { [weak self] in
self?.timerEngine?.dismissReminder() self?.timerEngine?.dismissReminder()
} }
windowManager.showReminderWindow(view, windowType: .subtle) windowManager.showReminderWindow(view, windowType: .subtle)
case .postureTriggered: case .postureTriggered:
let sizePercentage = settingsManager.settings.subtleReminderSize.percentage let sizePercentage = settingsManager.settings.subtleReminderSize.percentage
let view = PostureReminderView(sizePercentage: sizePercentage) { [weak self] in let view = PostureReminderView(sizePercentage: sizePercentage) { [weak self] in
self?.timerEngine?.dismissReminder() self?.timerEngine?.dismissReminder()
} }
windowManager.showReminderWindow(view, windowType: .subtle) windowManager.showReminderWindow(view, windowType: .subtle)
case .userTimerTriggered(let timer): case .userTimerTriggered(let timer):
if timer.type == .overlay { if timer.type == .overlay {
let view = UserTimerOverlayReminderView(timer: timer) { [weak self] in let view = UserTimerOverlayReminderView(timer: timer) { [weak self] in
@@ -208,7 +201,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
windowManager.showReminderWindow(view, windowType: .overlay) windowManager.showReminderWindow(view, windowType: .overlay)
} else { } else {
let sizePercentage = settingsManager.settings.subtleReminderSize.percentage let sizePercentage = settingsManager.settings.subtleReminderSize.percentage
let view = UserTimerReminderView(timer: timer, sizePercentage: sizePercentage) { [weak self] in let view = UserTimerReminderView(timer: timer, sizePercentage: sizePercentage) {
[weak self] in
self?.timerEngine?.dismissReminder() self?.timerEngine?.dismissReminder()
} }
windowManager.showReminderWindow(view, windowType: .subtle) windowManager.showReminderWindow(view, windowType: .subtle)
@@ -228,7 +222,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
} }
return return
} }
handleMenuDismissal() handleMenuDismissal()
isSettingsWindowOpen = true isSettingsWindowOpen = true
@@ -247,7 +241,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
} }
return return
} }
handleMenuDismissal() handleMenuDismissal()
// Explicitly set the flag to true when we're about to show the onboarding window // Explicitly set the flag to true when we're about to show the onboarding window
isOnboardingWindowOpen = true isOnboardingWindowOpen = true
@@ -262,7 +256,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
NotificationCenter.default.post(name: Notification.Name("CloseMenuBarPopover"), object: nil) NotificationCenter.default.post(name: Notification.Name("CloseMenuBarPopover"), object: nil)
windowManager.dismissOverlayReminder() windowManager.dismissOverlayReminder()
} }
private func setupWindowCloseObservers() { private func setupWindowCloseObservers() {
// Observe settings window closing // Observe settings window closing
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
@@ -271,7 +265,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
name: Notification.Name("SettingsWindowDidClose"), name: Notification.Name("SettingsWindowDidClose"),
object: nil object: nil
) )
// Observe onboarding window closing // Observe onboarding window closing
NotificationCenter.default.addObserver( NotificationCenter.default.addObserver(
self, self,
@@ -280,11 +274,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
object: nil object: nil
) )
} }
@objc private func settingsWindowDidClose() { @objc private func settingsWindowDidClose() {
isSettingsWindowOpen = false isSettingsWindowOpen = false
} }
@objc private func onboardingWindowDidClose() { @objc private func onboardingWindowDidClose() {
// Reset the flag when we receive the close notification // Reset the flag when we receive the close notification
isOnboardingWindowOpen = false isOnboardingWindowOpen = false

View File

@@ -24,7 +24,12 @@ class EyeTrackingService: NSObject, ObservableObject {
@Published var debugRightPupilRatio: Double? @Published var debugRightPupilRatio: Double?
@Published var debugYaw: Double? @Published var debugYaw: Double?
@Published var debugPitch: Double? @Published var debugPitch: Double?
@Published var enableDebugLogging: Bool = false @Published var enableDebugLogging: Bool = false {
didSet {
// Sync with PupilDetector's diagnostic logging
PupilDetector.enableDiagnosticLogging = enableDebugLogging
}
}
// Throttle for debug logging // Throttle for debug logging
private var lastDebugLogTime: Date = .distantPast private var lastDebugLogTime: Date = .distantPast
@@ -228,6 +233,10 @@ class EyeTrackingService: NSObject, ObservableObject {
result.faceDetected = true result.faceDetected = true
let face = observations.first! let face = observations.first!
// Always extract yaw/pitch from face, even if landmarks aren't available
result.debugYaw = face.yaw?.doubleValue ?? 0.0
result.debugPitch = face.pitch?.doubleValue ?? 0.0
guard let landmarks = face.landmarks else { guard let landmarks = face.landmarks else {
return result return result
} }
@@ -660,6 +669,9 @@ extension EyeTrackingService: AVCaptureVideoDataOutputSampleBufferDelegate {
return return
} }
// Advance frame counter for pupil detector frame skipping
PupilDetector.advanceFrame()
let request = VNDetectFaceLandmarksRequest { [weak self] request, error in let request = VNDetectFaceLandmarksRequest { [weak self] request, error in
guard let self = self else { return } guard let self = self else { return }

View File

@@ -46,15 +46,12 @@ final class LoggingManager {
// MARK: - Initialization // MARK: - Initialization
private init() { private init() {
// Private initializer to enforce singleton pattern
} }
// MARK: - Public Methods // MARK: - Public Methods
/// Configure the logging system for verbose output when needed
func configureLogging() { func configureLogging() {
// For now, we'll use standard OSLog behavior. //nothing needed for now
// This can be extended in the future to support runtime log level changes.
} }
/// Convenience method for debug logging /// Convenience method for debug logging
@@ -86,8 +83,6 @@ final class LoggingManager {
} }
} }
// MARK: - Global Convenience Functions
/// Log an info message using the shared LoggingManager /// Log an info message using the shared LoggingManager
public func logInfo(_ message: String, category: String = "General") { public func logInfo(_ message: String, category: String = "General") {
LoggingManager.shared.info(message, category: category) LoggingManager.shared.info(message, category: category)

View File

@@ -63,7 +63,9 @@ final class PupilCalibration: @unchecked Sendable {
} }
} }
private nonisolated func findBestThreshold(eyeData: UnsafePointer<UInt8>, width: Int, height: Int) -> Int { private nonisolated func findBestThreshold(
eyeData: UnsafePointer<UInt8>, width: Int, height: Int
) -> Int {
let averageIrisSize = 0.48 let averageIrisSize = 0.48
var bestThreshold = 50 var bestThreshold = 50
var bestDiff = Double.greatestFiniteMagnitude var bestDiff = Double.greatestFiniteMagnitude
@@ -91,7 +93,9 @@ final class PupilCalibration: @unchecked Sendable {
return bestThreshold return bestThreshold
} }
private nonisolated static func irisSize(data: UnsafePointer<UInt8>, width: Int, height: Int) -> Double { private nonisolated static func irisSize(data: UnsafePointer<UInt8>, width: Int, height: Int)
-> Double
{
let margin = 5 let margin = 5
guard width > margin * 2, height > margin * 2 else { return 0 } guard width > margin * 2, height > margin * 2 else { return 0 }
@@ -145,15 +149,17 @@ final class PupilDetector: @unchecked Sendable {
nonisolated(unsafe) static var enableDebugImageSaving = false nonisolated(unsafe) static var enableDebugImageSaving = false
nonisolated(unsafe) static var enablePerformanceLogging = false nonisolated(unsafe) static var enablePerformanceLogging = false
nonisolated(unsafe) static var enableDiagnosticLogging = false
nonisolated(unsafe) static var frameSkipCount = 10 // Process every Nth frame nonisolated(unsafe) static var frameSkipCount = 10 // Process every Nth frame
// MARK: - State (protected by lock) // MARK: - State (protected by lock)
private nonisolated(unsafe) static var _debugImageCounter = 0 private nonisolated(unsafe) static var _debugImageCounter = 0
private nonisolated(unsafe) static var _frameCounter = 0 private nonisolated(unsafe) static var _frameCounter = 0
private nonisolated(unsafe) static var _lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) = ( private nonisolated(unsafe) static var _lastPupilPositions:
nil, nil (left: PupilPosition?, right: PupilPosition?) = (
) nil, nil
)
private nonisolated(unsafe) static var _metrics = PupilDetectorMetrics() private nonisolated(unsafe) static var _metrics = PupilDetectorMetrics()
nonisolated(unsafe) static let calibration = PupilCalibration() nonisolated(unsafe) static let calibration = PupilCalibration()
@@ -170,7 +176,8 @@ final class PupilDetector: @unchecked Sendable {
set { _frameCounter = newValue } set { _frameCounter = newValue }
} }
private nonisolated static var lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) { private nonisolated static var lastPupilPositions: (left: PupilPosition?, right: PupilPosition?)
{
get { _lastPupilPositions } get { _lastPupilPositions }
set { _lastPupilPositions = newValue } set { _lastPupilPositions = newValue }
} }
@@ -218,6 +225,11 @@ final class PupilDetector: @unchecked Sendable {
// MARK: - Public API // MARK: - Public API
/// Call once per video frame to enable proper frame skipping
nonisolated static func advanceFrame() {
frameCounter += 1
}
/// Detects pupil position with frame skipping for performance /// Detects pupil position with frame skipping for performance
/// Returns cached result on skipped frames /// Returns cached result on skipped frames
nonisolated static func detectPupil( nonisolated static func detectPupil(
@@ -252,7 +264,7 @@ final class PupilDetector: @unchecked Sendable {
let elapsed = (CFAbsoluteTimeGetCurrent() - startTime) * 1000 let elapsed = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
metrics.recordProcessingTime(elapsed) metrics.recordProcessingTime(elapsed)
if metrics.processedFrameCount % 30 == 0 { if metrics.processedFrameCount % 30 == 0 {
print( logDebug(
"👁 PupilDetector: \(String(format: "%.2f", elapsed))ms (avg: \(String(format: "%.2f", metrics.averageProcessingTimeMs))ms)" "👁 PupilDetector: \(String(format: "%.2f", elapsed))ms (avg: \(String(format: "%.2f", metrics.averageProcessingTimeMs))ms)"
) )
} }
@@ -266,10 +278,18 @@ final class PupilDetector: @unchecked Sendable {
imageSize: imageSize imageSize: imageSize
) )
guard eyePoints.count >= 6 else { return nil } guard eyePoints.count >= 6 else {
if enableDiagnosticLogging {
logDebug("👁 PupilDetector: Failed - eyePoints.count=\(eyePoints.count) < 6")
}
return nil
}
// Step 2: Create eye region bounding box with margin // Step 2: Create eye region bounding box with margin
guard let eyeRegion = createEyeRegion(from: eyePoints, imageSize: imageSize) else { guard let eyeRegion = createEyeRegion(from: eyePoints, imageSize: imageSize) else {
if enableDiagnosticLogging {
logDebug("👁 PupilDetector: Failed - createEyeRegion returned nil")
}
return nil return nil
} }
@@ -285,6 +305,9 @@ final class PupilDetector: @unchecked Sendable {
let eyeBuf = eyeBuffer, let eyeBuf = eyeBuffer,
let tmpBuf = tempBuffer let tmpBuf = tempBuffer
else { else {
if enableDiagnosticLogging {
logDebug("👁 PupilDetector: Failed - buffers not allocated")
}
return nil return nil
} }
@@ -293,6 +316,9 @@ final class PupilDetector: @unchecked Sendable {
extractGrayscaleDataOptimized( extractGrayscaleDataOptimized(
from: pixelBuffer, to: grayBuffer, width: frameWidth, height: frameHeight) from: pixelBuffer, to: grayBuffer, width: frameWidth, height: frameHeight)
else { else {
if enableDiagnosticLogging {
logDebug("👁 PupilDetector: Failed - grayscale extraction failed")
}
return nil return nil
} }
@@ -301,7 +327,13 @@ final class PupilDetector: @unchecked Sendable {
let eyeHeight = Int(eyeRegion.frame.height) let eyeHeight = Int(eyeRegion.frame.height)
// Early exit for tiny regions (less than 10x10 pixels) // Early exit for tiny regions (less than 10x10 pixels)
guard eyeWidth >= 10, eyeHeight >= 10 else { return nil } guard eyeWidth >= 10, eyeHeight >= 10 else {
if enableDiagnosticLogging {
logDebug(
"👁 PupilDetector: Failed - eye region too small (\(eyeWidth)x\(eyeHeight))")
}
return nil
}
guard guard
isolateEyeWithMaskOptimized( isolateEyeWithMaskOptimized(
@@ -313,6 +345,9 @@ final class PupilDetector: @unchecked Sendable {
output: eyeBuf output: eyeBuf
) )
else { else {
if enableDiagnosticLogging {
logDebug("👁 PupilDetector: Failed - isolateEyeWithMask failed")
}
return nil return nil
} }
@@ -352,9 +387,20 @@ final class PupilDetector: @unchecked Sendable {
height: eyeHeight height: eyeHeight
) )
else { else {
if enableDiagnosticLogging {
logDebug(
"👁 PupilDetector: Failed - findPupilFromContours returned nil (not enough dark pixels)"
)
}
return nil return nil
} }
if enableDiagnosticLogging {
logDebug(
"👁 PupilDetector: Success - centroid at (\(String(format: "%.1f", centroidX)), \(String(format: "%.1f", centroidY))) in \(eyeWidth)x\(eyeHeight) region"
)
}
let pupilPosition = PupilPosition(x: CGFloat(centroidX), y: CGFloat(centroidY)) let pupilPosition = PupilPosition(x: CGFloat(centroidX), y: CGFloat(centroidY))
// Cache result // Cache result
@@ -738,7 +784,9 @@ final class PupilDetector: @unchecked Sendable {
} }
} }
private nonisolated static func createEyeRegion(from points: [CGPoint], imageSize: CGSize) -> EyeRegion? { private nonisolated static func createEyeRegion(from points: [CGPoint], imageSize: CGSize)
-> EyeRegion?
{
guard !points.isEmpty else { return nil } guard !points.isEmpty else { return nil }
let margin: CGFloat = 5 let margin: CGFloat = 5
@@ -792,10 +840,12 @@ final class PupilDetector: @unchecked Sendable {
CGImageDestinationAddImage(destination, cgImage, nil) CGImageDestinationAddImage(destination, cgImage, nil)
CGImageDestinationFinalize(destination) CGImageDestinationFinalize(destination)
print("💾 Saved debug image: \(url.path)") logDebug("💾 Saved debug image: \(url.path)")
} }
private nonisolated static func createCGImage(from data: UnsafePointer<UInt8>, width: Int, height: Int) private nonisolated static func createCGImage(
from data: UnsafePointer<UInt8>, width: Int, height: Int
)
-> CGImage? -> CGImage?
{ {
let mutableData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height) let mutableData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height)

View File

@@ -7,7 +7,6 @@
import Combine import Combine
import Foundation import Foundation
import os.log
@MainActor @MainActor
class TimerEngine: ObservableObject { class TimerEngine: ObservableObject {
@@ -17,20 +16,17 @@ class TimerEngine: ObservableObject {
private var timerSubscription: AnyCancellable? private var timerSubscription: AnyCancellable?
private let settingsProvider: any SettingsProviding private let settingsProvider: any SettingsProviding
private var sleepStartTime: Date? private var sleepStartTime: Date?
/// Time provider for deterministic testing (defaults to system time) /// Time provider for deterministic testing (defaults to system time)
private let timeProvider: TimeProviding private let timeProvider: TimeProviding
// For enforce mode integration // For enforce mode integration
private var enforceModeService: EnforceModeService? private var enforceModeService: EnforceModeService?
// Smart Mode services // Smart Mode services
private var fullscreenService: FullscreenDetectionService? private var fullscreenService: FullscreenDetectionService?
private var idleService: IdleMonitoringService? private var idleService: IdleMonitoringService?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
// Logging manager
private let logger = LoggingManager.shared.timerLogger
convenience init( convenience init(
settingsManager: any SettingsProviding, settingsManager: any SettingsProviding,
@@ -51,19 +47,19 @@ class TimerEngine: ObservableObject {
self.settingsProvider = settingsManager self.settingsProvider = settingsManager
self.enforceModeService = enforceModeService ?? EnforceModeService.shared self.enforceModeService = enforceModeService ?? EnforceModeService.shared
self.timeProvider = timeProvider self.timeProvider = timeProvider
Task { @MainActor in Task { @MainActor in
self.enforceModeService?.setTimerEngine(self) self.enforceModeService?.setTimerEngine(self)
} }
} }
func setupSmartMode( func setupSmartMode(
fullscreenService: FullscreenDetectionService?, fullscreenService: FullscreenDetectionService?,
idleService: IdleMonitoringService? idleService: IdleMonitoringService?
) { ) {
self.fullscreenService = fullscreenService self.fullscreenService = fullscreenService
self.idleService = idleService self.idleService = idleService
// Subscribe to fullscreen state changes // Subscribe to fullscreen state changes
fullscreenService?.$isFullscreenActive fullscreenService?.$isFullscreenActive
.sink { [weak self] isFullscreen in .sink { [weak self] isFullscreen in
@@ -72,7 +68,7 @@ class TimerEngine: ObservableObject {
} }
} }
.store(in: &cancellables) .store(in: &cancellables)
// Subscribe to idle state changes // Subscribe to idle state changes
idleService?.$isIdle idleService?.$isIdle
.sink { [weak self] isIdle in .sink { [weak self] isIdle in
@@ -82,31 +78,31 @@ class TimerEngine: ObservableObject {
} }
.store(in: &cancellables) .store(in: &cancellables)
} }
private func handleFullscreenChange(isFullscreen: Bool) { private func handleFullscreenChange(isFullscreen: Bool) {
guard settingsProvider.settings.smartMode.autoPauseOnFullscreen else { return } guard settingsProvider.settings.smartMode.autoPauseOnFullscreen else { return }
if isFullscreen { if isFullscreen {
pauseAllTimers(reason: .fullscreen) pauseAllTimers(reason: .fullscreen)
logger.info("⏸️ Timers paused: fullscreen detected") logInfo("⏸️ Timers paused: fullscreen detected")
} else { } else {
resumeAllTimers(reason: .fullscreen) resumeAllTimers(reason: .fullscreen)
logger.info("▶️ Timers resumed: fullscreen exited") logInfo("▶️ Timers resumed: fullscreen exited")
} }
} }
private func handleIdleChange(isIdle: Bool) { private func handleIdleChange(isIdle: Bool) {
guard settingsProvider.settings.smartMode.autoPauseOnIdle else { return } guard settingsProvider.settings.smartMode.autoPauseOnIdle else { return }
if isIdle { if isIdle {
pauseAllTimers(reason: .idle) pauseAllTimers(reason: .idle)
logger.info("⏸️ Timers paused: user idle") logInfo("⏸️ Timers paused: user idle")
} else { } else {
resumeAllTimers(reason: .idle) resumeAllTimers(reason: .idle)
logger.info("▶️ Timers resumed: user active") logInfo("▶️ Timers resumed: user active")
} }
} }
private func pauseAllTimers(reason: PauseReason) { private func pauseAllTimers(reason: PauseReason) {
for (id, var state) in timerStates { for (id, var state) in timerStates {
state.pauseReasons.insert(reason) state.pauseReasons.insert(reason)
@@ -114,7 +110,7 @@ class TimerEngine: ObservableObject {
timerStates[id] = state timerStates[id] = state
} }
} }
private func resumeAllTimers(reason: PauseReason) { private func resumeAllTimers(reason: PauseReason) {
for (id, var state) in timerStates { for (id, var state) in timerStates {
state.pauseReasons.remove(reason) state.pauseReasons.remove(reason)
@@ -129,12 +125,12 @@ class TimerEngine: ObservableObject {
updateConfigurations() updateConfigurations()
return return
} }
// Initial start - create all timer states // Initial start - create all timer states
stop() stop()
var newStates: [TimerIdentifier: TimerState] = [:] var newStates: [TimerIdentifier: TimerState] = [:]
// Add built-in timers // Add built-in timers
for timerType in TimerType.allCases { for timerType in TimerType.allCases {
let config = settingsProvider.timerConfiguration(for: timerType) let config = settingsProvider.timerConfiguration(for: timerType)
@@ -148,7 +144,7 @@ class TimerEngine: ObservableObject {
) )
} }
} }
// Add user timers // Add user timers
for userTimer in settingsProvider.settings.userTimers where userTimer.enabled { for userTimer in settingsProvider.settings.userTimers where userTimer.enabled {
let identifier = TimerIdentifier.user(id: userTimer.id) let identifier = TimerIdentifier.user(id: userTimer.id)
@@ -159,7 +155,7 @@ class TimerEngine: ObservableObject {
isActive: true isActive: true
) )
} }
// Assign the entire dictionary at once to trigger @Published // Assign the entire dictionary at once to trigger @Published
timerStates = newStates timerStates = newStates
@@ -171,27 +167,27 @@ class TimerEngine: ObservableObject {
} }
} }
} }
/// Check if enforce mode is active and should affect timer behavior /// Check if enforce mode is active and should affect timer behavior
func checkEnforceMode() { func checkEnforceMode() {
// Deprecated - camera is now activated in handleTick before timer triggers // Deprecated - camera is now activated in handleTick before timer triggers
} }
private func updateConfigurations() { private func updateConfigurations() {
logger.debug("Updating timer configurations") logDebug("Updating timer configurations")
var newStates: [TimerIdentifier: TimerState] = [:] var newStates: [TimerIdentifier: TimerState] = [:]
// Update built-in timers // Update built-in timers
for timerType in TimerType.allCases { for timerType in TimerType.allCases {
let config = settingsProvider.timerConfiguration(for: timerType) let config = settingsProvider.timerConfiguration(for: timerType)
let identifier = TimerIdentifier.builtIn(timerType) let identifier = TimerIdentifier.builtIn(timerType)
if config.enabled { if config.enabled {
if let existingState = timerStates[identifier] { if let existingState = timerStates[identifier] {
// Timer exists - check if interval changed // Timer exists - check if interval changed
if existingState.originalIntervalSeconds != config.intervalSeconds { if existingState.originalIntervalSeconds != config.intervalSeconds {
// Interval changed - reset with new interval // Interval changed - reset with new interval
logger.debug("Timer interval changed") logDebug("Timer interval changed")
newStates[identifier] = TimerState( newStates[identifier] = TimerState(
identifier: identifier, identifier: identifier,
intervalSeconds: config.intervalSeconds, intervalSeconds: config.intervalSeconds,
@@ -204,7 +200,7 @@ class TimerEngine: ObservableObject {
} }
} else { } else {
// Timer was just enabled - create new state // Timer was just enabled - create new state
logger.debug("Timer enabled") logDebug("Timer enabled")
newStates[identifier] = TimerState( newStates[identifier] = TimerState(
identifier: identifier, identifier: identifier,
intervalSeconds: config.intervalSeconds, intervalSeconds: config.intervalSeconds,
@@ -215,18 +211,18 @@ class TimerEngine: ObservableObject {
} }
// If config.enabled is false and timer exists, it will be removed // If config.enabled is false and timer exists, it will be removed
} }
// Update user timers // Update user timers
for userTimer in settingsProvider.settings.userTimers { for userTimer in settingsProvider.settings.userTimers {
let identifier = TimerIdentifier.user(id: userTimer.id) let identifier = TimerIdentifier.user(id: userTimer.id)
let newIntervalSeconds = userTimer.intervalMinutes * 60 let newIntervalSeconds = userTimer.intervalMinutes * 60
if userTimer.enabled { if userTimer.enabled {
if let existingState = timerStates[identifier] { if let existingState = timerStates[identifier] {
// Check if interval changed // Check if interval changed
if existingState.originalIntervalSeconds != newIntervalSeconds { if existingState.originalIntervalSeconds != newIntervalSeconds {
// Interval changed - reset with new interval // Interval changed - reset with new interval
logger.debug("User timer interval changed") logDebug("User timer interval changed")
newStates[identifier] = TimerState( newStates[identifier] = TimerState(
identifier: identifier, identifier: identifier,
intervalSeconds: newIntervalSeconds, intervalSeconds: newIntervalSeconds,
@@ -239,7 +235,7 @@ class TimerEngine: ObservableObject {
} }
} else { } else {
// New timer - create state // New timer - create state
logger.debug("User timer created") logDebug("User timer created")
newStates[identifier] = TimerState( newStates[identifier] = TimerState(
identifier: identifier, identifier: identifier,
intervalSeconds: newIntervalSeconds, intervalSeconds: newIntervalSeconds,
@@ -250,7 +246,7 @@ class TimerEngine: ObservableObject {
} }
// If timer is disabled, it will be removed // If timer is disabled, it will be removed
} }
// Assign the entire dictionary at once to trigger @Published // Assign the entire dictionary at once to trigger @Published
timerStates = newStates timerStates = newStates
} }
@@ -276,14 +272,14 @@ class TimerEngine: ObservableObject {
timerStates[id] = state timerStates[id] = state
} }
} }
func pauseTimer(identifier: TimerIdentifier) { func pauseTimer(identifier: TimerIdentifier) {
guard var state = timerStates[identifier] else { return } guard var state = timerStates[identifier] else { return }
state.pauseReasons.insert(.manual) state.pauseReasons.insert(.manual)
state.isPaused = true state.isPaused = true
timerStates[identifier] = state timerStates[identifier] = state
} }
func resumeTimer(identifier: TimerIdentifier) { func resumeTimer(identifier: TimerIdentifier) {
guard var state = timerStates[identifier] else { return } guard var state = timerStates[identifier] else { return }
state.pauseReasons.remove(.manual) state.pauseReasons.remove(.manual)
@@ -293,17 +289,18 @@ class TimerEngine: ObservableObject {
func skipNext(identifier: TimerIdentifier) { func skipNext(identifier: TimerIdentifier) {
guard let state = timerStates[identifier] else { return } guard let state = timerStates[identifier] else { return }
let intervalSeconds: Int let intervalSeconds: Int
switch identifier { switch identifier {
case .builtIn(let type): case .builtIn(let type):
let config = settingsProvider.timerConfiguration(for: type) let config = settingsProvider.timerConfiguration(for: type)
intervalSeconds = config.intervalSeconds intervalSeconds = config.intervalSeconds
case .user(let id): case .user(let id):
guard let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id }) else { return } guard let userTimer = settingsProvider.settings.userTimers.first(where: { $0.id == id })
else { return }
intervalSeconds = userTimer.intervalMinutes * 60 intervalSeconds = userTimer.intervalMinutes * 60
} }
timerStates[identifier] = TimerState( timerStates[identifier] = TimerState(
identifier: identifier, identifier: identifier,
intervalSeconds: intervalSeconds, intervalSeconds: intervalSeconds,
@@ -319,7 +316,7 @@ class TimerEngine: ObservableObject {
let identifier = reminder.identifier let identifier = reminder.identifier
skipNext(identifier: identifier) skipNext(identifier: identifier)
resumeTimer(identifier: identifier) resumeTimer(identifier: identifier)
enforceModeService?.handleReminderDismissed() enforceModeService?.handleReminderDismissed()
} }
@@ -327,7 +324,7 @@ class TimerEngine: ObservableObject {
for (identifier, state) in timerStates { for (identifier, state) in timerStates {
guard !state.isPaused else { continue } guard !state.isPaused else { continue }
guard state.isActive else { continue } guard state.isActive else { continue }
if state.targetDate < timeProvider.now() - 3.0 { if state.targetDate < timeProvider.now() - 3.0 {
skipNext(identifier: identifier) skipNext(identifier: identifier)
continue continue
@@ -340,12 +337,13 @@ class TimerEngine: ObservableObject {
if case .builtIn(.lookAway) = identifier { if case .builtIn(.lookAway) = identifier {
if enforceModeService?.shouldEnforceBreak(for: identifier) == true { if enforceModeService?.shouldEnforceBreak(for: identifier) == true {
Task { @MainActor in Task { @MainActor in
await enforceModeService?.startCameraForLookawayTimer(secondsRemaining: updatedState.remainingSeconds) await enforceModeService?.startCameraForLookawayTimer(
secondsRemaining: updatedState.remainingSeconds)
} }
} }
} }
} }
if updatedState.remainingSeconds <= 0 { if updatedState.remainingSeconds <= 0 {
triggerReminder(for: identifier) triggerReminder(for: identifier)
break break
@@ -357,7 +355,7 @@ class TimerEngine: ObservableObject {
func triggerReminder(for identifier: TimerIdentifier) { func triggerReminder(for identifier: TimerIdentifier) {
// Pause only the timer that triggered // Pause only the timer that triggered
pauseTimer(identifier: identifier) pauseTimer(identifier: identifier)
switch identifier { switch identifier {
case .builtIn(let type): case .builtIn(let type):
switch type { switch type {
@@ -384,16 +382,16 @@ class TimerEngine: ObservableObject {
func getFormattedTimeRemaining(for identifier: TimerIdentifier) -> String { func getFormattedTimeRemaining(for identifier: TimerIdentifier) -> String {
return getTimeRemaining(for: identifier).formatAsTimerDurationFull() return getTimeRemaining(for: identifier).formatAsTimerDurationFull()
} }
func isTimerPaused(_ identifier: TimerIdentifier) -> Bool { func isTimerPaused(_ identifier: TimerIdentifier) -> Bool {
return timerStates[identifier]?.isPaused ?? true return timerStates[identifier]?.isPaused ?? true
} }
/// Handles system sleep event /// Handles system sleep event
/// - Saves current time for elapsed calculation /// - Saves current time for elapsed calculation
/// - Pauses all active timers /// - Pauses all active timers
func handleSystemSleep() { func handleSystemSleep() {
logger.debug("System going to sleep") logDebug("System going to sleep")
sleepStartTime = timeProvider.now() sleepStartTime = timeProvider.now()
for (id, var state) in timerStates { for (id, var state) in timerStates {
state.pauseReasons.insert(.system) state.pauseReasons.insert(.system)
@@ -401,24 +399,24 @@ class TimerEngine: ObservableObject {
timerStates[id] = state timerStates[id] = state
} }
} }
/// Handles system wake event /// Handles system wake event
/// - Calculates elapsed time during sleep /// - Calculates elapsed time during sleep
/// - Adjusts remaining time for all active timers /// - Adjusts remaining time for all active timers
/// - Timers that expired during sleep will trigger immediately (1s delay) /// - Timers that expired during sleep will trigger immediately (1s delay)
/// - Resumes all timers /// - Resumes all timers
func handleSystemWake() { func handleSystemWake() {
logger.debug("System waking up") logDebug("System waking up")
guard let sleepStart = sleepStartTime else { guard let sleepStart = sleepStartTime else {
return return
} }
defer { defer {
sleepStartTime = nil sleepStartTime = nil
} }
let elapsedSeconds = Int(timeProvider.now().timeIntervalSince(sleepStart)) let elapsedSeconds = Int(timeProvider.now().timeIntervalSince(sleepStart))
guard elapsedSeconds >= 1 else { guard elapsedSeconds >= 1 else {
for (id, var state) in timerStates { for (id, var state) in timerStates {
state.pauseReasons.remove(.system) state.pauseReasons.remove(.system)
@@ -427,18 +425,19 @@ class TimerEngine: ObservableObject {
} }
return return
} }
for (identifier, state) in timerStates where state.isActive { for (identifier, state) in timerStates where state.isActive {
var updatedState = state var updatedState = state
updatedState.remainingSeconds = max(0, state.remainingSeconds - elapsedSeconds) updatedState.remainingSeconds = max(0, state.remainingSeconds - elapsedSeconds)
if updatedState.remainingSeconds <= 0 { if updatedState.remainingSeconds <= 0 {
updatedState.remainingSeconds = 1 updatedState.remainingSeconds = 1
} }
updatedState.pauseReasons.remove(.system) updatedState.pauseReasons.remove(.system)
updatedState.isPaused = !updatedState.pauseReasons.isEmpty updatedState.isPaused = !updatedState.pauseReasons.isEmpty
timerStates[identifier] = updatedState timerStates[identifier] = updatedState
} }
} }
} }

View File

@@ -201,7 +201,6 @@ struct MenuBarContentView: View {
.padding(.vertical, 6) .padding(.vertical, 6)
} }
.buttonStyle(MenuBarHoverButtonStyle()) .buttonStyle(MenuBarHoverButtonStyle())
.padding(.horizontal, 8)
.padding(.vertical, 8) .padding(.vertical, 8)
Spacer() Spacer()
Text( Text(