Something in the way

This commit is contained in:
Hariharan Mudaliar 2026-01-13 14:35:23 +05:30
commit 9bd445986d
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
19 changed files with 2395 additions and 1 deletions

View file

@ -0,0 +1,53 @@
import Foundation
import Defaults
import AtollExtensionKit
import SwiftUI
struct ExtensionLiveActivityPayload: Identifiable, Hashable {
let bundleIdentifier: String
let descriptor: AtollLiveActivityDescriptor
let receivedAt: Date
var id: String { descriptor.id }
var priority: AtollLiveActivityPriority { descriptor.priority }
}
struct ExtensionLockScreenWidgetPayload: Identifiable, Hashable {
let bundleIdentifier: String
let descriptor: AtollLockScreenWidgetDescriptor
let receivedAt: Date
var id: String { descriptor.id }
var priority: AtollLiveActivityPriority { descriptor.priority }
}
enum ExtensionValidationError: LocalizedError, Equatable {
case featureDisabled
case unauthorized
case invalidDescriptor(String)
case rateLimited
case exceedsCapacity
case duplicateIdentifier
case unsupportedContent
var errorDescription: String? {
switch self {
case .featureDisabled:
return "Extensions feature is currently disabled in Atoll."
case .unauthorized:
return "Your app is not authorized to post this content."
case .invalidDescriptor(let reason):
return "Descriptor validation failed: \(reason)."
case .rateLimited:
return "Too many requests in a short period. Please slow down."
case .exceedsCapacity:
return "Atoll reached its limit for simultaneous extension content."
case .duplicateIdentifier:
return "An item with the same identifier already exists."
case .unsupportedContent:
return "Descriptor includes unsupported content for this surface."
}
}
}

View file

