fix fullscreen

This commit is contained in:
Michael Freno
2026-01-30 12:55:41 -05:00
parent cbd60fdd08
commit 4b446db817

View File

@@ -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<Void, Never>?
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
}
}