Milestone 0 completed.
This commit is contained in:
parent
24ed7f2655
commit
5b57df0214
12 changed files with 290 additions and 11 deletions
|
|
@ -11,6 +11,7 @@ Sapling is organized as a Swift Package with an executable app target and focuse
|
||||||
- `SaplingEditor`: Hybrid Markdown editor state and SwiftUI editing surface.
|
- `SaplingEditor`: Hybrid Markdown editor state and SwiftUI editing surface.
|
||||||
- `SaplingRenderer`: Markdown parsing/rendering primitives for headings, emphasis, code blocks, task lists, and images.
|
- `SaplingRenderer`: Markdown parsing/rendering primitives for headings, emphasis, code blocks, task lists, and images.
|
||||||
- `SaplingStorage`: Configuration and workspace metadata persistence contracts.
|
- `SaplingStorage`: Configuration and workspace metadata persistence contracts.
|
||||||
|
- `SaplingLogging`: Thin logging facade over `OSLog`.
|
||||||
- `SaplingUI`: Shared SwiftUI sidebar, inspector, and empty-state components.
|
- `SaplingUI`: Shared SwiftUI sidebar, inspector, and empty-state components.
|
||||||
|
|
||||||
## Architectural Decisions
|
## Architectural Decisions
|
||||||
|
|
@ -23,6 +24,12 @@ The editor follows an MVVM shape. `HybridMarkdownEditorViewModel` owns document
|
||||||
|
|
||||||
Storage is abstracted behind small protocols. The current in-memory stores make the application buildable and testable; `JSONWorkspaceMetadataStore` establishes the first production path for workspace metadata without forcing a database decision.
|
Storage is abstracted behind small protocols. The current in-memory stores make the application buildable and testable; `JSONWorkspaceMetadataStore` establishes the first production path for workspace metadata without forcing a database decision.
|
||||||
|
|
||||||
|
Dependency injection starts in `SaplingApp` through `AppDependencies`. The composition root owns concrete providers and passes protocols into application state. This keeps feature modules testable and prevents UI code from constructing infrastructure ad hoc.
|
||||||
|
|
||||||
|
Settings are represented by `SaplingConfiguration`, persisted by `ConfigurationStore`, and exposed through the SwiftUI `Settings` scene. Logging flows through `SaplingLogger` so feature code uses stable categories without depending directly on logger construction details.
|
||||||
|
|
||||||
|
The development workflow is captured in `Makefile` and `Scripts/`. `make validate` runs formatting, linting, build, and tests; this is the Milestone 0 gate before updating roadmap status.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- TODO: Replace sample data composition with real workspace opening and file loading.
|
- TODO: Replace sample data composition with real workspace opening and file loading.
|
||||||
|
|
|
||||||
15
Makefile
Normal file
15
Makefile
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
.PHONY: build test format lint validate
|
||||||
|
|
||||||
|
build:
|
||||||
|
swift build
|
||||||
|
|
||||||
|
test:
|
||||||
|
swift test
|
||||||
|
|
||||||
|
format:
|
||||||
|
sh Scripts/format.sh
|
||||||
|
|
||||||
|
lint:
|
||||||
|
sh Scripts/lint.sh
|
||||||
|
|
||||||
|
validate: format lint build test
|
||||||
|
|
@ -16,6 +16,7 @@ let package = Package(
|
||||||
.library(name: "SaplingEditor", targets: ["SaplingEditor"]),
|
.library(name: "SaplingEditor", targets: ["SaplingEditor"]),
|
||||||
.library(name: "SaplingRenderer", targets: ["SaplingRenderer"]),
|
.library(name: "SaplingRenderer", targets: ["SaplingRenderer"]),
|
||||||
.library(name: "SaplingStorage", targets: ["SaplingStorage"]),
|
.library(name: "SaplingStorage", targets: ["SaplingStorage"]),
|
||||||
|
.library(name: "SaplingLogging", targets: ["SaplingLogging"]),
|
||||||
.library(name: "SaplingUI", targets: ["SaplingUI"])
|
.library(name: "SaplingUI", targets: ["SaplingUI"])
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
|
@ -28,6 +29,7 @@ let package = Package(
|
||||||
"SaplingEditor",
|
"SaplingEditor",
|
||||||
"SaplingRenderer",
|
"SaplingRenderer",
|
||||||
"SaplingStorage",
|
"SaplingStorage",
|
||||||
|
"SaplingLogging",
|
||||||
"SaplingUI"
|
"SaplingUI"
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|
@ -52,9 +54,10 @@ let package = Package(
|
||||||
name: "SaplingStorage",
|
name: "SaplingStorage",
|
||||||
dependencies: ["SaplingCore"]
|
dependencies: ["SaplingCore"]
|
||||||
),
|
),
|
||||||
|
.target(name: "SaplingLogging"),
|
||||||
.target(
|
.target(
|
||||||
name: "SaplingUI",
|
name: "SaplingUI",
|
||||||
dependencies: ["SaplingCore", "SaplingWorkspace", "SaplingGit", "SaplingEditor"]
|
dependencies: ["SaplingCore", "SaplingWorkspace", "SaplingGit", "SaplingEditor", "SaplingStorage"]
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "SaplingCoreTests",
|
name: "SaplingCoreTests",
|
||||||
|
|
|
||||||
8
README
8
README
|
|
@ -189,6 +189,14 @@ Test:
|
||||||
swift test
|
swift test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Validate the Milestone 0 development workflow:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make validate
|
||||||
|
```
|
||||||
|
|
||||||
|
The validation target runs formatting, linting, build, and tests. If `swift-format` is installed, `make format` formats Swift sources in place; otherwise it falls back to the repository lint checks.
|
||||||
|
|
||||||
The initial app shell launches with a sample workspace tree, a hybrid Markdown editor, a Git/project inspector, and a mock Git provider. The architecture is intentionally protocol-first so the macOS implementation can use system Git while future iOS support can use an embedded Git implementation behind the same interfaces.
|
The initial app shell launches with a sample workspace tree, a hybrid Markdown editor, a Git/project inspector, and a mock Git provider. The architecture is intentionally protocol-first so the macOS implementation can use system Git while future iOS support can use an embedded Git implementation behind the same interfaces.
|
||||||
|
|
||||||
See [Docs/Architecture.md](Docs/Architecture.md) for module responsibilities, architectural decisions, and TODO markers.
|
See [Docs/Architecture.md](Docs/Architecture.md) for module responsibilities, architectural decisions, and TODO markers.
|
||||||
|
|
|
||||||
12
Scripts/format.sh
Normal file
12
Scripts/format.sh
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
if command -v swift-format >/dev/null 2>&1; then
|
||||||
|
swift-format --in-place --recursive Package.swift Sources Tests
|
||||||
|
else
|
||||||
|
echo "swift-format not found; checking formatting invariants only."
|
||||||
|
sh Scripts/lint.sh
|
||||||
|
fi
|
||||||
24
Scripts/lint.sh
Normal file
24
Scripts/lint.sh
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
FILES=$(find Package.swift Sources Tests -type f -name '*.swift' | sort)
|
||||||
|
|
||||||
|
if [ -z "$FILES" ]; then
|
||||||
|
echo "No Swift files found."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -n '[[:blank:]]$' $FILES; then
|
||||||
|
echo "Trailing whitespace found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -n "$(printf '\t')" $FILES; then
|
||||||
|
echo "Tabs found; use spaces for Swift source indentation."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Lint passed."
|
||||||
60
Sources/SaplingApp/AppDependencies.swift
Normal file
60
Sources/SaplingApp/AppDependencies.swift
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import Foundation
|
||||||
|
import SaplingGit
|
||||||
|
import SaplingLogging
|
||||||
|
import SaplingStorage
|
||||||
|
import SaplingWorkspace
|
||||||
|
|
||||||
|
struct AppDependencies {
|
||||||
|
let gitProvider: any GitProvider
|
||||||
|
let configurationStore: any ConfigurationStore
|
||||||
|
let metadataStore: any WorkspaceMetadataStore
|
||||||
|
let workspaceManager: any WorkspaceManaging
|
||||||
|
let logger: SaplingLogger
|
||||||
|
|
||||||
|
static func live() -> AppDependencies {
|
||||||
|
let gitProvider = MockGitProvider()
|
||||||
|
let metadataStore = InMemoryWorkspaceMetadataStore()
|
||||||
|
let configurationStore = JSONConfigurationStore(
|
||||||
|
fileURL: supportDirectory().appendingPathComponent("Configuration.json")
|
||||||
|
)
|
||||||
|
let workspaceManager = LocalWorkspaceManager(
|
||||||
|
gitProvider: gitProvider,
|
||||||
|
metadataStore: metadataStore
|
||||||
|
)
|
||||||
|
|
||||||
|
return AppDependencies(
|
||||||
|
gitProvider: gitProvider,
|
||||||
|
configurationStore: configurationStore,
|
||||||
|
metadataStore: metadataStore,
|
||||||
|
workspaceManager: workspaceManager,
|
||||||
|
logger: SaplingLogger()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func preview() -> AppDependencies {
|
||||||
|
let gitProvider = MockGitProvider()
|
||||||
|
let metadataStore = InMemoryWorkspaceMetadataStore()
|
||||||
|
let configurationStore = InMemoryConfigurationStore()
|
||||||
|
let workspaceManager = LocalWorkspaceManager(
|
||||||
|
gitProvider: gitProvider,
|
||||||
|
metadataStore: metadataStore
|
||||||
|
)
|
||||||
|
|
||||||
|
return AppDependencies(
|
||||||
|
gitProvider: gitProvider,
|
||||||
|
configurationStore: configurationStore,
|
||||||
|
metadataStore: metadataStore,
|
||||||
|
workspaceManager: workspaceManager,
|
||||||
|
logger: SaplingLogger(subsystem: "app.sapling.Sapling.preview")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func supportDirectory() -> URL {
|
||||||
|
let baseURL = FileManager.default.urls(
|
||||||
|
for: .applicationSupportDirectory,
|
||||||
|
in: .userDomainMask
|
||||||
|
).first ?? FileManager.default.temporaryDirectory
|
||||||
|
|
||||||
|
return baseURL.appendingPathComponent("Sapling", isDirectory: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,13 +3,14 @@ import SaplingCore
|
||||||
import SaplingWorkspace
|
import SaplingWorkspace
|
||||||
import SaplingGit
|
import SaplingGit
|
||||||
import SaplingStorage
|
import SaplingStorage
|
||||||
|
import SaplingLogging
|
||||||
import SaplingEditor
|
import SaplingEditor
|
||||||
import SaplingRenderer
|
import SaplingRenderer
|
||||||
import SaplingUI
|
import SaplingUI
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct SaplingApplication: App {
|
struct SaplingApplication: App {
|
||||||
@StateObject private var model = SaplingAppModel()
|
@StateObject private var model = SaplingAppModel(dependencies: .live())
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
|
|
@ -19,6 +20,15 @@ struct SaplingApplication: App {
|
||||||
.windowStyle(.titleBar)
|
.windowStyle(.titleBar)
|
||||||
.windowToolbarStyle(.unified)
|
.windowToolbarStyle(.unified)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
Settings {
|
||||||
|
SettingsView(
|
||||||
|
configuration: $model.configuration,
|
||||||
|
onSave: model.save(configuration:)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -29,23 +39,27 @@ private final class SaplingAppModel: ObservableObject {
|
||||||
@Published var selectedDocument: MarkdownDocument?
|
@Published var selectedDocument: MarkdownDocument?
|
||||||
@Published var editorViewModel: HybridMarkdownEditorViewModel?
|
@Published var editorViewModel: HybridMarkdownEditorViewModel?
|
||||||
@Published var gitStatuses: [GitFileStatus] = []
|
@Published var gitStatuses: [GitFileStatus] = []
|
||||||
|
@Published var configuration: SaplingConfiguration
|
||||||
|
|
||||||
private let gitProvider: any GitProvider
|
private let gitProvider: any GitProvider
|
||||||
|
private let configurationStore: any ConfigurationStore
|
||||||
private let workspaceManager: any WorkspaceManaging
|
private let workspaceManager: any WorkspaceManaging
|
||||||
|
private let logger: SaplingLogger
|
||||||
|
|
||||||
init() {
|
init(dependencies: AppDependencies) {
|
||||||
let gitProvider = MockGitProvider()
|
self.gitProvider = dependencies.gitProvider
|
||||||
self.gitProvider = gitProvider
|
self.configurationStore = dependencies.configurationStore
|
||||||
self.workspaceManager = LocalWorkspaceManager(
|
self.workspaceManager = dependencies.workspaceManager
|
||||||
gitProvider: gitProvider,
|
self.logger = dependencies.logger
|
||||||
metadataStore: InMemoryWorkspaceMetadataStore()
|
self.configuration = (try? dependencies.configurationStore.loadConfiguration()) ?? SaplingConfiguration()
|
||||||
)
|
|
||||||
|
|
||||||
let workspace = workspaceManager.sampleWorkspace()
|
let workspace = workspaceManager.sampleWorkspace()
|
||||||
self.workspace = workspace
|
self.workspace = workspace
|
||||||
self.selectedProject = workspace.firstProject
|
self.selectedProject = workspace.firstProject
|
||||||
setSelectedDocument(SaplingSampleData.document)
|
setSelectedDocument(SaplingSampleData.document)
|
||||||
|
|
||||||
|
logger.info("Sapling application model initialized", category: .app)
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
await refreshGitStatus()
|
await refreshGitStatus()
|
||||||
}
|
}
|
||||||
|
|
@ -67,11 +81,21 @@ private final class SaplingAppModel: ObservableObject {
|
||||||
|
|
||||||
func select(project: Project) {
|
func select(project: Project) {
|
||||||
selectedProject = project
|
selectedProject = project
|
||||||
|
logger.info("Selected project: \(project.name)", category: .workspace)
|
||||||
Task {
|
Task {
|
||||||
await refreshGitStatus()
|
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 {
|
private func refreshGitStatus() async {
|
||||||
guard let selectedProject else {
|
guard let selectedProject else {
|
||||||
gitStatuses = []
|
gitStatuses = []
|
||||||
|
|
@ -81,16 +105,19 @@ private final class SaplingAppModel: ObservableObject {
|
||||||
do {
|
do {
|
||||||
let repository = gitProvider.repository(at: selectedProject.repositoryURL)
|
let repository = gitProvider.repository(at: selectedProject.repositoryURL)
|
||||||
gitStatuses = try await repository.status()
|
gitStatuses = try await repository.status()
|
||||||
|
logger.debug("Loaded \(gitStatuses.count) Git status entries", category: .git)
|
||||||
} catch {
|
} catch {
|
||||||
gitStatuses = [
|
gitStatuses = [
|
||||||
GitFileStatus(path: "Unable to load status: \(error)", state: .conflicted)
|
GitFileStatus(path: "Unable to load status: \(error)", state: .conflicted)
|
||||||
]
|
]
|
||||||
|
logger.error("Failed to refresh Git status: \(error)", category: .git)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setSelectedDocument(_ document: MarkdownDocument) {
|
private func setSelectedDocument(_ document: MarkdownDocument) {
|
||||||
selectedDocument = document
|
selectedDocument = document
|
||||||
editorViewModel = HybridMarkdownEditorViewModel(document: document)
|
editorViewModel = HybridMarkdownEditorViewModel(document: document)
|
||||||
|
logger.info("Selected document: \(document.title)", category: .editor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
39
Sources/SaplingLogging/SaplingLogger.swift
Normal file
39
Sources/SaplingLogging/SaplingLogger.swift
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import Foundation
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
public struct SaplingLogger: Sendable {
|
||||||
|
public enum Category: String, Sendable {
|
||||||
|
case app
|
||||||
|
case workspace
|
||||||
|
case git
|
||||||
|
case editor
|
||||||
|
case storage
|
||||||
|
case rendering
|
||||||
|
}
|
||||||
|
|
||||||
|
private let subsystem: String
|
||||||
|
|
||||||
|
public init(subsystem: String = "app.sapling.Sapling") {
|
||||||
|
self.subsystem = subsystem
|
||||||
|
}
|
||||||
|
|
||||||
|
public func debug(_ message: String, category: Category) {
|
||||||
|
logger(for: category).debug("\(message, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func info(_ message: String, category: Category) {
|
||||||
|
logger(for: category).info("\(message, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func warning(_ message: String, category: Category) {
|
||||||
|
logger(for: category).warning("\(message, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
|
public func error(_ message: String, category: Category) {
|
||||||
|
logger(for: category).error("\(message, privacy: .public)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func logger(for category: Category) -> Logger {
|
||||||
|
Logger(subsystem: subsystem, category: category.rawValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,15 +4,21 @@ public struct SaplingConfiguration: Hashable, Codable, Sendable {
|
||||||
public var recentWorkspaceURLs: [URL]
|
public var recentWorkspaceURLs: [URL]
|
||||||
public var defaultBranchName: String
|
public var defaultBranchName: String
|
||||||
public var autosavesDrafts: Bool
|
public var autosavesDrafts: Bool
|
||||||
|
public var showsHiddenFiles: Bool
|
||||||
|
public var preferredEditorFontSize: Double
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
recentWorkspaceURLs: [URL] = [],
|
recentWorkspaceURLs: [URL] = [],
|
||||||
defaultBranchName: String = "main",
|
defaultBranchName: String = "main",
|
||||||
autosavesDrafts: Bool = true
|
autosavesDrafts: Bool = true,
|
||||||
|
showsHiddenFiles: Bool = false,
|
||||||
|
preferredEditorFontSize: Double = 15
|
||||||
) {
|
) {
|
||||||
self.recentWorkspaceURLs = recentWorkspaceURLs
|
self.recentWorkspaceURLs = recentWorkspaceURLs
|
||||||
self.defaultBranchName = defaultBranchName
|
self.defaultBranchName = defaultBranchName
|
||||||
self.autosavesDrafts = autosavesDrafts
|
self.autosavesDrafts = autosavesDrafts
|
||||||
|
self.showsHiddenFiles = showsHiddenFiles
|
||||||
|
self.preferredEditorFontSize = preferredEditorFontSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,3 +42,34 @@ public final class InMemoryConfigurationStore: ConfigurationStore, @unchecked Se
|
||||||
self.configuration = configuration
|
self.configuration = configuration
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class JSONConfigurationStore: ConfigurationStore, @unchecked Sendable {
|
||||||
|
private let fileURL: URL
|
||||||
|
private let encoder: JSONEncoder
|
||||||
|
private let decoder: JSONDecoder
|
||||||
|
|
||||||
|
public init(fileURL: URL) {
|
||||||
|
self.fileURL = fileURL
|
||||||
|
self.encoder = JSONEncoder()
|
||||||
|
self.decoder = JSONDecoder()
|
||||||
|
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
||||||
|
}
|
||||||
|
|
||||||
|
public func loadConfiguration() throws -> SaplingConfiguration {
|
||||||
|
guard FileManager.default.fileExists(atPath: fileURL.path) else {
|
||||||
|
return SaplingConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = try Data(contentsOf: fileURL)
|
||||||
|
return try decoder.decode(SaplingConfiguration.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func saveConfiguration(_ configuration: SaplingConfiguration) throws {
|
||||||
|
try FileManager.default.createDirectory(
|
||||||
|
at: fileURL.deletingLastPathComponent(),
|
||||||
|
withIntermediateDirectories: true
|
||||||
|
)
|
||||||
|
let data = try encoder.encode(configuration)
|
||||||
|
try data.write(to: fileURL, options: [.atomic])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
47
Sources/SaplingUI/SettingsView.swift
Normal file
47
Sources/SaplingUI/SettingsView.swift
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
import SwiftUI
|
||||||
|
import SaplingStorage
|
||||||
|
|
||||||
|
public struct SettingsView: View {
|
||||||
|
@Binding private var configuration: SaplingConfiguration
|
||||||
|
private let onSave: (SaplingConfiguration) -> Void
|
||||||
|
|
||||||
|
public init(
|
||||||
|
configuration: Binding<SaplingConfiguration>,
|
||||||
|
onSave: @escaping (SaplingConfiguration) -> Void
|
||||||
|
) {
|
||||||
|
self._configuration = configuration
|
||||||
|
self.onSave = onSave
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Form {
|
||||||
|
Section("General") {
|
||||||
|
TextField("Default branch", text: binding(\.defaultBranchName))
|
||||||
|
Toggle("Autosave drafts", isOn: binding(\.autosavesDrafts))
|
||||||
|
Toggle("Show hidden files", isOn: binding(\.showsHiddenFiles))
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Editor") {
|
||||||
|
HStack {
|
||||||
|
Slider(value: binding(\.preferredEditorFontSize), in: 11...24, step: 1)
|
||||||
|
Text("\(Int(configuration.preferredEditorFontSize)) pt")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.monospacedDigit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
.frame(minWidth: 420, minHeight: 260)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func binding<Value>(_ keyPath: WritableKeyPath<SaplingConfiguration, Value>) -> Binding<Value> {
|
||||||
|
Binding(
|
||||||
|
get: { configuration[keyPath: keyPath] },
|
||||||
|
set: { newValue in
|
||||||
|
configuration[keyPath: keyPath] = newValue
|
||||||
|
onSave(configuration)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -27,7 +27,7 @@ without relying on proprietary services.
|
||||||
|
|
||||||
# Milestone 0 — Foundation
|
# Milestone 0 — Foundation
|
||||||
|
|
||||||
Status: In Progress
|
Status: Complete
|
||||||
|
|
||||||
Goal:
|
Goal:
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue