import AppKit
|
import CoreGraphics
|
import Darwin
|
import Foundation
|
import AlignerCore
|
|
final class SkyLightSpaceEnumerator: SpaceEnumeratorProtocol {
|
enum SkyLightError: Error, CustomStringConvertible {
|
case frameworkUnavailable
|
case symbolUnavailable(String)
|
case managedDisplaySpacesUnavailable
|
|
var description: String {
|
switch self {
|
case .frameworkUnavailable:
|
return "SkyLight framework could not be opened."
|
case .symbolUnavailable(let name):
|
return "SkyLight symbol unavailable: \(name)."
|
case .managedDisplaySpacesUnavailable:
|
return "CGSCopyManagedDisplaySpaces returned no usable data."
|
}
|
}
|
}
|
|
private typealias CGSMainConnectionIDFunction = @convention(c) () -> UInt32
|
private typealias CGSCopyManagedDisplaySpacesFunction = @convention(c) (UInt32) -> Unmanaged<CFArray>?
|
private typealias CGSCopySpacesForWindowsFunction = @convention(c) (UInt32, UInt32, CFArray) -> Unmanaged<CFArray>?
|
|
private let handle: UnsafeMutableRawPointer?
|
private let mainConnectionID: CGSMainConnectionIDFunction
|
private let copyManagedDisplaySpaces: CGSCopyManagedDisplaySpacesFunction
|
private let copySpacesForWindows: CGSCopySpacesForWindowsFunction?
|
|
init() throws {
|
guard let handle = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_LAZY) else {
|
throw SkyLightError.frameworkUnavailable
|
}
|
|
self.handle = handle
|
self.mainConnectionID = try Self.symbol("CGSMainConnectionID", in: handle)
|
self.copyManagedDisplaySpaces = try Self.symbol("CGSCopyManagedDisplaySpaces", in: handle)
|
self.copySpacesForWindows = try? Self.symbol("CGSCopySpacesForWindows", in: handle)
|
}
|
|
deinit {
|
if let handle {
|
dlclose(handle)
|
}
|
}
|
|
func displays() throws -> [AlignerDisplay] {
|
let displayRecords = try managedDisplaySpaceRecords()
|
|
let physicalDisplayUUIDs = Self.physicalDisplayUUIDs()
|
let records = displayRecords.flatMap { displayRecord -> [SpaceEnumerationRecord] in
|
let displayUUID = stringValue(displayRecord["Display Identifier"])
|
let spaceRecords = displayRecord["Spaces"] as? [[String: Any]] ?? []
|
|
return spaceRecords.compactMap { spaceRecord in
|
guard let id = uint64Value(spaceRecord["id64"] ?? spaceRecord["id"]) else {
|
return nil
|
}
|
|
return SpaceEnumerationRecord(
|
id: id,
|
cgsType: intValue(spaceRecord["type"]) ?? -1,
|
displayUUID: displayUUID
|
)
|
}
|
}
|
|
return SpaceEnumerationPolicy.normalizedDisplays(
|
from: records,
|
physicalDisplayUUIDs: physicalDisplayUUIDs
|
)
|
}
|
|
func currentOverlayScreenSpaceStatesByDisplayUUID() throws -> [String: OverlayScreenSpaceState] {
|
let displayRecords = try managedDisplaySpaceRecords()
|
var result: [String: OverlayScreenSpaceState] = [:]
|
|
for displayRecord in displayRecords {
|
let displayUUID = stringValue(displayRecord["Display Identifier"])
|
guard !displayUUID.isEmpty,
|
let currentSpace = displayRecord["Current Space"] as? [String: Any]
|
else {
|
continue
|
}
|
|
let isFullscreen = SpaceEnumerationPolicy.spaceType(fromCGSType: intValue(currentSpace["type"]) ?? -1) == .fullscreen
|
result[displayUUID] = OverlayScreenSpaceState(
|
isFullscreenSpace: isFullscreen,
|
isSplitViewSpace: Self.isSplitViewSpace(currentSpace)
|
)
|
}
|
|
return result
|
}
|
|
func currentSpaceIDs() throws -> Set<UInt64> {
|
let displayRecords = try managedDisplaySpaceRecords()
|
var result = Set<UInt64>()
|
|
for displayRecord in displayRecords {
|
guard let currentSpace = displayRecord["Current Space"] as? [String: Any],
|
let id = uint64Value(currentSpace["id64"] ?? currentSpace["id"])
|
else {
|
continue
|
}
|
|
result.insert(id)
|
}
|
|
return result
|
}
|
|
func spaceIDsByWindowID(windowIDs: [UInt32]) -> [UInt32: [UInt64]] {
|
guard let copySpacesForWindows else { return [:] }
|
|
let connectionID = mainConnectionID()
|
var result: [UInt32: [UInt64]] = [:]
|
|
for windowID in windowIDs {
|
let windowIDArray = [NSNumber(value: windowID)] as CFArray
|
guard let unmanagedSpaces = copySpacesForWindows(connectionID, 0x7, windowIDArray) else {
|
continue
|
}
|
|
let spaces = (unmanagedSpaces.takeRetainedValue() as? [Any] ?? [])
|
.compactMap { item -> UInt64? in
|
uint64Value(item)
|
}
|
|
if !spaces.isEmpty {
|
result[windowID] = spaces
|
}
|
}
|
|
return result
|
}
|
|
private static func symbol<T>(_ name: String, in handle: UnsafeMutableRawPointer) throws -> T {
|
guard let rawSymbol = dlsym(handle, name) else {
|
throw SkyLightError.symbolUnavailable(name)
|
}
|
|
return unsafeBitCast(rawSymbol, to: T.self)
|
}
|
|
private func managedDisplaySpaceRecords() throws -> [[String: Any]] {
|
let connectionID = mainConnectionID()
|
guard
|
let unmanagedDisplays = copyManagedDisplaySpaces(connectionID),
|
let displayRecords = unmanagedDisplays.takeRetainedValue() as? [[String: Any]]
|
else {
|
throw SkyLightError.managedDisplaySpacesUnavailable
|
}
|
|
return displayRecords
|
}
|
|
private static func isSplitViewSpace(_ spaceRecord: [String: Any]) -> Bool {
|
guard let tileLayoutManager = spaceRecord["TileLayoutManager"] as? [String: Any] else {
|
return false
|
}
|
|
if let tileSpaces = tileLayoutManager["TileSpaces"] as? [Any] {
|
return tileSpaces.count > 1
|
}
|
|
return true
|
}
|
|
private static func physicalDisplayUUIDs() -> Set<String> {
|
Set(
|
NSScreen.screens.compactMap { screen in
|
guard
|
let number = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber,
|
let unmanagedUUID = CGDisplayCreateUUIDFromDisplayID(CGDirectDisplayID(number.uint32Value))
|
else {
|
return nil
|
}
|
|
let uuid = unmanagedUUID.takeRetainedValue()
|
return CFUUIDCreateString(nil, uuid) as String?
|
}
|
)
|
}
|
}
|
|
private func stringValue(_ value: Any?) -> String {
|
value as? String ?? ""
|
}
|
|
private func intValue(_ value: Any?) -> Int? {
|
switch value {
|
case let number as NSNumber:
|
return number.intValue
|
case let int as Int:
|
return int
|
default:
|
return nil
|
}
|
}
|
|
private func uint64Value(_ value: Any?) -> UInt64? {
|
switch value {
|
case let number as NSNumber:
|
return number.uint64Value
|
case let uint64 as UInt64:
|
return uint64
|
case let uint as UInt:
|
return UInt64(uint)
|
case let int as Int where int >= 0:
|
return UInt64(int)
|
default:
|
return nil
|
}
|
}
|