@ -0,0 +1,202 @@
import Foundation
import Defaults
import AtollExtensionKit
@MainActor
final class ExtensionAuthorizationManager: ObservableObject {
static let shared = ExtensionAuthorizationManager()
@Published private(set) var entries: [ExtensionAuthorizationEntry]
@Published private(set) var rateLimitRecords: [ExtensionRateLimitRecord]
private let persistenceQueue = DispatchQueue(label: "com.atoll.extensions.authorization", qos: .utility)
private init() {
self.entries = Defaults[.extensionAuthorizationEntries]
self.rateLimitRecords = Defaults[.extensionRateLimitRecords]
normalizeState()
}
// MARK: - Public API
var isExtensionsFeatureEnabled: Bool { Defaults[.enableThirdPartyExtensions] }
var areLiveActivitiesEnabled: Bool { Defaults[.enableExtensionLiveActivities] }
var areLockScreenWidgetsEnabled: Bool { Defaults[.enableExtensionLockScreenWidgets] }
func updateFeatureToggles(extensionsEnabled: Bool? = nil,
liveActivitiesEnabled: Bool? = nil,
lockScreenWidgetsEnabled: Bool? = nil) {
if let extensionsEnabled { Defaults[.enableThirdPartyExtensions] = extensionsEnabled }
if let liveActivitiesEnabled { Defaults[.enableExtensionLiveActivities] = liveActivitiesEnabled }
if let lockScreenWidgetsEnabled { Defaults[.enableExtensionLockScreenWidgets] = lockScreenWidgetsEnabled }
objectWillChange.send()
}
func authorizationEntry(for bundleIdentifier: String) -> ExtensionAuthorizationEntry? {
entries.first { $0.bundleIdentifier == bundleIdentifier }
}
func ensureEntryExists(bundleIdentifier: String, appName: String?) -> ExtensionAuthorizationEntry {
if let existing = authorizationEntry(for: bundleIdentifier) {
return existing
}
let resolvedName = appName ?? bundleIdentifier
let entry = ExtensionAuthorizationEntry(
bundleIdentifier: bundleIdentifier,
appName: resolvedName,
status: .pending,
allowedScopes: defaultScopes()
)
entries.append(entry)
persistEntries()
return entry
}
func authorize(bundleIdentifier: String, appName: String?, scopes: Set<ExtensionPermissionScope>? = nil) {
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
entry.appName = appName ?? entry.appName
entry.status = .authorized
entry.allowedScopes = scopes ?? entry.allowedScopes
entry.grantedAt = .now
entry.lastDeniedReason = nil
}
}
func deny(bundleIdentifier: String, reason: String?) {
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
entry.status = .denied
entry.lastDeniedReason = reason
}
}
func revoke(bundleIdentifier: String, reason: String?) {
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
entry.status = .revoked
entry.lastDeniedReason = reason
}
}
func recordActivity(for bundleIdentifier: String, scope: ExtensionPermissionScope) {
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
entry.lastActivityAt = .now
}
updateRateLimitRecord(bundleIdentifier: bundleIdentifier) { record in
let now = Date()
switch scope {
case .liveActivities:
record.activityTimestamps.append(now)
case .lockScreenWidgets:
record.widgetTimestamps.append(now)
}
record.activityTimestamps = flushOldTimestamps(record.activityTimestamps)
record.widgetTimestamps = flushOldTimestamps(record.widgetTimestamps)
}
}
func updateAllowedScopes(bundleIdentifier: String, allowedScopes: Set<ExtensionPermissionScope>) {
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
entry.allowedScopes = allowedScopes
}
}
func resetRateLimits(for bundleIdentifier: String) {
rateLimitRecords.removeAll { $0.bundleIdentifier == bundleIdentifier }
persistRateLimitRecords()
}
func removeEntry(bundleIdentifier: String) {
entries.removeAll { $0.bundleIdentifier == bundleIdentifier }
rateLimitRecords.removeAll { $0.bundleIdentifier == bundleIdentifier }
persistEntries()
persistRateLimitRecords()
}
// MARK: - Validation Helpers
func canProcessLiveActivityRequest(from bundleIdentifier: String) -> Bool {
guard preflight(bundleIdentifier: bundleIdentifier, scope: .liveActivities) else { return false }
guard areLiveActivitiesEnabled else { return false }
return true
}
func canProcessLockScreenRequest(from bundleIdentifier: String) -> Bool {
guard preflight(bundleIdentifier: bundleIdentifier, scope: .lockScreenWidgets) else { return false }
guard areLockScreenWidgetsEnabled else { return false }
return true
}
func recordDeniedRequest(bundleIdentifier: String, reason: String) {
updateEntry(bundleIdentifier: bundleIdentifier) { entry in
entry.lastDeniedReason = reason
entry.status = entry.status == .authorized ? .revoked : entry.status
}
}
// MARK: - Internal State Updates
private func updateEntry(bundleIdentifier: String, mutate: (inout ExtensionAuthorizationEntry) -> Void) {
guard let index = entries.firstIndex(where: { $0.bundleIdentifier == bundleIdentifier }) else {
var entry = ensureEntryExists(bundleIdentifier: bundleIdentifier, appName: bundleIdentifier)
mutate(&entry)
entries.removeAll { $0.bundleIdentifier == bundleIdentifier }
entries.append(entry)
persistEntries()
return
}
var entry = entries[index]
mutate(&entry)
entries[index] = entry
persistEntries()
}
private func updateRateLimitRecord(bundleIdentifier: String, mutate: (inout ExtensionRateLimitRecord) -> Void) {
if let index = rateLimitRecords.firstIndex(where: { $0.bundleIdentifier == bundleIdentifier }) {
var record = rateLimitRecords[index]
mutate(&record)
rateLimitRecords[index] = record
persistRateLimitRecords()
} else {
var newRecord = ExtensionRateLimitRecord(bundleIdentifier: bundleIdentifier)
mutate(&newRecord)
rateLimitRecords.append(newRecord)
persistRateLimitRecords()
}
}
private func preflight(bundleIdentifier: String, scope: ExtensionPermissionScope) -> Bool {
guard isExtensionsFeatureEnabled else { return false }
guard let entry = authorizationEntry(for: bundleIdentifier) else { return false }
guard entry.isAuthorized else { return false }
guard entry.allowedScopes.contains(scope) else { return false }
return true
}
private func flushOldTimestamps(_ timestamps: [Date]) -> [Date] {
let threshold = Date().addingTimeInterval(-60 * 5)
return timestamps.filter { $0 >= threshold }
}
private func defaultScopes() -> Set<ExtensionPermissionScope> {
Set(ExtensionPermissionScope.allCases)
}
private func persistEntries() {
let entriesSnapshot = entries
persistenceQueue.async {
Defaults[.extensionAuthorizationEntries] = entriesSnapshot
}
}
private func persistRateLimitRecords() {
let recordsSnapshot = rateLimitRecords
persistenceQueue.async {
Defaults[.extensionRateLimitRecords] = recordsSnapshot
}
}
private func normalizeState() {
entries = entries.filter { !$0.bundleIdentifier.isEmpty }
rateLimitRecords = rateLimitRecords.filter { !$0.bundleIdentifier.isEmpty }
}
}

