This commit is contained in:
Michael Freno
2026-01-15 09:56:47 -05:00
parent 5dc223ec96
commit ff0339e6fc
11 changed files with 58 additions and 40 deletions

View File

@@ -8,7 +8,10 @@
import Combine import Combine
import Foundation 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() static let shared = EyeTrackingConstants()
// MARK: - Logging // MARK: - Logging

View File

@@ -9,7 +9,7 @@ import Foundation
// MARK: - Reminder Size // MARK: - Reminder Size
enum ReminderSize: String, Codable, CaseIterable { enum ReminderSize: String, Codable, CaseIterable, Sendable {
case small case small
case medium case medium
case large 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 lookAwayTimer: TimerConfiguration
var lookAwayCountdownSeconds: Int var lookAwayCountdownSeconds: Int
var blinkTimer: TimerConfiguration var blinkTimer: TimerConfiguration

View File

@@ -7,7 +7,7 @@
import Foundation import Foundation
struct SmartModeSettings: Codable, Equatable, Hashable { struct SmartModeSettings: Codable, Equatable, Hashable, Sendable {
var autoPauseOnFullscreen: Bool var autoPauseOnFullscreen: Bool
var autoPauseOnIdle: Bool var autoPauseOnIdle: Bool
var trackUsage: Bool var trackUsage: Bool

View File

@@ -7,7 +7,7 @@
import Foundation import Foundation
struct TimerConfiguration: Codable, Equatable, Hashable { struct TimerConfiguration: Codable, Equatable, Hashable, Sendable {
var enabled: Bool var enabled: Bool
var intervalSeconds: Int var intervalSeconds: Int

View File

@@ -9,7 +9,7 @@ import Foundation
import SwiftUI import SwiftUI
/// Represents a user-defined timer with customizable properties /// Represents a user-defined timer with customizable properties
struct UserTimer: Codable, Equatable, Identifiable, Hashable { struct UserTimer: Codable, Equatable, Identifiable, Hashable, Sendable {
let id: String let id: String
var title: String var title: String
var type: UserTimerType var type: UserTimerType
@@ -100,7 +100,7 @@ extension Color {
} }
/// Type of user timer - subtle or overlay /// Type of user timer - subtle or overlay
enum UserTimerType: String, Codable, CaseIterable, Identifiable { enum UserTimerType: String, Codable, CaseIterable, Identifiable, Sendable {
case subtle case subtle
case overlay case overlay

View File

@@ -8,7 +8,7 @@
import Foundation import Foundation
/// Protocol for providing current time, enabling deterministic tests. /// Protocol for providing current time, enabling deterministic tests.
protocol TimeProviding { protocol TimeProviding: Sendable {
/// Returns the current date/time /// Returns the current date/time
func now() -> Date func now() -> Date
} }
@@ -21,7 +21,7 @@ struct SystemTimeProvider: TimeProviding {
} }
/// Test implementation that allows manual time control /// Test implementation that allows manual time control
final class MockTimeProvider: TimeProviding { final class MockTimeProvider: TimeProviding, @unchecked Sendable {
private var currentTime: Date private var currentTime: Date
init(startTime: Date = Date()) { init(startTime: Date = Date()) {

View File

@@ -60,7 +60,7 @@ class EyeTrackingService: NSObject, ObservableObject {
// MARK: - Processing Result // MARK: - Processing Result
/// Result struct for off-main-thread processing /// Result struct for off-main-thread processing
private struct ProcessingResult { private struct ProcessingResult: Sendable {
var faceDetected: Bool = false var faceDetected: Bool = false
var isEyesClosed: Bool = false var isEyesClosed: Bool = false
var userLookingAtScreen: Bool = true var userLookingAtScreen: Bool = true
@@ -271,7 +271,7 @@ class EyeTrackingService: NSObject, ObservableObject {
} }
/// Non-isolated gaze detection result /// Non-isolated gaze detection result
private struct GazeResult { private struct GazeResult: Sendable {
var lookingAway: Bool = false var lookingAway: Bool = false
var leftPupilRatio: Double? var leftPupilRatio: Double?
var rightPupilRatio: Double? var rightPupilRatio: Double?

View File

@@ -83,13 +83,13 @@ final class FullscreenDetectionService: ObservableObject {
// Factory method to safely create instances from non-main actor contexts // Factory method to safely create instances from non-main actor contexts
static func create( static func create(
permissionManager: ScreenCapturePermissionManaging = ScreenCapturePermissionManager.shared, permissionManager: ScreenCapturePermissionManaging? = nil,
environmentProvider: FullscreenEnvironmentProviding = SystemFullscreenEnvironmentProvider() environmentProvider: FullscreenEnvironmentProviding? = nil
) async -> FullscreenDetectionService { ) async -> FullscreenDetectionService {
await MainActor.run { await MainActor.run {
return FullscreenDetectionService( return FullscreenDetectionService(
permissionManager: permissionManager, permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared,
environmentProvider: environmentProvider environmentProvider: environmentProvider ?? SystemFullscreenEnvironmentProvider()
) )
} }
} }

View File

@@ -20,28 +20,33 @@ import Accelerate
import ImageIO import ImageIO
import UniformTypeIdentifiers import UniformTypeIdentifiers
struct PupilPosition: Equatable { struct PupilPosition: Equatable, Sendable {
let x: CGFloat let x: CGFloat
let y: CGFloat let y: CGFloat
} }
struct EyeRegion { struct EyeRegion: Sendable {
let frame: CGRect let frame: CGRect
let center: CGPoint let center: CGPoint
let origin: CGPoint let origin: CGPoint
} }
/// Calibration state for adaptive thresholding (matches Python Calibration class) /// 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 let targetFrames = 20
private var thresholdsLeft: [Int] = [] private var thresholdsLeft: [Int] = []
private var thresholdsRight: [Int] = [] private var thresholdsRight: [Int] = []
var isComplete: Bool { 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 { func threshold(forSide side: Int) -> Int {
lock.lock()
defer { lock.unlock() }
let thresholds = side == 0 ? thresholdsLeft : thresholdsRight let thresholds = side == 0 ? thresholdsLeft : thresholdsRight
guard !thresholds.isEmpty else { return 50 } guard !thresholds.isEmpty else { return 50 }
return thresholds.reduce(0, +) / thresholds.count return thresholds.reduce(0, +) / thresholds.count
@@ -49,6 +54,8 @@ final class PupilCalibration {
func evaluate(eyeData: UnsafePointer<UInt8>, width: Int, height: Int, side: Int) { func evaluate(eyeData: UnsafePointer<UInt8>, width: Int, height: Int, side: Int) {
let bestThreshold = findBestThreshold(eyeData: eyeData, width: width, height: height) let bestThreshold = findBestThreshold(eyeData: eyeData, width: width, height: height)
lock.lock()
defer { lock.unlock() }
if side == 0 { if side == 0 {
thresholdsLeft.append(bestThreshold) thresholdsLeft.append(bestThreshold)
} else { } else {
@@ -106,13 +113,15 @@ final class PupilCalibration {
} }
func reset() { func reset() {
lock.lock()
defer { lock.unlock() }
thresholdsLeft.removeAll() thresholdsLeft.removeAll()
thresholdsRight.removeAll() thresholdsRight.removeAll()
} }
} }
/// Performance metrics for pupil detection /// Performance metrics for pupil detection
struct PupilDetectorMetrics { struct PupilDetectorMetrics: Sendable {
var lastProcessingTimeMs: Double = 0 var lastProcessingTimeMs: Double = 0
var averageProcessingTimeMs: Double = 0 var averageProcessingTimeMs: Double = 0
var frameCount: Int = 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 // MARK: - Configuration
@@ -134,14 +147,14 @@ final class PupilDetector {
static var enablePerformanceLogging = false static var enablePerformanceLogging = false
static var frameSkipCount = 10 // Process every Nth frame static var frameSkipCount = 10 // Process every Nth frame
// MARK: - State // MARK: - State (protected by lock)
private static var debugImageCounter = 0 private static var _debugImageCounter = 0
private static var frameCounter = 0 private static var _frameCounter = 0
private static var lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) = (nil, nil) private static var _lastPupilPositions: (left: PupilPosition?, right: PupilPosition?) = (nil, nil)
private static var _metrics = PupilDetectorMetrics()
static let calibration = PupilCalibration() static let calibration = PupilCalibration()
static var metrics = PupilDetectorMetrics()
// MARK: - Precomputed Tables // MARK: - Precomputed Tables
@@ -182,7 +195,7 @@ final class PupilDetector {
/// 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
static func detectPupil( nonisolated static func detectPupil(
in pixelBuffer: CVPixelBuffer, in pixelBuffer: CVPixelBuffer,
eyeLandmarks: VNFaceLandmarkRegion2D, eyeLandmarks: VNFaceLandmarkRegion2D,
faceBoundingBox: CGRect, faceBoundingBox: CGRect,

View File

@@ -62,7 +62,7 @@ final class SettingsWindowPresenter {
window.titleVisibility = .hidden window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true window.titlebarAppearsTransparent = true
window.toolbarStyle = .unified window.toolbarStyle = .unified
window.toolbar = NSToolbar() window.showsToolbarButton = false
window.center() window.center()
window.setFrameAutosaveName("SettingsWindow") window.setFrameAutosaveName("SettingsWindow")
window.isReleasedWhenClosed = false window.isReleasedWhenClosed = false
@@ -103,8 +103,10 @@ final class SettingsWindowPresenter {
} }
deinit { deinit {
Task { @MainActor in // Capture observer locally to avoid actor isolation issues
removeCloseObserver() // NotificationCenter.removeObserver is thread-safe
if let observer = closeObserver {
NotificationCenter.default.removeObserver(observer)
} }
} }
} }
@@ -133,6 +135,15 @@ struct SettingsWindowView: View {
.listStyle(.sidebar) .listStyle(.sidebar)
} detail: { } detail: {
detailView(for: selectedSection) 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 #if DEBUG
@@ -161,15 +172,6 @@ struct SettingsWindowView: View {
minHeight: 900 minHeight: 900
) )
#endif #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 @ViewBuilder

2
run
View File

@@ -98,7 +98,7 @@ if [ "$ACTION" = "build" ]; then
elif [ "$ACTION" = "test" ]; then elif [ "$ACTION" = "test" ]; then
echo "Running unit tests..." 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 if [ $? -eq 0 ]; then
echo "✅ Tests passed!" echo "✅ Tests passed!"