From 4b446db8174b75e08a344794b60cba3f10e0c0d2 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Fri, 30 Jan 2026 12:55:41 -0500 Subject: [PATCH] fix fullscreen --- .../System/FullscreenDetectionService.swift | 164 +++--------------- 1 file changed, 27 insertions(+), 137 deletions(-) diff --git a/Gaze/Services/System/FullscreenDetectionService.swift b/Gaze/Services/System/FullscreenDetectionService.swift index 9ba4481..96c2708 100644 --- a/Gaze/Services/System/FullscreenDetectionService.swift +++ b/Gaze/Services/System/FullscreenDetectionService.swift @@ -7,139 +7,45 @@ import AppKit import Combine -import CoreGraphics import Foundation - -public struct FullscreenWindowDescriptor: Equatable { - public let ownerPID: pid_t - public let layer: Int - public let bounds: CGRect - - public init(ownerPID: pid_t, layer: Int, bounds: CGRect) { - self.ownerPID = ownerPID - self.layer = layer - self.bounds = bounds - } -} - -protocol FullscreenEnvironmentProviding { - func frontmostProcessIdentifier() -> pid_t? - func windowDescriptors() -> [FullscreenWindowDescriptor] - func screenFrames() -> [CGRect] -} - -struct SystemFullscreenEnvironmentProvider: FullscreenEnvironmentProviding { - func frontmostProcessIdentifier() -> pid_t? { - NSWorkspace.shared.frontmostApplication?.processIdentifier - } - - func windowDescriptors() -> [FullscreenWindowDescriptor] { - let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements] - guard let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else { - return [] - } - - return windowList.compactMap { window in - guard let ownerPID = window[kCGWindowOwnerPID as String] as? pid_t, - let layer = window[kCGWindowLayer as String] as? Int, - let boundsDict = window[kCGWindowBounds as String] as? [String: CGFloat] else { - return nil - } - - let bounds = CGRect( - x: boundsDict["X"] ?? 0, - y: boundsDict["Y"] ?? 0, - width: boundsDict["Width"] ?? 0, - height: boundsDict["Height"] ?? 0 - ) - - return FullscreenWindowDescriptor(ownerPID: ownerPID, layer: layer, bounds: bounds) - } - } - - public func screenFrames() -> [CGRect] { - NSScreen.screens.map(\.frame) - } -} +import MacroVisionKit final class FullscreenDetectionService: ObservableObject { @Published private(set) var isFullscreenActive = false - private var observers: [NSObjectProtocol] = [] - private var frontmostAppObserver: AnyCancellable? + private var fullscreenTask: Task? private let permissionManager: ScreenCapturePermissionManaging - private let environmentProvider: FullscreenEnvironmentProviding - private let windowMatcher = FullscreenWindowMatcher() + #if canImport(MacroVisionKit) + private let monitor = FullScreenMonitor.shared + #endif init( - permissionManager: ScreenCapturePermissionManaging, - environmentProvider: FullscreenEnvironmentProviding + permissionManager: ScreenCapturePermissionManaging ) { self.permissionManager = permissionManager - self.environmentProvider = environmentProvider - setupObservers() + startMonitoring() } - + /// Convenience initializer using default services convenience init() { self.init( - permissionManager: ScreenCapturePermissionManager.shared, - environmentProvider: SystemFullscreenEnvironmentProvider() + permissionManager: ScreenCapturePermissionManager.shared ) } - + // Factory method to safely create instances from non-main actor contexts static func create( - permissionManager: ScreenCapturePermissionManaging? = nil, - environmentProvider: FullscreenEnvironmentProviding? = nil + permissionManager: ScreenCapturePermissionManaging? = nil ) async -> FullscreenDetectionService { await MainActor.run { return FullscreenDetectionService( - permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared, - environmentProvider: environmentProvider ?? SystemFullscreenEnvironmentProvider() + permissionManager: permissionManager ?? ScreenCapturePermissionManager.shared ) } } deinit { - let notificationCenter = NSWorkspace.shared.notificationCenter - observers.forEach { notificationCenter.removeObserver($0) } - frontmostAppObserver?.cancel() - } - - private func setupObservers() { - let workspace = NSWorkspace.shared - let notificationCenter = workspace.notificationCenter - - let stateChangeHandler: (Notification) -> Void = { [weak self] _ in - self?.checkFullscreenState() - } - - let notifications: [(NSNotification.Name, Any?)] = [ - (NSWorkspace.activeSpaceDidChangeNotification, workspace), - (NSApplication.didChangeScreenParametersNotification, nil), - (NSWindow.willEnterFullScreenNotification, nil), - (NSWindow.willExitFullScreenNotification, nil), - ] - - observers = notifications.map { notification, object in - notificationCenter.addObserver( - forName: notification, - object: object, - queue: .main, - using: stateChangeHandler - ) - } - - frontmostAppObserver = NotificationCenter.default.publisher( - for: NSWorkspace.didActivateApplicationNotification, - object: workspace - ) - .sink { [weak self] _ in - self?.checkFullscreenState() - } - - checkFullscreenState() + fullscreenTask?.cancel() } private func canReadWindowInfo() -> Bool { @@ -151,25 +57,17 @@ final class FullscreenDetectionService: ObservableObject { return true } - private func checkFullscreenState() { - guard canReadWindowInfo() else { return } - - guard let frontmostPID = environmentProvider.frontmostProcessIdentifier() else { - setFullscreenState(false) - return - } - - let windows = environmentProvider.windowDescriptors() - let screens = environmentProvider.screenFrames() - - for window in windows where window.ownerPID == frontmostPID && window.layer == 0 { - if windowMatcher.isFullscreen(windowBounds: window.bounds, screenFrames: screens) { - setFullscreenState(true) - return + private func startMonitoring() { + fullscreenTask = Task { [weak self] in + guard let self else { return } + let stream = await monitor.spaceChanges() + for await spaces in stream { + guard self.canReadWindowInfo() else { continue } + self.setFullscreenState(!spaces.isEmpty) } } - setFullscreenState(false) + forceUpdate() } fileprivate func setFullscreenState(_ isActive: Bool) { @@ -179,7 +77,12 @@ final class FullscreenDetectionService: ObservableObject { } func forceUpdate() { - checkFullscreenState() + Task { [weak self] in + guard let self else { return } + guard self.canReadWindowInfo() else { return } + let spaces = await monitor.detectFullscreenApps() + self.setFullscreenState(!spaces.isEmpty) + } } #if DEBUG @@ -188,16 +91,3 @@ final class FullscreenDetectionService: ObservableObject { } #endif } - -struct FullscreenWindowMatcher { - func isFullscreen(windowBounds: CGRect, screenFrames: [CGRect], tolerance: CGFloat = 1) -> Bool { - screenFrames.contains { matches(windowBounds, screenFrame: $0, tolerance: tolerance) } - } - - private func matches(_ windowBounds: CGRect, screenFrame: CGRect, tolerance: CGFloat) -> Bool { - abs(windowBounds.width - screenFrame.width) < tolerance - && abs(windowBounds.height - screenFrame.height) < tolerance - && abs(windowBounds.origin.x - screenFrame.origin.x) < tolerance - && abs(windowBounds.origin.y - screenFrame.origin.y) < tolerance - } -}