150 lines
5.3 KiB
Swift
150 lines
5.3 KiB
Swift
import CoreGraphics
|
|
import Foundation
|
|
import AppKit
|
|
|
|
@_silgen_name("MTConfigureDisplayEnabled")
|
|
private func MTConfigureDisplayEnabled(
|
|
_ config: CGDisplayConfigRef,
|
|
_ display: CGDirectDisplayID,
|
|
_ enabled: Bool
|
|
) -> CGError
|
|
|
|
@_silgen_name("MTDisplayEnableSPIAvailable")
|
|
private func MTDisplayEnableSPIAvailable() -> Bool
|
|
|
|
@MainActor
|
|
protocol DisplayDisableServicing: AnyObject {
|
|
var isSupported: Bool { get }
|
|
|
|
func listDisplays() -> [DisplayDisableDisplay]
|
|
func setDisplay(_ displayID: CGDirectDisplayID, enabled: Bool) throws
|
|
}
|
|
|
|
@MainActor
|
|
protocol DisplayDisableStateStoring: AnyObject {
|
|
var snapshot: DisplayDisableRecoverySnapshot? { get set }
|
|
}
|
|
|
|
enum DisplayDisableServiceError: Error, LocalizedError {
|
|
case privateSPIUnavailable
|
|
case beginConfigurationFailed(CGError)
|
|
case configureDisplayFailed(CGError)
|
|
case completeConfigurationFailed(CGError)
|
|
|
|
var errorDescription: String? {
|
|
switch self {
|
|
case .privateSPIUnavailable:
|
|
return DisplayBrightnessLocalization.string(
|
|
"displayDisable.unsupported",
|
|
defaultValue: "当前系统不支持关闭内建显示屏"
|
|
)
|
|
case .beginConfigurationFailed:
|
|
return DisplayBrightnessLocalization.string(
|
|
"displayDisable.error.beginConfiguration",
|
|
defaultValue: "无法开始显示器配置"
|
|
)
|
|
case .configureDisplayFailed:
|
|
return DisplayBrightnessLocalization.string(
|
|
"displayDisable.error.configureDisplay",
|
|
defaultValue: "无法切换显示器状态"
|
|
)
|
|
case .completeConfigurationFailed:
|
|
return DisplayBrightnessLocalization.string(
|
|
"displayDisable.error.completeConfiguration",
|
|
defaultValue: "无法提交显示器配置"
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class SystemDisplayDisableService: DisplayDisableServicing {
|
|
var isSupported: Bool {
|
|
MTDisplayEnableSPIAvailable()
|
|
}
|
|
|
|
func listDisplays() -> [DisplayDisableDisplay] {
|
|
let activeDisplayIDs = Set(Self.activeDisplayIDs())
|
|
let visibleDisplayIDs = Set(Self.visibleAppKitDisplayIDs())
|
|
|
|
return Self.onlineDisplayIDs().enumerated().map { index, displayID in
|
|
let screen = NSScreen.screens.first(where: { screen in
|
|
Self.displayID(for: screen) == displayID
|
|
})
|
|
let vendorNumber = CGDisplayVendorNumber(displayID)
|
|
let modelNumber = CGDisplayModelNumber(displayID)
|
|
let serialNumber = CGDisplaySerialNumber(displayID)
|
|
|
|
return DisplayDisableDisplay(
|
|
id: displayID,
|
|
name: screen?.localizedName ?? "Display \(index + 1)",
|
|
isBuiltin: CGDisplayIsBuiltin(displayID) != 0,
|
|
isActive: activeDisplayIDs.contains(displayID),
|
|
isInMirrorSet: CGDisplayIsInMirrorSet(displayID) != 0,
|
|
isVisibleToAppKit: visibleDisplayIDs.contains(displayID),
|
|
vendorNumber: vendorNumber == 0 ? nil : vendorNumber,
|
|
modelNumber: modelNumber == 0 ? nil : modelNumber,
|
|
serialNumber: serialNumber == 0 ? nil : serialNumber
|
|
)
|
|
}
|
|
}
|
|
|
|
func setDisplay(_ displayID: CGDirectDisplayID, enabled: Bool) throws {
|
|
guard isSupported else {
|
|
throw DisplayDisableServiceError.privateSPIUnavailable
|
|
}
|
|
|
|
var config: CGDisplayConfigRef?
|
|
let beginError = CGBeginDisplayConfiguration(&config)
|
|
guard beginError == .success, let config else {
|
|
throw DisplayDisableServiceError.beginConfigurationFailed(beginError)
|
|
}
|
|
|
|
var committed = false
|
|
defer {
|
|
if !committed {
|
|
CGCancelDisplayConfiguration(config)
|
|
}
|
|
}
|
|
|
|
let configureError = MTConfigureDisplayEnabled(config, displayID, enabled)
|
|
guard configureError == .success else {
|
|
throw DisplayDisableServiceError.configureDisplayFailed(configureError)
|
|
}
|
|
|
|
let completeError = CGCompleteDisplayConfiguration(config, .forSession)
|
|
committed = completeError == .success
|
|
guard completeError == .success else {
|
|
throw DisplayDisableServiceError.completeConfigurationFailed(completeError)
|
|
}
|
|
}
|
|
|
|
private static func onlineDisplayIDs() -> [CGDirectDisplayID] {
|
|
var count: UInt32 = 0
|
|
CGGetOnlineDisplayList(0, nil, &count)
|
|
|
|
var displayIDs = Array(repeating: CGDirectDisplayID(), count: Int(count))
|
|
CGGetOnlineDisplayList(count, &displayIDs, &count)
|
|
return Array(displayIDs.prefix(Int(count)))
|
|
}
|
|
|
|
private static func activeDisplayIDs() -> [CGDirectDisplayID] {
|
|
var count: UInt32 = 0
|
|
CGGetActiveDisplayList(0, nil, &count)
|
|
|
|
var displayIDs = Array(repeating: CGDirectDisplayID(), count: Int(count))
|
|
CGGetActiveDisplayList(count, &displayIDs, &count)
|
|
return Array(displayIDs.prefix(Int(count)))
|
|
}
|
|
|
|
private static func visibleAppKitDisplayIDs() -> [CGDirectDisplayID] {
|
|
NSScreen.screens.compactMap(displayID(for:))
|
|
}
|
|
|
|
private static func displayID(for screen: NSScreen) -> CGDirectDisplayID? {
|
|
(
|
|
screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber
|
|
)?.uint32Value
|
|
}
|
|
}
|