Files
downterm/CommandNotch/CommandNotch/Models/WorkspaceRegistry.swift
2026-03-13 21:26:06 +11:00

182 lines
6.2 KiB
Swift

import SwiftUI
@MainActor
final class WorkspaceRegistry: ObservableObject {
static let shared = WorkspaceRegistry(store: UserDefaultsWorkspaceStore())
@Published private(set) var workspaceSummaries: [WorkspaceSummary]
private let store: any WorkspaceStoreType
private var controllers: [WorkspaceID: WorkspaceController] = [:]
private let controllerFactory: @MainActor (WorkspaceSummary) -> WorkspaceController
init(
initialWorkspaces: [WorkspaceSummary]? = nil,
store: (any WorkspaceStoreType)? = nil,
controllerFactory: @escaping @MainActor (WorkspaceSummary) -> WorkspaceController = { summary in
WorkspaceController(summary: summary)
}
) {
let resolvedStore = store ?? UserDefaultsWorkspaceStore()
let resolvedWorkspaces = initialWorkspaces ?? resolvedStore.loadWorkspaceSummaries()
self.store = resolvedStore
self.controllerFactory = controllerFactory
self.workspaceSummaries = resolvedWorkspaces
for summary in resolvedWorkspaces {
controllers[summary.id] = controllerFactory(summary)
}
_ = ensureWorkspaceExists()
}
var defaultWorkspaceID: WorkspaceID {
ensureWorkspaceExists()
}
var defaultWorkspaceController: WorkspaceController {
let workspaceID = ensureWorkspaceExists()
guard let controller = controllers[workspaceID] else {
let summary = WorkspaceSummary(id: workspaceID, name: "Main")
let controller = controllerFactory(summary)
controllers[workspaceID] = controller
return controller
}
return controller
}
func allWorkspaceSummaries() -> [WorkspaceSummary] {
workspaceSummaries
}
func summary(for id: WorkspaceID) -> WorkspaceSummary? {
workspaceSummaries.first { $0.id == id }
}
func controller(for id: WorkspaceID) -> WorkspaceController? {
controllers[id]
}
func canDeleteWorkspace(id: WorkspaceID) -> Bool {
workspaceSummaries.count > 1 && workspaceSummaries.contains { $0.id == id }
}
func deletionFallbackWorkspaceID(
forDeleting id: WorkspaceID,
preferredFallback preferredFallbackID: WorkspaceID? = nil
) -> WorkspaceID? {
let candidates = workspaceSummaries.filter { $0.id != id }
if let preferredFallbackID,
candidates.contains(where: { $0.id == preferredFallbackID }) {
return preferredFallbackID
}
return candidates.first?.id
}
@discardableResult
func ensureWorkspaceExists() -> WorkspaceID {
if let existing = workspaceSummaries.first {
return existing.id
}
return createWorkspace(named: "Main")
}
@discardableResult
func createWorkspace(named name: String? = nil) -> WorkspaceID {
let workspaceName = resolvedWorkspaceName(from: name)
let summary = WorkspaceSummary(name: workspaceName)
workspaceSummaries.append(summary)
controllers[summary.id] = controllerFactory(summary)
persistWorkspaceSummaries()
return summary.id
}
func renameWorkspace(id: WorkspaceID, to name: String) {
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else { return }
workspaceSummaries[index].name = trimmed
controllers[id]?.rename(to: trimmed)
persistWorkspaceSummaries()
}
func updateWorkspaceHotkey(id: WorkspaceID, to hotkey: HotkeyBinding?) {
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else { return }
guard workspaceSummaries[index].hotkey != hotkey else { return }
workspaceSummaries[index].hotkey = hotkey
controllers[id]?.updateHotkey(hotkey)
persistWorkspaceSummaries()
}
func nextWorkspaceID(after id: WorkspaceID) -> WorkspaceID? {
guard !workspaceSummaries.isEmpty else { return nil }
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else {
return workspaceSummaries.first?.id
}
let nextIndex = workspaceSummaries.index(after: index)
return workspaceSummaries[nextIndex == workspaceSummaries.endIndex ? workspaceSummaries.startIndex : nextIndex].id
}
func previousWorkspaceID(before id: WorkspaceID) -> WorkspaceID? {
guard !workspaceSummaries.isEmpty else { return nil }
guard let index = workspaceSummaries.firstIndex(where: { $0.id == id }) else {
return workspaceSummaries.last?.id
}
let previousIndex = index == workspaceSummaries.startIndex
? workspaceSummaries.index(before: workspaceSummaries.endIndex)
: workspaceSummaries.index(before: index)
return workspaceSummaries[previousIndex].id
}
@discardableResult
func deleteWorkspace(id: WorkspaceID) -> Bool {
guard canDeleteWorkspace(id: id) else { return false }
workspaceSummaries.removeAll { $0.id == id }
controllers.removeValue(forKey: id)
_ = ensureWorkspaceExists()
persistWorkspaceSummaries()
return true
}
func updateAllWorkspacesFontSizes(_ size: CGFloat) {
for controller in controllers.values {
controller.updateAllFontSizes(size)
}
}
func updateAllWorkspacesThemes(_ theme: TerminalTheme) {
for controller in controllers.values {
controller.updateAllThemes(theme)
}
}
private func resolvedWorkspaceName(from proposedName: String?) -> String {
let trimmed = proposedName?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !trimmed.isEmpty {
return trimmed
}
let existing = Set(workspaceSummaries.map(\.name))
if !existing.contains("Main") {
return "Main"
}
var index = 2
while existing.contains("Workspace \(index)") {
index += 1
}
return "Workspace \(index)"
}
private func persistWorkspaceSummaries() {
store.saveWorkspaceSummaries(workspaceSummaries)
}
}