View file

@ -0,0 +1,33 @@
import Foundation
import AtollExtensionKit
struct ExtensionDescriptorValidator {
static func validate(_ descriptor: AtollLiveActivityDescriptor) throws {
guard descriptor.isValid else {
throw ExtensionValidationError.invalidDescriptor("Missing mandatory fields")
}
guard descriptor.leadingIcon.isValid else {
throw ExtensionValidationError.invalidDescriptor("Invalid leading icon data")
}
if let badge = descriptor.badgeIcon {
guard badge.isValid else {
throw ExtensionValidationError.invalidDescriptor("Invalid badge icon data")
}
}
if descriptor.metadata.count > 32 {
throw ExtensionValidationError.invalidDescriptor("Metadata keys must be ≤ 32")
}
}
static func validate(_ descriptor: AtollLockScreenWidgetDescriptor) throws {
guard descriptor.isValid else {
throw ExtensionValidationError.invalidDescriptor("Missing mandatory fields")
}
guard descriptor.content.count <= 12 else {
throw ExtensionValidationError.invalidDescriptor("Too many content elements")
}
guard descriptor.size.width <= 480, descriptor.size.height <= 280 else {
throw ExtensionValidationError.invalidDescriptor("Widget exceeds size limits")
}
}
}

View file

@ -0,0 +1,90 @@
import Foundation
import Defaults
import AtollExtensionKit
@MainActor
final class ExtensionLiveActivityManager: ObservableObject {
static let shared = ExtensionLiveActivityManager()
@Published private(set) var activeActivities: [ExtensionLiveActivityPayload] = []
private let authorizationManager = ExtensionAuthorizationManager.shared
private let maxCapacityKey = Defaults.Keys.extensionLiveActivityCapacity
private init() {}
func present(descriptor: AtollLiveActivityDescriptor, bundleIdentifier: String) throws {
guard authorizationManager.canProcessLiveActivityRequest(from: bundleIdentifier) else {
throw ExtensionValidationError.unauthorized
}
guard descriptor.isValid else {
throw ExtensionValidationError.invalidDescriptor("Structure validation failed")
}
guard activeActivities.count < Defaults[maxCapacityKey] else {
throw ExtensionValidationError.exceedsCapacity
}
guard activeActivities.contains(where: { $0.descriptor.id == descriptor.id }) == false else {
throw ExtensionValidationError.duplicateIdentifier
}
let payload = ExtensionLiveActivityPayload(
bundleIdentifier: bundleIdentifier,
descriptor: descriptor,
receivedAt: .now
)
activeActivities.append(payload)
sortActivities()
authorizationManager.recordActivity(for: bundleIdentifier, scope: .liveActivities)
}
func update(descriptor: AtollLiveActivityDescriptor, bundleIdentifier: String) throws {
guard descriptor.isValid else {
throw ExtensionValidationError.invalidDescriptor("Structure validation failed")
}
guard let index = activeActivities.firstIndex(where: { $0.descriptor.id == descriptor.id && $0.bundleIdentifier == bundleIdentifier }) else {
throw ExtensionValidationError.invalidDescriptor("Missing existing activity")
}
let payload = ExtensionLiveActivityPayload(
bundleIdentifier: bundleIdentifier,
descriptor: descriptor,
receivedAt: activeActivities[index].receivedAt
)
activeActivities[index] = payload
sortActivities()
authorizationManager.recordActivity(for: bundleIdentifier, scope: .liveActivities)
}
func dismiss(activityID: String, bundleIdentifier: String) {
let previousCount = activeActivities.count
activeActivities.removeAll { $0.descriptor.id == activityID && $0.bundleIdentifier == bundleIdentifier }
if previousCount != activeActivities.count {
Logger.log("Dismissed extension live activity \(activityID) from \(bundleIdentifier)", category: .extensions)
ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: activityID)
}
}
func dismissAll(for bundleIdentifier: String) {
let ids = activeActivities
.filter { $0.bundleIdentifier == bundleIdentifier }
.map { $0.descriptor.id }
activeActivities.removeAll { $0.bundleIdentifier == bundleIdentifier }
ids.forEach { ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: $0) }
}
func sortedActivities(for coexistence: Bool = false) -> [ExtensionLiveActivityPayload] {
activeActivities
.filter { coexistence ? $0.descriptor.allowsMusicCoexistence : true }
.sorted(by: descriptorComparator)
}
private func descriptorComparator(lhs: ExtensionLiveActivityPayload, rhs: ExtensionLiveActivityPayload) -> Bool {
if lhs.descriptor.priority == rhs.descriptor.priority {
return lhs.receivedAt < rhs.receivedAt
}
return lhs.descriptor.priority > rhs.descriptor.priority
}
private func sortActivities() {
activeActivities.sort(by: descriptorComparator)
}
}

