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 | /// The theme from the file read last and its theme file. |
111 | /// The theme from the file read last and its theme file. |
| 112 | func readTheme() { |
112 | func readTheme() { |
| 113 | guard let text, let tables = try? TOML.parse(text) else { return } |
113 | guard let text, let read = UserConfig.theme( |
| 114 | var spec = ThemeSpec.default |
114 | config: text, themeFile: defaults.string(forKey: "themeFile"), in: url.deletingLastPathComponent() |
| 115 | var themeProblems: [String] = [] |
115 | ) else { return } |
| 116 | if let name = defaults.string(forKey: "themeFile"), let file = themeFile { |
116 | if read.theme != theme { theme = read.theme } |
| 117 | if let themeText = try? String(contentsOf: file, encoding: .utf8) { |
117 | problems = configProblems + read.problems |
| 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 |
| |
| 130 | } |
118 | } |
| 131 | |
119 | |
| 132 | /// Writes settings that differ from the file into it. |
120 | /// Writes settings that differ from the file into it. |
Sources/OrgApp/OrgPreferences.swift
+14
| @@ -1,5 +1,6 @@ |
| 1 | import Foundation |
1 | import Foundation |
| 2 | import OrgCore |
2 | import OrgCore |
| |
3 | import OrgPresentation |
| 3 | |
4 | |
| 4 | /// The Org settings `config.toml` holds, read from user defaults with the file's fallbacks, for |
5 | /// The Org settings `config.toml` holds, read from user defaults with the file's fallbacks, for |
| 5 | /// both apps. |
6 | /// both apps. |
| @@ -33,6 +34,19 @@ public enum OrgPreferences { |
| 33 | return options |
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 | public static func agendaSpan(_ defaults: UserDefaults = .standard) -> Int { integer("agendaSpan", defaults) } |
50 | public static func agendaSpan(_ defaults: UserDefaults = .standard) -> Int { integer("agendaSpan", defaults) } |
| 37 | public static func agendaStartOffset(_ defaults: UserDefaults = .standard) -> Int { integer("agendaStartOffset", defaults) } |
51 | public static func agendaStartOffset(_ defaults: UserDefaults = .standard) -> Int { integer("agendaStartOffset", defaults) } |
| 38 | public static func agendaSubfolders(_ defaults: UserDefaults = .standard) -> Bool { bool("agendaSubfolders", defaults) } |
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 | return URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".config/orgstar") |
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 | /// Where earlier versions kept the configuration files. |
51 | /// Where earlier versions kept the configuration files. |
| 27 | public static var legacyDirectory: URL { |
52 | public static var legacyDirectory: URL { |
| 28 | FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent("Orgstar") |
53 | FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0].appendingPathComponent("Orgstar") |
Tests/OrgAppTests/OrgPreferencesTests.swift
+15
| @@ -1,5 +1,6 @@ |
| 1 | import Foundation |
1 | import Foundation |
| 2 | import OrgCore |
2 | import OrgCore |
| |
3 | import OrgPresentation |
| 3 | import Testing |
4 | import Testing |
| 4 | @testable import OrgApp |
5 | @testable import OrgApp |
| 5 | |
6 | |
| @@ -36,4 +37,18 @@ struct OrgPreferencesTests { |
| 36 | #expect(OrgPreferences.agendaSpan(defaults) == 7 && OrgPreferences.agendaStartOffset(defaults) == -1) |
37 | #expect(OrgPreferences.agendaSpan(defaults) == 7 && OrgPreferences.agendaStartOffset(defaults) == -1) |
| 37 | #expect(!OrgPreferences.reminders(defaults) && OrgPreferences.reminderLead(defaults) == 30) |
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 | #expect(!sync.isThemeFile(folder.appendingPathComponent("x.toml"))) |
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 | } |