krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Commit 8af828ca78

8af828ca78168be96c1ec24203c5c9c43194f570

parent: 23deff7456

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-08 06:53 UTC

Read the theme and its type settings outside ConfigSync

UserConfig.theme stacks config.toml's colors over the theme file's over the
default theme, for the iOS app; OrgPreferences gives the [theme] type
settings and show-markup.

Layout: unified · split

Sources/OrgApp/ConfigSync.swift +5 −17
@@ -110,23 +110,11 @@ public final class ConfigSync {
110110
111111 /// The theme from the file read last and its theme file.
112112 func readTheme() {
113 guard let text, let tables = try? TOML.parse(text) else { return }
114 var spec = ThemeSpec.default
115 var themeProblems: [String] = []
116 if let name = defaults.string(forKey: "themeFile"), let file = themeFile {
117 if let themeText = try? String(contentsOf: file, encoding: .utf8) {
118 do {
119 (spec, themeProblems) = ThemeSpec.reading(try TOML.parse(themeText), file: name)
120 } catch {
121 themeProblems.append("\(name): \(error)")
122 }
123 } else {
124 themeProblems.append("config.toml: theme-file \(name) can't be read")
125 }
126 }
127 let (merged, more) = ThemeSpec.reading(tables, over: spec)
128 if merged != theme { theme = merged }
129 problems = configProblems + themeProblems + more
113 guard let text, let read = UserConfig.theme(
114 config: text, themeFile: defaults.string(forKey: "themeFile"), in: url.deletingLastPathComponent()
115 ) else { return }
116 if read.theme != theme { theme = read.theme }
117 problems = configProblems + read.problems
130118 }
131119
132120 /// Writes settings that differ from the file into it.
Sources/OrgApp/OrgPreferences.swift +14
@@ -1,5 +1,6 @@
11import Foundation
22import OrgCore
3import OrgPresentation
34
45/// The Org settings `config.toml` holds, read from user defaults with the file's fallbacks, for
56/// both apps.
@@ -33,6 +34,19 @@ public enum OrgPreferences {
3334 return options
3435 }
3536
37 /// `colors` with the type settings of `[theme]`: font, size, line spacing, heading step.
38 public static func theme(_ colors: ThemeSpec, _ defaults: UserDefaults = .standard) -> ThemeSpec {
39 var spec = colors
40 spec.font = string("themeFont", defaults)
41 spec.fontSize = integer("themeFontSize", defaults)
42 spec.lineSpacing = integer("themeLineSpacing", defaults)
43 spec.headingSizeStep = integer("themeHeadingStep", defaults)
44 return spec
45 }
46
47 /// `show-markup`: link brackets and emphasis markers shown.
48 public static func showMarkup(_ defaults: UserDefaults = .standard) -> Bool { bool("showMarkup", defaults) }
49
3650 public static func agendaSpan(_ defaults: UserDefaults = .standard) -> Int { integer("agendaSpan", defaults) }
3751 public static func agendaStartOffset(_ defaults: UserDefaults = .standard) -> Int { integer("agendaStartOffset", defaults) }
3852 public static func agendaSubfolders(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSubfolders", defaults) }
Sources/OrgApp/UserConfig.swift +25
@@ -23,6 +23,31 @@ public enum UserConfig {
2323 return URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".config/orgstar")
2424 }
2525
26 /// The theme `config.toml` (`text`) sets: its `[theme]` tables' colors over those of
27 /// `theme-file` (a path relative to `folder`), over the default theme; with problems in
28 /// both. Nil when `text` isn't TOML.
29 public static func theme(
30 config text: String, themeFile name: String?, in folder: URL,
31 read: (URL) -> String? = { try? String(contentsOf: $0, encoding: .utf8) }
32 ) -> (theme: ThemeSpec, problems: [String])? {
33 guard let tables = try? TOML.parse(text) else { return nil }
34 var spec = ThemeSpec.default
35 var problems: [String] = []
36 if let name, !name.isEmpty {
37 if let themeText = read(folder.appendingPathComponent(name)) {
38 do {
39 (spec, problems) = ThemeSpec.reading(try TOML.parse(themeText), file: name)
40 } catch {
41 problems.append("\(name): \(error)")
42 }
43 } else {
44 problems.append("config.toml: theme-file \(name) can't be read")
45 }
46 }
47 let (merged, more) = ThemeSpec.reading(tables, over: spec)
48 return (merged, problems + more)
49 }
50
2651 /// Where earlier versions kept the configuration files.
2752 public static var legacyDirectory: URL {
2853 FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent("Orgstar")
Tests/OrgAppTests/OrgPreferencesTests.swift +15
@@ -1,5 +1,6 @@
11import Foundation
22import OrgCore
3import OrgPresentation
34import Testing
45@testable import OrgApp
56
@@ -36,4 +37,18 @@ struct OrgPreferencesTests {
3637 #expect(OrgPreferences.agendaSpan(defaults) == 7 && OrgPreferences.agendaStartOffset(defaults) == -1)
3738 #expect(!OrgPreferences.reminders(defaults) && OrgPreferences.reminderLead(defaults) == 30)
3839 }
40
41 @Test func themeTypeAndShowMarkup() throws {
42 let defaults = try #require(UserDefaults(suiteName: "orgstar-preferences-\(UUID().uuidString)"))
43 var colors = ThemeSpec.default
44 colors.light["link"] = RGBA(hex: "#000001")
45 let fallback = OrgPreferences.theme(colors, defaults)
46 #expect(fallback.font == "" && fallback.fontSize == 13 && fallback.lineSpacing == 2 && fallback.headingSizeStep == 1)
47 #expect(fallback.light["link"]?.hex == "#000001")
48 #expect(!OrgPreferences.showMarkup(defaults))
49 _ = UserConfig.apply("[theme]\nfont = \"Menlo\"\nfont-size = 16\nline-spacing = 4\nheading-size-step = 2\n[orgstar]\nshow-markup = true\n", to: defaults)
50 let spec = OrgPreferences.theme(colors, defaults)
51 #expect(spec.font == "Menlo" && spec.fontSize == 16 && spec.lineSpacing == 4 && spec.headingSizeStep == 2)
52 #expect(OrgPreferences.showMarkup(defaults))
53 }
3954}
Tests/OrgAppTests/UserConfigTests.swift +36
@@ -151,3 +151,39 @@ struct ThemeConfigTests {
151151 #expect(!sync.isThemeFile(folder.appendingPathComponent("x.toml")))
152152 }
153153}
154
155struct ThemeReadingTests {
156 let folder = URL(fileURLWithPath: "/config")
157
158 @Test func configColorsOverThemeFileOverDefault() throws {
159 var read: [String] = []
160 let config = "[theme]\ntheme-file = \"themes/mine.toml\"\nlink = \"#000003\"\n[theme.dark]\nbackground = \"#000004\"\n"
161 let result = try #require(UserConfig.theme(config: config, themeFile: "themes/mine.toml", in: folder) { url in
162 read.append(url.path)
163 return "[theme.light]\nlink = \"#000001\"\ntodo = \"#000002\"\n[theme.todo]\nWAIT = \"#000005\"\n"
164 })
165 #expect(read == ["/config/themes/mine.toml"])
166 #expect(result.problems.isEmpty, "\(result.problems)")
167 #expect(result.theme.light["link"]?.hex == "#000003" && result.theme.dark["link"]?.hex == "#000003")
168 #expect(result.theme.light["todo"]?.hex == "#000002")
169 #expect(result.theme.dark["todo"] == ThemeSpec.default.dark["todo"])
170 #expect(result.theme.dark["background"]?.hex == "#000004" && result.theme.light["background"] == ThemeSpec.default.light["background"])
171 #expect(result.theme.todo["WAIT"]?.hex == "#000005")
172 }
173
174 @Test func noThemeFileIsTheDefaultWithConfigColors() throws {
175 let result = try #require(UserConfig.theme(config: "[theme]\nfont-size = 15\n", themeFile: "", in: folder) { _ in
176 Issue.record("read a theme file")
177 return nil
178 })
179 #expect(result.theme == ThemeSpec.default && result.problems.isEmpty)
180 }
181
182 @Test func problems() throws {
183 let missing = try #require(UserConfig.theme(config: "", themeFile: "gone.toml", in: folder) { _ in nil })
184 #expect(missing.problems == ["config.toml: theme-file gone.toml can't be read"])
185 let bad = try #require(UserConfig.theme(config: "[theme]\nlink = \"blue\"\n", themeFile: "t.toml", in: folder) { _ in "[theme]\nnope = \"#000000\"\n" })
186 #expect(bad.problems == ["t.toml: unknown theme color nope", "config.toml: theme.link must be a color such as \"#1f2328\""])
187 #expect(UserConfig.theme(config: "[theme", themeFile: nil, in: folder)?.theme == nil)
188 }
189}