Files

160 lines
5.5 KiB
Swift

import Combine
import Foundation
struct PluginEditorSession: Identifiable {
let id = UUID()
let recordID: UUID?
let managedProgram: String?
let isEditing: Bool
let initialNames: [String]
let initialPath: String
let initialArguments: [String]
var initialMode: PluginTransportMode = .tcpOnly
}
struct PluginManagementRow: Identifiable {
let id: String
let recordID: UUID?
let managedProgram: String?
let names: [String]
let overriddenNames: [String]
let path: String?
let availability: PluginCatalogSnapshot.Availability?
let securityProgram: String?
let overridesManaged: Bool
}
@MainActor
final class PluginManagementModel: ObservableObject {
let catalog: PluginCatalog
@Published private(set) var snapshot: PluginCatalogSnapshot
private var subscription: AnyCancellable?
init(catalog: PluginCatalog) {
self.catalog = catalog
snapshot = catalog.catalogSnapshot()
subscription = catalog.didCommit.sink { [weak self] _ in
MainActor.assumeIsolated {
guard let self else { return }
self.snapshot = self.catalog.catalogSnapshot()
}
}
}
var rows: [PluginManagementRow] {
let managed = ManagedPluginCatalog.plugins.map { info in
let effective = snapshot.managedRecords[info.program]
return PluginManagementRow(
id: "managed:" + info.program, recordID: nil, managedProgram: info.program,
names: info.names,
overriddenNames: info.names.filter { snapshot.entry(for: $0)?.source == .user },
path: effective?.path, availability: effective?.availability, securityProgram: nil,
overridesManaged: false)
}
let user = catalog.userRecords.map { record in
let entry = record.names.first.flatMap { snapshot.entry(for: $0) }
return PluginManagementRow(
id: record.id.uuidString, recordID: record.id, managedProgram: nil,
names: record.names, overriddenNames: [], path: record.path,
availability: entry?.availability, securityProgram: record.names.first,
overridesManaged: record.names.contains { ManagedPluginCatalog.info(forProgram: $0) != nil }
)
}
return managed + user
}
func beginAdding() -> PluginEditorSession {
PluginEditorSession(
recordID: UUID(), managedProgram: nil, isEditing: false,
initialNames: [""], initialPath: "", initialArguments: [])
}
func beginEditing(_ id: UUID) -> PluginEditorSession? {
guard !snapshot.mappingsUnreadable,
let record = catalog.userRecords.first(where: { $0.id == id })
else { return nil }
return PluginEditorSession(
recordID: id, managedProgram: nil, isEditing: true,
initialNames: record.names, initialPath: record.path, initialArguments: record.arguments,
initialMode: record.mode)
}
func beginOverriding(_ program: String) -> PluginEditorSession? {
guard !snapshot.mappingsUnreadable, snapshot.entry(for: program)?.source == .managed else {
return nil
}
return PluginEditorSession(
recordID: UUID(), managedProgram: nil, isEditing: false,
initialNames: [program], initialPath: "", initialArguments: [])
}
func beginManagedSettings(_ program: String) -> PluginEditorSession? {
guard !snapshot.mappingsUnreadable,
let info = ManagedPluginCatalog.plugins.first(where: { $0.program == program })
else { return nil }
return PluginEditorSession(
recordID: nil, managedProgram: program, isEditing: true,
initialNames: info.names, initialPath: "",
initialArguments: snapshot.managedArguments[program] ?? [],
initialMode: snapshot.managedRecords[program]?.mode ?? .tcpOnly)
}
func namesIssue(_ names: [String], session: PluginEditorSession) -> PluginMappingError? {
let names = names.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
if names.isEmpty || names.contains(where: { $0.isEmpty || $0.contains("\0") }) {
return .invalidName
}
if Set(names).count != names.count { return .nameExists }
let occupied = Set(catalog.userRecords.filter { $0.id != session.recordID }.flatMap(\.names))
return names.contains(where: { occupied.contains($0) }) ? .nameExists : nil
}
func pathIssue(_ path: String) -> PluginMappingError? {
path.hasPrefix("/") && !path.contains("\0") ? nil : .invalidPath
}
func save(
_ session: PluginEditorSession, names: [String], path: String, arguments: [String] = [],
mode: PluginTransportMode = .tcpOnly
)
throws
{
if let program = session.managedProgram {
try catalog.commit([
.managedArguments(program: program, arguments: arguments),
.managedMode(program: program, mode: mode),
])
} else {
if let issue = namesIssue(names, session: session) { throw issue }
if let issue = pathIssue(path) { throw issue }
guard let id = session.recordID,
!session.isEditing || catalog.userRecords.contains(where: { $0.id == id })
else { throw PluginMappingError.nameMissing }
try catalog.commit([
.save(UserPluginRecord(id: id, names: names, path: path, arguments: arguments, mode: mode))
])
}
refresh()
}
func remove(_ id: UUID) throws {
try catalog.commit([.remove(id)])
refresh()
}
func clearManagedArguments(_ program: String) throws {
try catalog.commit([.managedArguments(program: program, arguments: [])])
refresh()
}
func retryReading() throws {
try catalog.retryReadingMappings()
refresh()
}
func refresh() {
snapshot = catalog.catalogSnapshot()
catalog.refreshSecurityFacts()
}
}