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() } @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) } }