Yep. AI rewrote the whole thing.
This commit is contained in:
268
Downterm/CommandNotch/Models/ScreenRegistry.swift
Normal file
268
Downterm/CommandNotch/Models/ScreenRegistry.swift
Normal file
@@ -0,0 +1,268 @@
|
||||
import AppKit
|
||||
import Combine
|
||||
import SwiftUI
|
||||
|
||||
struct ConnectedScreenSummary: Identifiable, Equatable {
|
||||
let id: ScreenID
|
||||
let displayName: String
|
||||
let isActive: Bool
|
||||
let assignedWorkspaceID: WorkspaceID
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class ScreenRegistry: ObservableObject {
|
||||
static let shared = ScreenRegistry(assignmentStore: UserDefaultsScreenAssignmentStore())
|
||||
|
||||
@Published private(set) var screenContexts: [ScreenContext] = []
|
||||
|
||||
private let workspaceRegistry: WorkspaceRegistry
|
||||
private let settingsController: AppSettingsController
|
||||
private let assignmentStore: any ScreenAssignmentStoreType
|
||||
private let connectedScreenIDsProvider: @MainActor () -> [ScreenID]
|
||||
private let activeScreenIDProvider: @MainActor () -> ScreenID?
|
||||
private let screenLookup: @MainActor (ScreenID) -> NSScreen?
|
||||
|
||||
private var contextsByID: [ScreenID: ScreenContext] = [:]
|
||||
private var preferredAssignments: [ScreenID: WorkspaceID]
|
||||
private var workspacePresenters: [WorkspaceID: ScreenID] = [:]
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(
|
||||
workspaceRegistry: WorkspaceRegistry? = nil,
|
||||
settingsController: AppSettingsController? = nil,
|
||||
assignmentStore: (any ScreenAssignmentStoreType)? = nil,
|
||||
initialAssignments: [ScreenID: WorkspaceID]? = nil,
|
||||
connectedScreenIDsProvider: @escaping @MainActor () -> [ScreenID] = {
|
||||
NSScreen.screens.map(\.displayUUID)
|
||||
},
|
||||
activeScreenIDProvider: @escaping @MainActor () -> ScreenID? = {
|
||||
let mouseLocation = NSEvent.mouseLocation
|
||||
return NSScreen.screens.first { NSMouseInRect(mouseLocation, $0.frame, false) }?.displayUUID
|
||||
?? NSScreen.main?.displayUUID
|
||||
},
|
||||
screenLookup: @escaping @MainActor (ScreenID) -> NSScreen? = { screenID in
|
||||
NSScreen.screens.first { $0.displayUUID == screenID }
|
||||
}
|
||||
) {
|
||||
let resolvedWorkspaceRegistry = workspaceRegistry ?? WorkspaceRegistry.shared
|
||||
let resolvedSettingsController = settingsController ?? AppSettingsController.shared
|
||||
let resolvedAssignmentStore = assignmentStore ?? UserDefaultsScreenAssignmentStore()
|
||||
|
||||
self.workspaceRegistry = resolvedWorkspaceRegistry
|
||||
self.settingsController = resolvedSettingsController
|
||||
self.assignmentStore = resolvedAssignmentStore
|
||||
self.preferredAssignments = initialAssignments ?? resolvedAssignmentStore.loadScreenAssignments()
|
||||
self.connectedScreenIDsProvider = connectedScreenIDsProvider
|
||||
self.activeScreenIDProvider = activeScreenIDProvider
|
||||
self.screenLookup = screenLookup
|
||||
|
||||
observeWorkspaceChanges()
|
||||
refreshConnectedScreens()
|
||||
}
|
||||
|
||||
func allScreens() -> [ScreenContext] {
|
||||
screenContexts
|
||||
}
|
||||
|
||||
func screenContext(for id: ScreenID) -> ScreenContext? {
|
||||
contextsByID[id]
|
||||
}
|
||||
|
||||
func workspaceController(for screenID: ScreenID) -> WorkspaceController {
|
||||
let workspaceID = contextsByID[screenID]?.workspaceID ?? workspaceRegistry.defaultWorkspaceID
|
||||
return workspaceRegistry.controller(for: workspaceID) ?? workspaceRegistry.defaultWorkspaceController
|
||||
}
|
||||
|
||||
func assignedScreenIDs(to workspaceID: WorkspaceID) -> [ScreenID] {
|
||||
preferredAssignments
|
||||
.filter { $0.value == workspaceID }
|
||||
.map(\.key)
|
||||
.sorted()
|
||||
}
|
||||
|
||||
func assignedScreenCount(to workspaceID: WorkspaceID) -> Int {
|
||||
assignedScreenIDs(to: workspaceID).count
|
||||
}
|
||||
|
||||
func connectedScreenSummaries() -> [ConnectedScreenSummary] {
|
||||
let activeScreenID = activeScreenID()
|
||||
|
||||
return screenContexts.enumerated().map { index, context in
|
||||
ConnectedScreenSummary(
|
||||
id: context.id,
|
||||
displayName: resolvedDisplayName(for: context.id, fallbackIndex: index),
|
||||
isActive: context.id == activeScreenID,
|
||||
assignedWorkspaceID: context.workspaceID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func assignWorkspace(_ workspaceID: WorkspaceID, to screenID: ScreenID) {
|
||||
guard workspaceRegistry.controller(for: workspaceID) != nil else { return }
|
||||
|
||||
let previousWorkspaceID = contextsByID[screenID]?.workspaceID ?? preferredAssignments[screenID]
|
||||
preferredAssignments[screenID] = workspaceID
|
||||
contextsByID[screenID]?.updateWorkspace(id: workspaceID)
|
||||
|
||||
if let previousWorkspaceID,
|
||||
previousWorkspaceID != workspaceID,
|
||||
workspacePresenters[previousWorkspaceID] == screenID {
|
||||
workspacePresenters.removeValue(forKey: previousWorkspaceID)
|
||||
}
|
||||
|
||||
persistAssignments()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func assignActiveScreen(to workspaceID: WorkspaceID) -> ScreenID? {
|
||||
guard let screenID = activeScreenID() else { return nil }
|
||||
assignWorkspace(workspaceID, to: screenID)
|
||||
return screenID
|
||||
}
|
||||
|
||||
func presentingScreenID(for workspaceID: WorkspaceID) -> ScreenID? {
|
||||
guard let screenID = workspacePresenters[workspaceID] else { return nil }
|
||||
guard preferredAssignments[screenID] == workspaceID else {
|
||||
workspacePresenters.removeValue(forKey: workspaceID)
|
||||
return nil
|
||||
}
|
||||
return screenID
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func claimWorkspacePresentation(for screenID: ScreenID) -> ScreenID? {
|
||||
guard let workspaceID = contextsByID[screenID]?.workspaceID ?? preferredAssignments[screenID] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let previousPresenter = workspacePresenters[workspaceID]
|
||||
workspacePresenters[workspaceID] = screenID
|
||||
return previousPresenter == screenID ? nil : previousPresenter
|
||||
}
|
||||
|
||||
func releaseWorkspacePresentation(for screenID: ScreenID) {
|
||||
workspacePresenters = workspacePresenters.filter { $0.value != screenID }
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func deleteWorkspace(
|
||||
_ workspaceID: WorkspaceID,
|
||||
preferredFallback preferredFallbackID: WorkspaceID? = nil
|
||||
) -> WorkspaceID? {
|
||||
guard workspaceRegistry.canDeleteWorkspace(id: workspaceID) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let fallbackWorkspaceID = workspaceRegistry.deletionFallbackWorkspaceID(
|
||||
forDeleting: workspaceID,
|
||||
preferredFallback: preferredFallbackID
|
||||
) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
workspacePresenters.removeValue(forKey: workspaceID)
|
||||
|
||||
for (screenID, assignedWorkspaceID) in preferredAssignments where assignedWorkspaceID == workspaceID {
|
||||
preferredAssignments[screenID] = fallbackWorkspaceID
|
||||
}
|
||||
|
||||
for context in contextsByID.values where context.workspaceID == workspaceID {
|
||||
context.updateWorkspace(id: fallbackWorkspaceID)
|
||||
}
|
||||
|
||||
guard workspaceRegistry.deleteWorkspace(id: workspaceID) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
persistAssignments()
|
||||
return fallbackWorkspaceID
|
||||
}
|
||||
|
||||
func activeScreenID() -> ScreenID? {
|
||||
activeScreenIDProvider() ?? screenContexts.first?.id
|
||||
}
|
||||
|
||||
func refreshConnectedScreens() {
|
||||
let connectedScreenIDs = connectedScreenIDsProvider()
|
||||
let validWorkspaceIDs = Set(workspaceRegistry.allWorkspaceSummaries().map(\.id))
|
||||
let defaultWorkspaceID = workspaceRegistry.defaultWorkspaceID
|
||||
var nextContextsByID: [ScreenID: ScreenContext] = [:]
|
||||
var nextContexts: [ScreenContext] = []
|
||||
|
||||
for screenID in connectedScreenIDs {
|
||||
let workspaceID = resolvedWorkspaceID(
|
||||
for: screenID,
|
||||
validWorkspaceIDs: validWorkspaceIDs,
|
||||
defaultWorkspaceID: defaultWorkspaceID
|
||||
)
|
||||
|
||||
let context = contextsByID[screenID] ?? ScreenContext(
|
||||
id: screenID,
|
||||
workspaceID: workspaceID,
|
||||
settingsController: settingsController,
|
||||
screenProvider: screenLookup
|
||||
)
|
||||
|
||||
context.updateWorkspace(id: workspaceID)
|
||||
context.refreshClosedSize()
|
||||
|
||||
nextContextsByID[screenID] = context
|
||||
nextContexts.append(context)
|
||||
}
|
||||
|
||||
contextsByID = nextContextsByID
|
||||
screenContexts = nextContexts
|
||||
reconcileWorkspacePresenters()
|
||||
persistAssignments()
|
||||
}
|
||||
|
||||
private func resolvedWorkspaceID(
|
||||
for screenID: ScreenID,
|
||||
validWorkspaceIDs: Set<WorkspaceID>,
|
||||
defaultWorkspaceID: WorkspaceID
|
||||
) -> WorkspaceID {
|
||||
guard let preferredWorkspaceID = preferredAssignments[screenID],
|
||||
validWorkspaceIDs.contains(preferredWorkspaceID) else {
|
||||
preferredAssignments[screenID] = defaultWorkspaceID
|
||||
return defaultWorkspaceID
|
||||
}
|
||||
|
||||
return preferredWorkspaceID
|
||||
}
|
||||
|
||||
private func observeWorkspaceChanges() {
|
||||
workspaceRegistry.$workspaceSummaries
|
||||
.dropFirst()
|
||||
.sink { [weak self] _ in
|
||||
Task { @MainActor [weak self] in
|
||||
self?.refreshConnectedScreens()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
private func persistAssignments() {
|
||||
assignmentStore.saveScreenAssignments(preferredAssignments)
|
||||
}
|
||||
|
||||
private func reconcileWorkspacePresenters() {
|
||||
let validScreenIDs = Set(contextsByID.keys)
|
||||
let validAssignments = preferredAssignments
|
||||
|
||||
workspacePresenters = workspacePresenters.filter { workspaceID, screenID in
|
||||
validScreenIDs.contains(screenID) && validAssignments[screenID] == workspaceID
|
||||
}
|
||||
}
|
||||
|
||||
private func resolvedDisplayName(for screenID: ScreenID, fallbackIndex: Int) -> String {
|
||||
let fallbackName = "Screen \(fallbackIndex + 1)"
|
||||
guard let screen = screenLookup(screenID) else {
|
||||
return fallbackName
|
||||
}
|
||||
|
||||
let localizedName = screen.localizedName.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return localizedName.isEmpty ? fallbackName : localizedName
|
||||
}
|
||||
}
|
||||
|
||||
extension ScreenRegistry: ScreenRegistryType {}
|
||||
Reference in New Issue
Block a user