Sapling/Sources/SaplingApp/SaplingApp.swift

184 lines
5.5 KiB
Swift
Raw Normal View History

2026-05-29 15:19:33 +02:00
import SwiftUI
import SaplingCore
import SaplingWorkspace
import SaplingGit
import SaplingStorage
2026-05-29 15:34:15 +02:00
import SaplingLogging
2026-05-29 15:19:33 +02:00
import SaplingEditor
import SaplingRenderer
import SaplingUI
@main
struct SaplingApplication: App {
2026-05-29 15:34:15 +02:00
@StateObject private var model = SaplingAppModel(dependencies: .live())
2026-05-29 15:19:33 +02:00
var body: some Scene {
WindowGroup {
MainWindow(model: model)
}
#if os(macOS)
.windowStyle(.titleBar)
.windowToolbarStyle(.unified)
#endif
2026-05-29 15:34:15 +02:00
#if os(macOS)
Settings {
SettingsView(
configuration: $model.configuration,
onSave: model.save(configuration:)
)
}
#endif
2026-05-29 15:19:33 +02:00
}
}
@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] = []
2026-05-29 15:34:15 +02:00
@Published var configuration: SaplingConfiguration
2026-05-29 15:19:33 +02:00
private let gitProvider: any GitProvider
2026-05-29 15:34:15 +02:00
private let configurationStore: any ConfigurationStore
2026-05-29 15:19:33 +02:00
private let workspaceManager: any WorkspaceManaging
2026-05-29 15:34:15 +02:00
private let logger: SaplingLogger
2026-05-29 15:19:33 +02:00
2026-05-29 15:34:15 +02:00
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()
2026-05-29 15:19:33 +02:00
let workspace = workspaceManager.sampleWorkspace()
self.workspace = workspace
self.selectedProject = workspace.firstProject
setSelectedDocument(SaplingSampleData.document)
2026-05-29 15:34:15 +02:00
logger.info("Sapling application model initialized", category: .app)
2026-05-29 15:19:33 +02:00
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
2026-05-29 15:34:15 +02:00
logger.info("Selected project: \(project.name)", category: .workspace)
2026-05-29 15:19:33 +02:00
Task {
await refreshGitStatus()
}
}
2026-05-29 15:34:15 +02:00
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)
}
}
2026-05-29 15:19:33 +02:00
private func refreshGitStatus() async {
guard let selectedProject else {
gitStatuses = []
return
}
do {
let repository = gitProvider.repository(at: selectedProject.repositoryURL)
gitStatuses = try await repository.status()
2026-05-29 15:34:15 +02:00
logger.debug("Loaded \(gitStatuses.count) Git status entries", category: .git)
2026-05-29 15:19:33 +02:00
} catch {
gitStatuses = [
GitFileStatus(path: "Unable to load status: \(error)", state: .conflicted)
]
2026-05-29 15:34:15 +02:00
logger.error("Failed to refresh Git status: \(error)", category: .git)
2026-05-29 15:19:33 +02:00
}
}
private func setSelectedDocument(_ document: MarkdownDocument) {
selectedDocument = document
editorViewModel = HybridMarkdownEditorViewModel(document: document)
2026-05-29 15:34:15 +02:00
logger.info("Selected document: \(document.title)", category: .editor)
2026-05-29 15:19:33 +02:00
}
}
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
}
}
}