View file

@ -0,0 +1,87 @@
import Foundation
import Defaults
import AtollExtensionKit
@MainActor
final class ExtensionLockScreenWidgetManager: ObservableObject {
static let shared = ExtensionLockScreenWidgetManager()
@Published private(set) var activeWidgets: [ExtensionLockScreenWidgetPayload] = []
private let authorizationManager = ExtensionAuthorizationManager.shared
private let maxCapacityKey = Defaults.Keys.extensionLockScreenWidgetCapacity
private var presentationController: ExtensionLockScreenWidgetPresentationController!
private init() {
presentationController = ExtensionLockScreenWidgetPresentationController(manager: self)
presentationController.activate()
}
func present(descriptor: AtollLockScreenWidgetDescriptor, bundleIdentifier: String) throws {
guard authorizationManager.canProcessLockScreenRequest(from: bundleIdentifier) else {
throw ExtensionValidationError.unauthorized
}
guard descriptor.isValid else {
throw ExtensionValidationError.invalidDescriptor("Structure validation failed")
}
guard activeWidgets.count < Defaults[maxCapacityKey] else {
throw ExtensionValidationError.exceedsCapacity
}
guard activeWidgets.contains(where: { $0.descriptor.id == descriptor.id }) == false else {
throw ExtensionValidationError.duplicateIdentifier
}
let payload = ExtensionLockScreenWidgetPayload(
bundleIdentifier: bundleIdentifier,
descriptor: descriptor,
receivedAt: .now
)
activeWidgets.append(payload)
sortWidgets()
authorizationManager.recordActivity(for: bundleIdentifier, scope: .lockScreenWidgets)
}
func update(descriptor: AtollLockScreenWidgetDescriptor, bundleIdentifier: String) throws {
guard descriptor.isValid else {
throw ExtensionValidationError.invalidDescriptor("Structure validation failed")
}
guard let index = activeWidgets.firstIndex(where: { $0.descriptor.id == descriptor.id && $0.bundleIdentifier == bundleIdentifier }) else {
throw ExtensionValidationError.invalidDescriptor("Missing existing widget")
}
let payload = ExtensionLockScreenWidgetPayload(
bundleIdentifier: bundleIdentifier,
descriptor: descriptor,
receivedAt: activeWidgets[index].receivedAt
)
activeWidgets[index] = payload
sortWidgets()
authorizationManager.recordActivity(for: bundleIdentifier, scope: .lockScreenWidgets)
}
func dismiss(widgetID: String, bundleIdentifier: String) {
let previousCount = activeWidgets.count
activeWidgets.removeAll { $0.descriptor.id == widgetID && $0.bundleIdentifier == bundleIdentifier }
if previousCount != activeWidgets.count {
Logger.log("Dismissed extension widget \(widgetID) from \(bundleIdentifier)", category: .extensions)
ExtensionXPCServiceHost.shared.notifyWidgetDismiss(bundleIdentifier: bundleIdentifier, widgetID: widgetID)
}
}
func dismissAll(for bundleIdentifier: String) {
let ids = activeWidgets
.filter { $0.bundleIdentifier == bundleIdentifier }
.map { $0.descriptor.id }
activeWidgets.removeAll { $0.bundleIdentifier == bundleIdentifier }
ids.forEach { ExtensionXPCServiceHost.shared.notifyWidgetDismiss(bundleIdentifier: bundleIdentifier, widgetID: $0) }
}
private func sortWidgets() {
activeWidgets.sort { lhs, rhs in
if lhs.priority == rhs.priority {
return lhs.receivedAt < rhs.receivedAt
}
return lhs.priority > rhs.priority
}
}
}

