import SwiftUI import SaplingCore import SaplingWorkspace import SaplingGit import SaplingStorage import SaplingLogging import SaplingEditor import SaplingRenderer import SaplingUI @main struct SaplingApplication: App { @StateObject private var model = SaplingAppModel(dependencies: .live()) var body: some Scene { WindowGroup { MainWindow(model: model) } #if os(macOS) .windowStyle(.titleBar) .windowToolbarStyle(.unified) #endif #if os(macOS) Settings { SettingsView( configuration: $model.configuration, onSave: model.save(configuration:) ) } #endif } } @MainActor private final class SaplingAppModel: ObservableObject { @Published var workspace: Workspace @Published var selectedProject: Project? @Published var selectedDocument: MarkdownDocument? @Published var editorViewModel: HybridMarkdownEditorViewModel? @Published var gitStatuses: [GitFileStatus] = [] @Published var configuration: SaplingConfiguration private let gitProvider: any GitProvider private let configurationStore: any ConfigurationStore private let workspaceManager: any WorkspaceManaging private let logger: SaplingLogger init(dependencies: AppDependencies) { self.gitProvider = dependencies.gitProvider self.configurationStore = dependencies.configurationStore self.workspaceManager = dependencies.workspaceManager self.logger = dependencies.logger self.configuration = (try? dependencies.configurationStore.loadConfiguration()) ?? SaplingConfiguration() let workspace = workspaceManager.sampleWorkspace() self.workspace = workspace self.selectedProject = workspace.firstProject setSelectedDocument(SaplingSampleData.document) logger.info("Sapling application model initialized", category: .app) Task { await refreshGitStatus() } } func select(file: WorkspaceFile) { guard file.kind == .markdown else { return } if file.url == SaplingSampleData.document.url { setSelectedDocument(SaplingSampleData.document) } else { setSelectedDocument(MarkdownDocument( url: file.url, title: file.name, content: "# \(file.name)\n\nTODO: Load document contents from disk." )) } } func select(project: Project) { selectedProject = project logger.info("Selected project: \(project.name)", category: .workspace) Task { await refreshGitStatus() } } func save(configuration: SaplingConfiguration) { do { try configurationStore.saveConfiguration(configuration) logger.debug("Saved configuration", category: .storage) } catch { logger.error("Failed to save configuration: \(error)", category: .storage) } } private func refreshGitStatus() async { guard let selectedProject else { gitStatuses = [] return } do { let repository = gitProvider.repository(at: selectedProject.repositoryURL) gitStatuses = try await repository.status() logger.debug("Loaded \(gitStatuses.count) Git status entries", category: .git) } catch { gitStatuses = [ GitFileStatus(path: "Unable to load status: \(error)", state: .conflicted) ] logger.error("Failed to refresh Git status: \(error)", category: .git) } } private func setSelectedDocument(_ document: MarkdownDocument) { selectedDocument = document editorViewModel = HybridMarkdownEditorViewModel(document: document) logger.info("Selected document: \(document.title)", category: .editor) } } private struct MainWindow: View { @ObservedObject var model: SaplingAppModel var body: some View { NavigationSplitView { WorkspaceTreeView( workspace: model.workspace, onSelectFile: model.select(file:), onSelectProject: model.select(project:) ) .frame(minWidth: 220) } content: { if let editorViewModel = model.editorViewModel { HybridMarkdownEditor( viewModel: editorViewModel, renderer: MarkdownRenderer() ) .navigationTitle(editorViewModel.document.title) } else { EmptyEditorPlaceholder() } } detail: { SaplingInspectorView( project: model.selectedProject, document: model.selectedDocument, statuses: model.gitStatuses ) .frame(minWidth: 260) } } } private extension Workspace { var firstProject: Project? { for item in items { if let project = item.firstProject { return project } } return nil } } private extension WorkspaceItem { var firstProject: Project? { switch self { case .project(let project): return project case .folder(let folder): for child in folder.children { if let project = child.firstProject { return project } } return nil case .file, .subproject: return nil } } }