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 { |
| 110 | 110 | |
| 111 | 111 | /// The theme from the file read last and its theme file. |
| 112 | 112 | 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 |
| 130 | 118 | } |
| 131 | 119 | |
| 132 | 120 | /// Writes settings that differ from the file into it. |
Sources/OrgApp/OrgPreferences.swift
+14
| @@ -1,5 +1,6 @@ |
| 1 | 1 | import Foundation |
| 2 | 2 | import OrgCore |
| 3 | import OrgPresentation |
| 3 | 4 | |
| 4 | 5 | /// The Org settings `config.toml` holds, read from user defaults with the file's fallbacks, for |
| 5 | 6 | /// both apps. |
| @@ -33,6 +34,19 @@ public enum OrgPreferences { |
| 33 | 34 | return options |
| 34 | 35 | } |
| 35 | 36 | |
| 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 | |
| 36 | 50 | public static func agendaSpan(_ defaults: UserDefaults = .standard) -> Int { integer("agendaSpan", defaults) } |
| 37 | 51 | public static func agendaStartOffset(_ defaults: UserDefaults = .standard) -> Int { integer("agendaStartOffset", defaults) } |
| 38 | 52 | public static func agendaSubfolders(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSubfolders", defaults) } |
Sources/OrgApp/UserConfig.swift
+25
| @@ -23,6 +23,31 @@ public enum UserConfig { |
| 23 | 23 | return URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".config/orgstar") |
| 24 | 24 | } |
| 25 | 25 | |
| 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 | |
| 26 | 51 | /// Where earlier versions kept the configuration files. |
| 27 | 52 | public static var legacyDirectory: URL { |
| 28 | 53 | FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent("Orgstar") |
Tests/OrgAppTests/OrgPreferencesTests.swift
+15
| @@ -1,5 +1,6 @@ |
| 1 | 1 | import Foundation |
| 2 | 2 | import OrgCore |
| 3 | import OrgPresentation |
| 3 | 4 | import Testing |
| 4 | 5 | @testable import OrgApp |
| 5 | 6 | |
| @@ -36,4 +37,18 @@ struct OrgPreferencesTests { |
| 36 | 37 | #expect(OrgPreferences.agendaSpan(defaults) == 7 && OrgPreferences.agendaStartOffset(defaults) == -1) |
| 37 | 38 | #expect(!OrgPreferences.reminders(defaults) && OrgPreferences.reminderLead(defaults) == 30) |
| 38 | 39 | } |
| 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 | } |
| 39 | 54 | } |
Tests/OrgAppTests/UserConfigTests.swift
+36
| @@ -151,3 +151,39 @@ struct ThemeConfigTests { |
| 151 | 151 | #expect(!sync.isThemeFile(folder.appendingPathComponent("x.toml"))) |
| 152 | 152 | } |
| 153 | 153 | } |
| 154 | |
| 155 | struct 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 | } |