View file

@ -0,0 +1,248 @@
import SwiftUI
import Combine
import Defaults
import AppKit
import SkyLightWindow
import AtollExtensionKit
@MainActor
final class ExtensionLockScreenWidgetPresentationController {
private unowned let manager: ExtensionLockScreenWidgetManager
private var cancellables: Set<AnyCancellable> = []
private let windowPool = ExtensionLockScreenWidgetWindowPool()
private var cachedPayloads: [ExtensionLockScreenWidgetPayload] = []
private var isLocked: Bool = false
init(manager: ExtensionLockScreenWidgetManager) {
self.manager = manager
}
func activate() {
observeLockState()
observePayloads()
observeDefaults()
}
private func observeLockState() {
LockScreenManager.shared.$isLocked
.removeDuplicates()
.receive(on: RunLoop.main)
.sink { [weak self] locked in
guard let self else { return }
self.isLocked = locked
self.refreshPresentation()
}
.store(in: &cancellables)
}
private func observePayloads() {
manager.$activeWidgets
.receive(on: RunLoop.main)
.sink { [weak self] payloads in
guard let self else { return }
self.cachedPayloads = payloads
self.refreshPresentation()
}
.store(in: &cancellables)
}
private func observeDefaults() {
Defaults.publisher(.enableExtensionLockScreenWidgets, options: [])
.receive(on: RunLoop.main)
.sink { [weak self] change in
guard let self else { return }
if change.newValue {
self.refreshPresentation()
} else {
self.windowPool.hideAll()
}
}
.store(in: &cancellables)
}
private func refreshPresentation() {
guard isLocked, LockScreenManager.shared.currentLockStatus, Defaults[.enableExtensionLockScreenWidgets] else {
windowPool.hideAll()
return
}
guard cachedPayloads.isEmpty == false else {
windowPool.hideAll()
return
}
windowPool.sync(with: cachedPayloads)
}
}
@MainActor
private final class ExtensionLockScreenWidgetWindowPool {
private struct WindowRecord {
let window: NSWindow
}
private var windows: [String: WindowRecord] = [:]
private var payloadsByID: [String: ExtensionLockScreenWidgetPayload] = [:]
private var screenChangeObserver: NSObjectProtocol?
private var workspaceObservers: [NSObjectProtocol] = []
init() {
registerScreenChangeObservers()
}
deinit {
if let screenChangeObserver {
NotificationCenter.default.removeObserver(screenChangeObserver)
}
let workspaceCenter = NSWorkspace.shared.notificationCenter
workspaceObservers.forEach { workspaceCenter.removeObserver($0) }
}
func sync(with payloads: [ExtensionLockScreenWidgetPayload]) {
payloadsByID = Dictionary(uniqueKeysWithValues: payloads.map { ($0.id, $0) })
let activeIDs = Set(payloadsByID.keys)
let staleIDs = Set(windows.keys).subtracting(activeIDs)
staleIDs.forEach { hideWindow(withID: $0) }
payloads.forEach { renderWindow(for: $0) }
}
func hideAll() {
payloadsByID.removeAll()
windows.values.forEach { record in
record.window.orderOut(nil)
record.window.contentView = nil
}
windows.removeAll()
}
private func renderWindow(for payload: ExtensionLockScreenWidgetPayload) {
guard let screen = currentScreen() else { return }
let descriptor = payload.descriptor
let window = ensureWindow(for: payload.id)
let hosting = NSHostingView(rootView: ExtensionLockScreenWidgetView(payload: payload))
hosting.frame = NSRect(origin: .zero, size: descriptor.size)
hosting.wantsLayer = true
hosting.layer?.masksToBounds = true
hosting.layer?.cornerRadius = descriptor.cornerRadius
window.contentView = hosting
window.setFrame(frame(for: descriptor, on: screen), display: true)
window.alphaValue = 1
window.orderFrontRegardless()
}
private func ensureWindow(for id: String) -> NSWindow {
if let record = windows[id] {
return record.window
}
let window = NSWindow(
contentRect: NSRect(origin: .zero, size: CGSize(width: 200, height: 80)),
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
window.isReleasedWhenClosed = false
window.isOpaque = false
window.backgroundColor = .clear
window.hasShadow = false
window.level = NSWindow.Level(rawValue: Int(CGShieldingWindowLevel()))
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
window.ignoresMouseEvents = true
window.isMovable = false
ScreenCaptureVisibilityManager.shared.register(window, scope: .entireInterface)
SkyLightOperator.shared.delegateWindow(window)
let record = WindowRecord(window: window)
windows[id] = record
return window
}
private func hideWindow(withID id: String) {
guard let record = windows[id] else { return }
record.window.orderOut(nil)
record.window.contentView = nil
windows.removeValue(forKey: id)
}
private func registerScreenChangeObservers() {
screenChangeObserver = NotificationCenter.default.addObserver(
forName: NSApplication.didChangeScreenParametersNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleScreenGeometryChange(reason: "screen-parameters")
}
let workspaceCenter = NSWorkspace.shared.notificationCenter
let wakeObserver = workspaceCenter.addObserver(
forName: NSWorkspace.screensDidWakeNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleScreenGeometryChange(reason: "screens-did-wake")
}
workspaceObservers = [wakeObserver]
}
private func handleScreenGeometryChange(reason: String) {
guard windows.isEmpty == false else { return }
repositionAll(animated: false)
print("ExtensionLockScreenWidgetWindowPool: realigned windows due to \(reason)")
}
private func repositionAll(animated: Bool) {
guard let screen = currentScreen() else { return }
for (id, record) in windows {
guard let payload = payloadsByID[id] else { continue }
let targetFrame = frame(for: payload.descriptor, on: screen)
if animated {
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.22
context.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
record.window.animator().setFrame(targetFrame, display: true)
}
} else {
record.window.setFrame(targetFrame, display: true)
}
}
}
private func frame(for descriptor: AtollLockScreenWidgetDescriptor, on screen: NSScreen) -> NSRect {
let size = descriptor.size
let safeInsets = NSEdgeInsets(top: 140, left: 48, bottom: 100, right: 48)
let screenFrame = screen.frame
let minX = screenFrame.minX + safeInsets.left
let maxX = screenFrame.maxX - safeInsets.right - size.width
let minY = screenFrame.minY + safeInsets.bottom
let maxY = screenFrame.maxY - safeInsets.top - size.height
var originX: CGFloat
switch descriptor.position.alignment {
case .leading:
originX = minX
case .center:
originX = screenFrame.midX - (size.width / 2)
case .trailing:
originX = maxX
}
originX += descriptor.position.horizontalOffset
originX = clamp(originX, lower: min(minX, maxX), upper: max(minX, maxX))
var originY = screenFrame.midY - (size.height / 2) + descriptor.position.verticalOffset
originY = clamp(originY, lower: min(minY, maxY), upper: max(minY, maxY))
return NSRect(x: originX, y: originY, width: size.width, height: size.height)
}
private func clamp(_ value: CGFloat, lower: CGFloat, upper: CGFloat) -> CGFloat {
guard lower <= upper else { return value }
return min(max(value, lower), upper)
}
private func currentScreen() -> NSScreen? {
LockScreenDisplayContextProvider.shared.contextSnapshot()?.screen ?? NSScreen.main
}
}