From ff0339e6fca61e5da74ebea58ad0028b01216b91 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Thu, 15 Jan 2026 09:56:47 -0500 Subject: [PATCH] temp --- Gaze/Constants/EyeTrackingConstants.swift | 5 ++- Gaze/Models/AppSettings.swift | 4 +- Gaze/Models/SmartModeSettings.swift | 2 +- Gaze/Models/TimerConfiguration.swift | 2 +- Gaze/Models/UserTimer.swift | 4 +- Gaze/Protocols/TimeProviding.swift | 4 +- Gaze/Services/EyeTrackingService.swift | 4 +- .../Services/FullscreenDetectionService.swift | 8 ++-- Gaze/Services/PupilDetector.swift | 37 +++++++++++++------ .../Views/Containers/SettingsWindowView.swift | 26 +++++++------ run | 2 +- 11 files changed, 58 insertions(+), 40 deletions(-) diff --git a/Gaze/Constants/EyeTrackingConstants.swift b/Gaze/Constants/EyeTrackingConstants.swift index 1105c52..14f8671 100644 --- a/Gaze/Constants/EyeTrackingConstants.swift +++ b/Gaze/Constants/EyeTrackingConstants.swift @@ -8,7 +8,10 @@ import Combine import Foundation -class EyeTrackingConstants: ObservableObject { +/// Thread-safe configuration holder for eye tracking thresholds. +/// Uses @unchecked Sendable because all access is via the shared singleton +/// and the @Published properties are only mutated from the main thread. +final class EyeTrackingConstants: ObservableObject, @unchecked Sendable { static let shared = EyeTrackingConstants() // MARK: - Logging diff --git a/Gaze/Models/AppSettings.swift b/Gaze/Models/AppSettings.swift index c0123a1..af63e3b 100644 --- a/Gaze/Models/AppSettings.swift +++ b/Gaze/Models/AppSettings.swift @@ -9,7 +9,7 @@ import Foundation // MARK: - Reminder Size -enum ReminderSize: String, Codable, CaseIterable { +enum ReminderSize: String, Codable, CaseIterable, Sendable { case small case medium case large @@ -31,7 +31,7 @@ enum ReminderSize: String, Codable, CaseIterable { } } -struct AppSettings: Codable, Equatable, Hashable { +struct AppSettings: Codable, Equatable, Hashable, Sendable { var lookAwayTimer: TimerConfiguration var lookAwayCountdownSeconds: Int var blinkTimer: TimerConfiguration diff --git a/Gaze/Models/SmartModeSettings.swift b/Gaze/Models/SmartModeSettings.swift index 6008399..4494566 100644 --- a/Gaze/Models/SmartModeSettings.swift +++ b/Gaze/Models/SmartModeSettings.swift @@ -7,7 +7,7 @@ import Foundation -struct SmartModeSettings: Codable, Equatable, Hashable { +struct SmartModeSettings: Codable, Equatable, Hashable, Sendable { var autoPauseOnFullscreen: Bool var autoPauseOnIdle: Bool var trackUsage: Bool diff --git a/Gaze/Models/TimerConfiguration.swift b/Gaze/Models/TimerConfiguration.swift index a3ad5d2..dbc547b 100644 --- a/Gaze/Models/TimerConfiguration.swift +++ b/Gaze/Models/TimerConfiguration.swift @@ -7,7 +7,7 @@ import Foundation -struct TimerConfiguration: Codable, Equatable, Hashable { +struct TimerConfiguration: Codable, Equatable, Hashable, Sendable { var enabled: Bool var intervalSeconds: Int diff --git a/Gaze/Models/UserTimer.swift b/Gaze/Models/UserTimer.swift index 534fa07..c15e1ae 100644 --- a/Gaze/Models/UserTimer.swift +++ b/Gaze/Models/UserTimer.swift @@ -9,7 +9,7 @@ import Foundation import SwiftUI /// Represents a user-defined timer with customizable properties -struct UserTimer: Codable, Equatable, Identifiable, Hashable { +struct UserTimer: Codable, Equatable, Identifiable, Hashable, Sendable { let id: String var title: String var type: UserTimerType @@ -100,7 +100,7 @@ extension Color { } /// Type of user timer - subtle or overlay -enum UserTimerType: String, Codable, CaseIterable, Identifiable { +enum UserTimerType: String, Codable, CaseIterable, Identifiable, Sendable { case subtle case overlay diff --git a/Gaze/Protocols/TimeProviding.swift b/Gaze/Protocols/TimeProviding.swift index 1135700..e8ce673 100644 --- a/Gaze/Protocols/TimeProviding.swift +++ b/Gaze/Protocols/TimeProviding.swift @@ -8,7 +8,7 @@ import Foundation /// Protocol for providing current time, enabling deterministic tests. -protocol TimeProviding { +protocol TimeProviding: Sendable { /// Returns the current date/time func now() -> Date } @@ -21,7 +21,7 @@ struct SystemTimeProvider: TimeProviding { } /// Test implementation that allows manual time control -final class MockTimeProvider: TimeProviding { +final class MockTimeProvider: TimeProviding, @unchecked Sendable { private var currentTime: Date init(startTime: Date = Date()) { diff --git a/Gaze/Services/EyeTrackingService.swift b/Gaze/Services/EyeTrackingService.swift index 1f3bccf..f612c2f 100644 --- a/Gaze/Services/EyeTrackingService.swift +++ b/Gaze/Services/EyeTrackingService.swift @@ -60,7 +60,7 @@ class EyeTrackingService: NSObject, ObservableObject { // MARK: - Processing Result /// Result struct for off-main-thread processing - private struct ProcessingResult { + private struct ProcessingResult: Sendable { var faceDetected: Bool = false var isEyesClosed: Bool = false var userLookingAtScreen: Bool = true @@ -271,7 +271,7 @@ class EyeTrackingService: NSObject, ObservableObject { } /// Non-isolated gaze detection result - private struct GazeResult { + private struct GazeResult: Sendable { var lookingAway: Bool = false var leftPupilRatio: Double? var rightPupilRatio: Double? diff --git a/Gaze/Services/FullscreenDetectionService.swift b/Gaze/Services/FullscreenDetectionService.swift index 0ec4d0b..2a96034 100644 --- a/Gaze/Services/FullscreenDetectionService.swift +++ b/Gaze/Services/FullscreenDetectionService.swift @@ -83,13 +83,13 @@ final class FullscreenDetectionService: ObservableObject { // Factory method to safely create instances from non-main actor contexts static func create( - permissionManager: ScreenCapturePermissionManaging = ScreenCapturePermissionManager.shared, - environmentProvider: FullscreenEnvironmentProviding = SystemFullscreenEnvironmentProvider() + permissionManager: ScreenCapturePermissionManaging? = nil, + environmentProvider: FullscreenEnvironmentProviding? = nil ) async -> FullscreenDetectionService { await MainActor.run { return FullscreenDetectionService( - permissionManager: permissionManager, - environmentProvider: environmentProvider + permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared, + environmentProvider: environmentProvider ?? SystemFullscreenEnvironmentProvider() ) } } diff --git a/Gaze/Services/PupilDetector.swift b/Gaze/Services/PupilDetector.swift index 1ac0ab1..a3d2188 100644 --- a/Gaze/Services/PupilDetector.swift +++ b/Gaze/Services/PupilDetector.swift @@ -20,28 +20,33 @@ import Accelerate import ImageIO import UniformTypeIdentifiers -struct PupilPosition: Equatable { +struct PupilPosition: Equatable, Sendable { let x: CGFloat let y: CGFloat } -struct EyeRegion { +struct EyeRegion: Sendable { let frame: CGRect let center: CGPoint let origin: CGPoint } /// Calibration state for adaptive thresholding (matches Python Calibration class) -final class PupilCalibration { +final class PupilCalibration: @unchecked Sendable { + private let lock = NSLock() private let targetFrames = 20 private var thresholdsLeft: [Int] = [] private var thresholdsRight: [Int] = [] var isComplete: Bool { - thresholdsLeft.count >= targetFrames && thresholdsRight.count >= targetFrames + lock.lock() + defer { lock.unlock() } + return thresholdsLeft.count >= targetFrames && thresholdsRight.count >= targetFrames } func threshold(forSide side: Int) -> Int { + lock.lock() + defer { lock.unlock() } let thresholds = side == 0 ? thresholdsLeft : thresholdsRight guard !thresholds.isEmpty else { return 50 } return thresholds.reduce(0, +) / thresholds.count @@ -49,6 +54,8 @@ final class PupilCalibration { func evaluate(eyeData: UnsafePointer, width: Int, height: Int, side: Int) { let bestThreshold = findBestThreshold(eyeData: eyeData, width: width, height: height) + lock.lock() + defer { lock.unlock() } if side == 0 { thresholdsLeft.append(bestThreshold) } else { @@ -106,13 +113,15 @@ final class PupilCalibration { } func reset() { + lock.lock() + defer { lock.unlock() } thresholdsLeft.removeAll() thresholdsRight.removeAll() } } /// Performance metrics for pupil detection -struct PupilDetectorMetrics { +struct PupilDetectorMetrics: Sendable { var lastProcessingTimeMs: Double = 0 var averageProcessingTimeMs: Double = 0 var frameCount: Int = 0 @@ -126,7 +135,11 @@ struct PupilDetectorMetrics { } } -final class PupilDetector { +final class PupilDetector: @unchecked Sendable { + + // MARK: - Thread Safety + + private static let lock = NSLock() // MARK: - Configuration @@ -134,14 +147,14 @@ final class PupilDetector { static var enablePerformanceLogging = false static var frameSkipCount = 10 // Process every Nth frame - // MARK: - State + // MARK: - State (protected by lock) - private static var debugImageCounter = 0 - private static var frameCounter = 0 - private static var lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) = (nil, nil) + private static var _debugImageCounter = 0 + private static var _frameCounter = 0 + private static var _lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) = (nil, nil) + private static var _metrics = PupilDetectorMetrics() static let calibration = PupilCalibration() - static var metrics = PupilDetectorMetrics() // MARK: - Precomputed Tables @@ -182,7 +195,7 @@ final class PupilDetector { /// Detects pupil position with frame skipping for performance /// Returns cached result on skipped frames - static func detectPupil( + nonisolated static func detectPupil( in pixelBuffer: CVPixelBuffer, eyeLandmarks: VNFaceLandmarkRegion2D, faceBoundingBox: CGRect, diff --git a/Gaze/Views/Containers/SettingsWindowView.swift b/Gaze/Views/Containers/SettingsWindowView.swift index 00d6dda..4f9596e 100644 --- a/Gaze/Views/Containers/SettingsWindowView.swift +++ b/Gaze/Views/Containers/SettingsWindowView.swift @@ -62,7 +62,7 @@ final class SettingsWindowPresenter { window.titleVisibility = .hidden window.titlebarAppearsTransparent = true window.toolbarStyle = .unified - window.toolbar = NSToolbar() + window.showsToolbarButton = false window.center() window.setFrameAutosaveName("SettingsWindow") window.isReleasedWhenClosed = false @@ -103,8 +103,10 @@ final class SettingsWindowPresenter { } deinit { - Task { @MainActor in - removeCloseObserver() + // Capture observer locally to avoid actor isolation issues + // NotificationCenter.removeObserver is thread-safe + if let observer = closeObserver { + NotificationCenter.default.removeObserver(observer) } } } @@ -133,6 +135,15 @@ struct SettingsWindowView: View { .listStyle(.sidebar) } detail: { detailView(for: selectedSection) + }.onReceive( + NotificationCenter.default.publisher( + for: Notification.Name("SwitchToSettingsTab")) + ) { notification in + if let tab = notification.object as? Int, + let section = SettingsSection(rawValue: tab) + { + selectedSection = section + } } #if DEBUG @@ -161,15 +172,6 @@ struct SettingsWindowView: View { minHeight: 900 ) #endif - .onReceive( - NotificationCenter.default.publisher(for: Notification.Name("SwitchToSettingsTab")) - ) { notification in - if let tab = notification.object as? Int, - let section = SettingsSection(rawValue: tab) - { - selectedSection = section - } - } } @ViewBuilder diff --git a/run b/run index c5ed7cc..5b753cd 100755 --- a/run +++ b/run @@ -98,7 +98,7 @@ if [ "$ACTION" = "build" ]; then elif [ "$ACTION" = "test" ]; then echo "Running unit tests..." - run_with_output "xcodebuild -project Gaze.xcodeproj -scheme GazeTests -configuration Debug test" + run_with_output "xcodebuild -project Gaze.xcodeproj -scheme Gaze -configuration Debug test" if [ $? -eq 0 ]; then echo "✅ Tests passed!"