Skip to main content

max / makenotwork

Remove shared/theme-common and shared/themes Both moved to the standalone makeover crate, published to crates.io. Every consumer now takes it from the registry: MNW server, goingson, balanced_breakfast, audiofiles, and Alloy. Nothing reads these paths. History is preserved in the makeover repo, which was split out of shared/theme-common with git subtree rather than copied.
Author: Max Johnson <me@maxj.phd> · 2026-07-19 20:11 UTC
Commit: 2a655ec8bdc854e28269c1c62daf3a51b4c60e91
Parent: ac22ade
39 files changed, +0 insertions, -2422 deletions
M README.md -2
@@ -14,9 +14,7 @@ MNW/
14 14 docengine/ Markdown rendering + documentation engine
15 15 tagtree/ Hierarchical tag standard
16 16 synckit-client/ SyncKit cloud sync client SDK
17 - theme-common/ Theme loading + parsing (TOML themes)
18 17 s3-storage/ S3-compatible storage abstraction
19 - themes/ TOML theme definitions (24 themes)
20 18 tauri-updater-ui/ OTA update UI components
21 19 ```
22 20
@@ -1,504 +0,0 @@
1 - # This file is automatically @generated by Cargo.
2 - # It is not intended for manual editing.
3 - version = 4
4 -
5 - [[package]]
6 - name = "anyhow"
7 - version = "1.0.102"
8 - source = "registry+https://github.com/rust-lang/crates.io-index"
9 - checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
10 -
11 - [[package]]
12 - name = "bitflags"
13 - version = "2.11.0"
14 - source = "registry+https://github.com/rust-lang/crates.io-index"
15 - checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
16 -
17 - [[package]]
18 - name = "cfg-if"
19 - version = "1.0.4"
20 - source = "registry+https://github.com/rust-lang/crates.io-index"
21 - checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
22 -
23 - [[package]]
24 - name = "equivalent"
25 - version = "1.0.2"
26 - source = "registry+https://github.com/rust-lang/crates.io-index"
27 - checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
28 -
29 - [[package]]
30 - name = "errno"
31 - version = "0.3.14"
32 - source = "registry+https://github.com/rust-lang/crates.io-index"
33 - checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
34 - dependencies = [
35 - "libc",
36 - "windows-sys",
37 - ]
38 -
39 - [[package]]
40 - name = "fastrand"
41 - version = "2.3.0"
42 - source = "registry+https://github.com/rust-lang/crates.io-index"
43 - checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
44 -
45 - [[package]]
46 - name = "foldhash"
47 - version = "0.1.5"
48 - source = "registry+https://github.com/rust-lang/crates.io-index"
49 - checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
50 -
51 - [[package]]
52 - name = "getrandom"
53 - version = "0.4.2"
54 - source = "registry+https://github.com/rust-lang/crates.io-index"
55 - checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
56 - dependencies = [
57 - "cfg-if",
58 - "libc",
59 - "r-efi",
60 - "wasip2",
61 - "wasip3",
62 - ]
63 -
64 - [[package]]
65 - name = "hashbrown"
66 - version = "0.15.5"
67 - source = "registry+https://github.com/rust-lang/crates.io-index"
68 - checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
69 - dependencies = [
70 - "foldhash",
71 - ]
72 -
73 - [[package]]
74 - name = "hashbrown"
75 - version = "0.16.1"
76 - source = "registry+https://github.com/rust-lang/crates.io-index"
77 - checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
78 -
79 - [[package]]
80 - name = "heck"
81 - version = "0.5.0"
82 - source = "registry+https://github.com/rust-lang/crates.io-index"
83 - checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
84 -
85 - [[package]]
86 - name = "id-arena"
87 - version = "2.3.0"
88 - source = "registry+https://github.com/rust-lang/crates.io-index"
89 - checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
90 -
91 - [[package]]
92 - name = "indexmap"
93 - version = "2.13.0"
94 - source = "registry+https://github.com/rust-lang/crates.io-index"
95 - checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
96 - dependencies = [
97 - "equivalent",
98 - "hashbrown 0.16.1",
99 - "serde",
100 - "serde_core",
101 - ]
102 -
103 - [[package]]
104 - name = "itoa"
105 - version = "1.0.18"
106 - source = "registry+https://github.com/rust-lang/crates.io-index"
107 - checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
108 -
109 - [[package]]
110 - name = "leb128fmt"
111 - version = "0.1.0"
112 - source = "registry+https://github.com/rust-lang/crates.io-index"
113 - checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
114 -
115 - [[package]]
116 - name = "libc"
117 - version = "0.2.183"
118 - source = "registry+https://github.com/rust-lang/crates.io-index"
119 - checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
120 -
121 - [[package]]
122 - name = "linux-raw-sys"
123 - version = "0.12.1"
124 - source = "registry+https://github.com/rust-lang/crates.io-index"
125 - checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
126 -
127 - [[package]]
128 - name = "log"
129 - version = "0.4.29"
130 - source = "registry+https://github.com/rust-lang/crates.io-index"
131 - checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
132 -
133 - [[package]]
134 - name = "memchr"
135 - version = "2.8.0"
136 - source = "registry+https://github.com/rust-lang/crates.io-index"
137 - checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
138 -
139 - [[package]]
140 - name = "once_cell"
141 - version = "1.21.4"
142 - source = "registry+https://github.com/rust-lang/crates.io-index"
143 - checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
144 -
145 - [[package]]
146 - name = "prettyplease"
147 - version = "0.2.37"
148 - source = "registry+https://github.com/rust-lang/crates.io-index"
149 - checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
150 - dependencies = [
151 - "proc-macro2",
152 - "syn",
153 - ]
154 -
155 - [[package]]
156 - name = "proc-macro2"
157 - version = "1.0.106"
158 - source = "registry+https://github.com/rust-lang/crates.io-index"
159 - checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
160 - dependencies = [
161 - "unicode-ident",
162 - ]
163 -
164 - [[package]]
165 - name = "quote"
166 - version = "1.0.45"
167 - source = "registry+https://github.com/rust-lang/crates.io-index"
168 - checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
169 - dependencies = [
170 - "proc-macro2",
171 - ]
172 -
173 - [[package]]
174 - name = "r-efi"
175 - version = "6.0.0"
176 - source = "registry+https://github.com/rust-lang/crates.io-index"
177 - checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
178 -
179 - [[package]]
180 - name = "rustix"
181 - version = "1.1.4"
182 - source = "registry+https://github.com/rust-lang/crates.io-index"
183 - checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
184 - dependencies = [
185 - "bitflags",
186 - "errno",
187 - "libc",
188 - "linux-raw-sys",
189 - "windows-sys",
190 - ]
191 -
192 - [[package]]
193 - name = "semver"
194 - version = "1.0.27"
195 - source = "registry+https://github.com/rust-lang/crates.io-index"
196 - checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
197 -
198 - [[package]]
199 - name = "serde"
200 - version = "1.0.228"
201 - source = "registry+https://github.com/rust-lang/crates.io-index"
202 - checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
203 - dependencies = [
204 - "serde_core",
205 - "serde_derive",
206 - ]
207 -
208 - [[package]]
209 - name = "serde_core"
210 - version = "1.0.228"
211 - source = "registry+https://github.com/rust-lang/crates.io-index"
212 - checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
213 - dependencies = [
214 - "serde_derive",
215 - ]
216 -
217 - [[package]]
218 - name = "serde_derive"
219 - version = "1.0.228"
220 - source = "registry+https://github.com/rust-lang/crates.io-index"
221 - checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
222 - dependencies = [
223 - "proc-macro2",
224 - "quote",
225 - "syn",
226 - ]
227 -
228 - [[package]]
229 - name = "serde_json"
230 - version = "1.0.149"
231 - source = "registry+https://github.com/rust-lang/crates.io-index"
232 - checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
233 - dependencies = [
234 - "itoa",
235 - "memchr",
236 - "serde",
237 - "serde_core",
238 - "zmij",
239 - ]
240 -
241 - [[package]]
242 - name = "serde_spanned"
243 - version = "0.6.9"
244 - source = "registry+https://github.com/rust-lang/crates.io-index"
245 - checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
246 - dependencies = [
247 - "serde",
248 - ]
249 -
250 - [[package]]
251 - name = "syn"
252 - version = "2.0.117"
253 - source = "registry+https://github.com/rust-lang/crates.io-index"
254 - checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
255 - dependencies = [
256 - "proc-macro2",
257 - "quote",
258 - "unicode-ident",
259 - ]
260 -
261 - [[package]]
262 - name = "tempfile"
263 - version = "3.27.0"
264 - source = "registry+https://github.com/rust-lang/crates.io-index"
265 - checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
266 - dependencies = [
267 - "fastrand",
268 - "getrandom",
269 - "once_cell",
270 - "rustix",
271 - "windows-sys",
272 - ]
273 -
274 - [[package]]
275 - name = "theme-common"
276 - version = "0.7.0"
277 - dependencies = [
278 - "serde",
279 - "tempfile",
280 - "toml",
281 - ]
282 -
283 - [[package]]
284 - name = "toml"
285 - version = "0.8.23"
286 - source = "registry+https://github.com/rust-lang/crates.io-index"
287 - checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
288 - dependencies = [
289 - "serde",
290 - "serde_spanned",
291 - "toml_datetime",
292 - "toml_edit",
293 - ]
294 -
295 - [[package]]
296 - name = "toml_datetime"
297 - version = "0.6.11"
298 - source = "registry+https://github.com/rust-lang/crates.io-index"
299 - checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
300 - dependencies = [
301 - "serde",
302 - ]
303 -
304 - [[package]]
305 - name = "toml_edit"
306 - version = "0.22.27"
307 - source = "registry+https://github.com/rust-lang/crates.io-index"
308 - checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
309 - dependencies = [
310 - "indexmap",
311 - "serde",
312 - "serde_spanned",
313 - "toml_datetime",
314 - "toml_write",
315 - "winnow",
316 - ]
317 -
318 - [[package]]
319 - name = "toml_write"
320 - version = "0.1.2"
321 - source = "registry+https://github.com/rust-lang/crates.io-index"
322 - checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
323 -
324 - [[package]]
325 - name = "unicode-ident"
326 - version = "1.0.24"
327 - source = "registry+https://github.com/rust-lang/crates.io-index"
328 - checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
329 -
330 - [[package]]
331 - name = "unicode-xid"
332 - version = "0.2.6"
333 - source = "registry+https://github.com/rust-lang/crates.io-index"
334 - checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
335 -
336 - [[package]]
337 - name = "wasip2"
338 - version = "1.0.2+wasi-0.2.9"
339 - source = "registry+https://github.com/rust-lang/crates.io-index"
340 - checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
341 - dependencies = [
342 - "wit-bindgen",
343 - ]
344 -
345 - [[package]]
346 - name = "wasip3"
347 - version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
348 - source = "registry+https://github.com/rust-lang/crates.io-index"
349 - checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
350 - dependencies = [
351 - "wit-bindgen",
352 - ]
353 -
354 - [[package]]
355 - name = "wasm-encoder"
356 - version = "0.244.0"
357 - source = "registry+https://github.com/rust-lang/crates.io-index"
358 - checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
359 - dependencies = [
360 - "leb128fmt",
361 - "wasmparser",
362 - ]
363 -
364 - [[package]]
365 - name = "wasm-metadata"
366 - version = "0.244.0"
367 - source = "registry+https://github.com/rust-lang/crates.io-index"
368 - checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
369 - dependencies = [
370 - "anyhow",
371 - "indexmap",
372 - "wasm-encoder",
373 - "wasmparser",
374 - ]
375 -
376 - [[package]]
377 - name = "wasmparser"
378 - version = "0.244.0"
379 - source = "registry+https://github.com/rust-lang/crates.io-index"
380 - checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
381 - dependencies = [
382 - "bitflags",
383 - "hashbrown 0.15.5",
384 - "indexmap",
385 - "semver",
386 - ]
387 -
388 - [[package]]
389 - name = "windows-link"
390 - version = "0.2.1"
391 - source = "registry+https://github.com/rust-lang/crates.io-index"
392 - checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
393 -
394 - [[package]]
395 - name = "windows-sys"
396 - version = "0.61.2"
397 - source = "registry+https://github.com/rust-lang/crates.io-index"
398 - checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
399 - dependencies = [
400 - "windows-link",
401 - ]
402 -
403 - [[package]]
404 - name = "winnow"
405 - version = "0.7.15"
406 - source = "registry+https://github.com/rust-lang/crates.io-index"
407 - checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
408 - dependencies = [
409 - "memchr",
410 - ]
411 -
412 - [[package]]
413 - name = "wit-bindgen"
414 - version = "0.51.0"
415 - source = "registry+https://github.com/rust-lang/crates.io-index"
416 - checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
417 - dependencies = [
418 - "wit-bindgen-rust-macro",
419 - ]
420 -
421 - [[package]]
422 - name = "wit-bindgen-core"
423 - version = "0.51.0"
424 - source = "registry+https://github.com/rust-lang/crates.io-index"
425 - checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
426 - dependencies = [
427 - "anyhow",
428 - "heck",
429 - "wit-parser",
430 - ]
431 -
432 - [[package]]
433 - name = "wit-bindgen-rust"
434 - version = "0.51.0"
435 - source = "registry+https://github.com/rust-lang/crates.io-index"
436 - checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
437 - dependencies = [
438 - "anyhow",
439 - "heck",
440 - "indexmap",
441 - "prettyplease",
442 - "syn",
443 - "wasm-metadata",
444 - "wit-bindgen-core",
445 - "wit-component",
446 - ]
447 -
448 - [[package]]
449 - name = "wit-bindgen-rust-macro"
450 - version = "0.51.0"
451 - source = "registry+https://github.com/rust-lang/crates.io-index"
452 - checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
453 - dependencies = [
454 - "anyhow",
455 - "prettyplease",
456 - "proc-macro2",
457 - "quote",
458 - "syn",
459 - "wit-bindgen-core",
460 - "wit-bindgen-rust",
461 - ]
462 -
463 - [[package]]
464 - name = "wit-component"
465 - version = "0.244.0"
466 - source = "registry+https://github.com/rust-lang/crates.io-index"
467 - checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
468 - dependencies = [
469 - "anyhow",
470 - "bitflags",
471 - "indexmap",
472 - "log",
473 - "serde",
474 - "serde_derive",
475 - "serde_json",
476 - "wasm-encoder",
477 - "wasm-metadata",
478 - "wasmparser",
479 - "wit-parser",
480 - ]
481 -
482 - [[package]]
483 - name = "wit-parser"
484 - version = "0.244.0"
485 - source = "registry+https://github.com/rust-lang/crates.io-index"
486 - checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
487 - dependencies = [
488 - "anyhow",
489 - "id-arena",
490 - "indexmap",
491 - "log",
492 - "semver",
493 - "serde",
494 - "serde_derive",
495 - "serde_json",
496 - "unicode-xid",
497 - "wasmparser",
498 - ]
499 -
500 - [[package]]
Lines truncated
@@ -1,11 +0,0 @@
1 - [package]
2 - name = "theme-common"
3 - version = "0.7.0"
4 - edition = "2024"
5 -
6 - [dependencies]
7 - serde = { version = "1", features = ["derive"] }
8 - toml = "0.8"
9 -
10 - [dev-dependencies]
11 - tempfile = "3"
@@ -1,110 +0,0 @@
1 - # theme-common
2 -
3 - Shared theme loading logic for TOML-based theme files. Parses theme metadata and color values from `.toml` files on disk.
4 -
5 - Used by GoingsOn and Balanced Breakfast (Tauri apps that load themes at runtime). audiofiles uses a different compile-time embedding approach and does not use this crate.
6 -
7 - ## Usage
8 -
9 - ```rust
10 - use theme_common::{load_theme, list_themes_from_dirs, dev_themes_dir};
11 - use std::path::PathBuf;
12 -
13 - // Set up theme directories (later entries override earlier ones)
14 - let bundled = PathBuf::from("/path/to/MNW/shared/themes");
15 - let custom = PathBuf::from("/path/to/user/custom-themes");
16 - let dirs = vec![(bundled, false), (custom, true)];
17 -
18 - // List available themes (sorted by name)
19 - let themes = list_themes_from_dirs(&dirs);
20 - for t in &themes {
21 - println!("{} ({}, {})", t.name, t.id, t.variant);
22 - }
23 -
24 - // Load a specific theme by ID
25 - let theme = load_theme(&dirs, "catppuccin-mocha").unwrap();
26 - println!("Name: {}", theme.meta.name); // "Catppuccin Mocha"
27 - println!("Variant: {}", theme.meta.variant); // "dark"
28 - println!("BG: {}", theme.colors["background.primary"]); // "#181825"
29 -
30 - // Dev helper: find MNW/shared/themes/ relative to CARGO_MANIFEST_DIR
31 - if let Some(dev_dir) = dev_themes_dir(Path::new(env!("CARGO_MANIFEST_DIR")), 3) {
32 - // dev_dir = .../Code/MNW/shared/themes
33 - }
34 - ```
35 -
36 - ## Theme File Format
37 -
38 - Theme files are TOML with four color sections. See `../themes/` for the 16 bundled themes.
39 -
40 - ```toml
41 - # Attribution comment (optional, for credit)
42 - # Based on Catppuccin by Catppuccin Org -- MIT License
43 -
44 - [meta]
45 - name = "Theme Name" # Display name (required)
46 - variant = "dark" # "dark", "light", or "high-contrast" (default: "dark")
47 -
48 - [background]
49 - primary = "#181825" # Main background
50 - secondary = "#11111b" # Sidebar / panel background (optional)
51 - tertiary = "#313244" # Hover / selection background (optional)
52 - surface = "#1e1e2e" # Card / elevated surface (optional)
53 -
54 - [foreground]
55 - primary = "#cdd6f4" # Main text
56 - secondary = "#bac2de" # Secondary text (optional)
57 - muted = "#9399b2" # Placeholder / disabled text (optional)
58 -
59 - [accent]
60 - red = "#f38ba8" # Error, destructive actions
61 - green = "#a6e3a1" # Success, positive actions
62 - blue = "#89b4fa" # Links, primary accent
63 - yellow = "#f9e2af" # Warnings
64 - purple = "#cba6f7" # Tags, special elements
65 - cyan = "#89dceb" # Info, secondary accent
66 -
67 - [border]
68 - default = "#45475a" # Default border color
69 - ```
70 -
71 - ### Color Key Flattening
72 -
73 - Colors are loaded into a flat `HashMap<String, String>` with dotted keys:
74 -
75 - ```
76 - "background.primary" -> "#181825"
77 - "foreground.muted" -> "#9399b2"
78 - "accent.blue" -> "#89b4fa"
79 - "border.default" -> "#45475a"
80 - ```
81 -
82 - Apps map these keys to CSS variables or egui color values.
83 -
84 - ### Theme ID
85 -
86 - The theme ID is the filename without `.toml` (e.g., `catppuccin-mocha.toml` has ID `catppuccin-mocha`). IDs must contain only alphanumeric characters, hyphens, and underscores. Path traversal characters are rejected.
87 -
88 - ## API
89 -
90 - | Function | Description |
91 - |----------|-------------|
92 - | `list_themes_from_dirs(dirs)` | Scan directories for `.toml` files, return sorted `Vec<ThemeMeta>` |
93 - | `load_theme(dirs, id)` | Load a theme by ID, returning `ThemeColors` (metadata + color map) |
94 - | `find_theme_path(dirs, id)` | Find the file path for a theme ID (highest-priority directory wins) |
95 - | `parse_meta(id, table, is_custom)` | Parse `[meta]` from a TOML table into `ThemeMeta` |
96 - | `extract_colors(table)` | Flatten color sections into a `HashMap<String, String>` |
97 - | `validate_theme_id(id)` | Check that an ID contains only safe characters |
98 - | `dev_themes_dir(manifest_dir, levels)` | Walk up parent directories to find `MNW/shared/themes/` (dev convenience) |
99 -
100 - ## Directory Priority
101 -
102 - `list_themes_from_dirs` and `load_theme` accept a list of `(PathBuf, bool)` pairs. Later directories override earlier ones by theme ID. The `bool` marks whether the directory contains user-custom themes (`is_custom` on `ThemeMeta`).
103 -
104 - Typical setup for a Tauri app:
105 - 1. Bundled themes from `MNW/shared/themes/` (is_custom = false)
106 - 2. User themes from an app data directory (is_custom = true)
107 -
108 - ## License
109 -
110 - PolyForm Noncommercial 1.0.0
@@ -1,26 +0,0 @@
1 - # theme-common — Architecture
2 -
3 - Shared TOML theme loading for GO, BB, and AF.
4 -
5 - ## What It Does
6 -
7 - Parses TOML theme files with `[meta]`, `[background]`, `[foreground]`, `[accent]`, `[border]` sections. Provides metadata extraction, color flattening, filesystem scanning, import/export, and ID validation.
8 -
9 - ## Consumers
10 -
11 - | App | Usage |
12 - |-----|-------|
13 - | GO, BB | `list_themes_from_dirs`, `load_theme`, `import_theme`, `export_theme` (runtime filesystem) |
14 - | AF | `ThemeMeta`, `parse_meta`, `extract_colors` (compile-time embedded themes) |
15 -
16 - ## Key Types
17 -
18 - - `ThemeMeta` — id, name, variant, is_custom
19 - - `ThemeColors` — metadata + flattened color map (`background.primary` etc.)
20 -
21 - ## Key Paths
22 -
23 - | What | Where |
24 - |------|-------|
25 - | All code | `src/lib.rs` (single file, ~585 lines including tests) |
26 - | Theme files | `../themes/*.toml` (16 bundled themes) |
@@ -1,8 +0,0 @@
1 - fn main() {
2 - let id = std::env::args().nth(1).unwrap_or_else(|| "makenotwork".into());
3 - let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("themes");
4 - let t = theme_common::load_semantic(&[(dir, false)], &id).unwrap();
5 - for (k, v) in &t.intents {
6 - println!(" --{k}: {v};");
7 - }
8 - }
@@ -1,986 +0,0 @@
1 - //! Shared theme loading + intent resolution for TOML-based theme files.
2 - //!
3 - //! Used by GoingsOn, Balanced Breakfast (Tauri apps), audiofiles (egui), and the
4 - //! MNW web server. Themes are authored by **intent** ("human design"): colors are
5 - //! declared by role (surface / content / action / status / line / category), not
6 - //! by hue. This crate is the single place that resolves an authored theme into a
7 - //! full set of intent tokens — including the derived interactive states
8 - //! (hover/active/selection/row-stripe/contrast) that each app used to recompute
9 - //! itself — and emits them as CSS variables or RGB tuples.
10 - //!
11 - //! Theme file shape:
12 - //! ```text
13 - //! [meta]
14 - //! name = "Nord"
15 - //! variant = "dark" # or "light"
16 - //!
17 - //! [surface] # container backgrounds by role/elevation
18 - //! page = "#2e3440"; raised = "#3b4252"; sunken = "#434c5e"; overlay = "#3b4252"
19 - //!
20 - //! [content] # text/ink by emphasis
21 - //! primary = "#d8dee9"; secondary = "#e5e9f0"; muted = "#616e88"
22 - //!
23 - //! [action] # interactive / brand color
24 - //! primary = "#81a1c1"
25 - //!
26 - //! [status] # state semantics
27 - //! danger = "#bf616a"; success = "#a3be8c"; warning = "#ebcb8b"; info = "#88c0d0"
28 - //!
29 - //! [line]
30 - //! border = "#4c566a"
31 - //!
32 - //! [category] # distinct decorative colors for tags/badges/charts
33 - //! one = "#bf616a"; two = "#a3be8c"; three = "#81a1c1"
34 - //! four = "#ebcb8b"; five = "#b48ead"; six = "#88c0d0"
35 - //! ```
36 -
37 - use serde::Serialize;
38 - use std::collections::{BTreeMap, HashMap};
39 - use std::path::{Path, PathBuf};
40 -
41 - /// The color sections an authored theme may declare.
42 - pub const COLOR_SECTIONS: &[&str] = &["surface", "content", "action", "status", "line", "category"];
43 -
44 - /// Theme metadata parsed from the `[meta]` section.
45 - #[derive(Debug, Clone, Serialize)]
46 - #[serde(rename_all = "camelCase")]
47 - pub struct ThemeMeta {
48 - pub id: String,
49 - pub name: String,
50 - pub variant: String,
51 - pub is_custom: bool,
52 - }
53 -
54 - /// A loaded theme: metadata plus the authored colors, flattened to dotted keys
55 - /// (e.g. `"surface.page"`, `"status.danger"`, `"category.one"`).
56 - #[derive(Debug, Serialize)]
57 - #[serde(rename_all = "camelCase")]
58 - pub struct ThemeColors {
59 - pub meta: ThemeMeta,
60 - pub colors: HashMap<String, String>,
61 - }
62 -
63 - // ============================================================================
64 - // Color math — perceptual (OKLab) derivations + WCAG contrast.
65 - //
66 - // Interactive states (hover/active/selection/surfaces) are derived in OKLab so
67 - // equal steps look equal across every theme's hues (Ottosson 2020; the modern
68 - // CIELAB). Text-on-color is picked by the WCAG 2.x contrast ratio, not a naive
69 - // luminance threshold, so the choice actually meets AA where achievable.
70 - // This is the single source of truth shared by every product.
71 - // ============================================================================
72 -
73 - /// An sRGB color. Hex round-trips losslessly.
74 - #[derive(Clone, Copy, Debug, PartialEq, Eq)]
75 - pub struct Rgb {
76 - pub r: u8,
77 - pub g: u8,
78 - pub b: u8,
79 - }
80 -
81 - impl Rgb {
82 - /// Parse `#rgb` or `#rrggbb` (case-insensitive). Returns `None` otherwise.
83 - pub fn from_hex(s: &str) -> Option<Rgb> {
84 - let h = s.strip_prefix('#')?;
85 - let (r, g, b) = match h.len() {
86 - 6 => (
87 - u8::from_str_radix(&h[0..2], 16).ok()?,
88 - u8::from_str_radix(&h[2..4], 16).ok()?,
89 - u8::from_str_radix(&h[4..6], 16).ok()?,
90 - ),
91 - 3 => {
92 - let d = |c: &str| u8::from_str_radix(c, 16).ok().map(|v| v * 17);
93 - (d(&h[0..1])?, d(&h[1..2])?, d(&h[2..3])?)
94 - }
95 - _ => return None,
96 - };
97 - Some(Rgb { r, g, b })
98 - }
99 -
100 - /// Lowercase `#rrggbb`.
101 - pub fn to_hex(self) -> String {
102 - format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
103 - }
104 -
105 - pub fn tuple(self) -> (u8, u8, u8) {
106 - (self.r, self.g, self.b)
107 - }
108 - }
109 -
110 - /// A color in OKLab (perceptually uniform): `l` lightness in [0,1], `a`/`b` opponent axes.
111 - #[derive(Clone, Copy, Debug)]
112 - pub struct Oklab {
113 - pub l: f32,
114 - pub a: f32,
115 - pub b: f32,
116 - }
117 -
118 - fn srgb_to_linear(c: u8) -> f32 {
119 - let c = c as f32 / 255.0;
120 - if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
121 - }
122 -
123 - fn linear_to_srgb(c: f32) -> u8 {
124 - let c = c.clamp(0.0, 1.0);
125 - let v = if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 };
126 - (v * 255.0).round().clamp(0.0, 255.0) as u8
127 - }
128 -
129 - impl Rgb {
130 - /// Convert to OKLab (Ottosson's sRGB matrices).
131 - pub fn to_oklab(self) -> Oklab {
132 - let (r, g, b) = (srgb_to_linear(self.r), srgb_to_linear(self.g), srgb_to_linear(self.b));
133 - let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
134 - let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
135 - let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
136 - let (l_, m_, s_) = (l.cbrt(), m.cbrt(), s.cbrt());
137 - Oklab {
138 - l: 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
139 - a: 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
140 - b: 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
141 - }
142 - }
143 -
144 - /// Convert from OKLab back to the nearest in-gamut sRGB.
145 - pub fn from_oklab(c: Oklab) -> Rgb {
146 - let l_ = c.l + 0.3963377774 * c.a + 0.2158037573 * c.b;
147 - let m_ = c.l - 0.1055613458 * c.a - 0.0638541728 * c.b;
148 - let s_ = c.l - 0.0894841775 * c.a - 1.2914855480 * c.b;
149 - let (l, m, s) = (l_ * l_ * l_, m_ * m_ * m_, s_ * s_ * s_);
150 - Rgb {
151 - r: linear_to_srgb(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
152 - g: linear_to_srgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
153 - b: linear_to_srgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s),
154 - }
155 - }
156 - }
157 -
158 - /// WCAG 2.x relative luminance of an sRGB color.
159 - fn rel_luminance(c: Rgb) -> f32 {
160 - 0.2126 * srgb_to_linear(c.r) + 0.7152 * srgb_to_linear(c.g) + 0.0722 * srgb_to_linear(c.b)
161 - }
162 -
163 - /// WCAG 2.x contrast ratio between two colors, in [1, 21].
164 - pub fn wcag_contrast(a: Rgb, b: Rgb) -> f32 {
165 - let (la, lb) = (rel_luminance(a), rel_luminance(b));
166 - let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) };
167 - (hi + 0.05) / (lo + 0.05)
168 - }
169 -
170 - /// Pick black or white for legible text on `bg`, by the higher WCAG contrast
171 - /// ratio (so the choice meets AA wherever the background allows it).
172 - pub fn readable_on(bg: Rgb) -> Rgb {
173 - let white = Rgb { r: 255, g: 255, b: 255 };
174 - let black = Rgb { r: 0, g: 0, b: 0 };
175 - if wcag_contrast(white, bg) >= wcag_contrast(black, bg) { white } else { black }
176 - }
177 -
178 - /// Shift OKLab lightness by `delta` (perceptually uniform). Positive lightens.
179 - pub fn lighten(c: Rgb, delta: f32) -> Rgb {
180 - let mut lab = c.to_oklab();
181 - lab.l = (lab.l + delta).clamp(0.0, 1.0);
182 - Rgb::from_oklab(lab)
183 - }
184 -
185 - /// Shift OKLab lightness down by `delta` (perceptually uniform).
186 - pub fn darken(c: Rgb, delta: f32) -> Rgb {
187 - lighten(c, -delta)
188 - }
189 -
190 - /// Interpolate between `a` and `b` by `t` in [0,1] in OKLab (perceptual blend).
191 - pub fn mix(a: Rgb, b: Rgb, t: f32) -> Rgb {
192 - let (x, y) = (a.to_oklab(), b.to_oklab());
193 - Rgb::from_oklab(Oklab {
194 - l: x.l + (y.l - x.l) * t,
195 - a: x.a + (y.a - x.a) * t,
196 - b: x.b + (y.b - x.b) * t,
197 - })
198 - }
199 -
200 - // ============================================================================
201 - // Intent resolution
202 - // ============================================================================
203 -
204 - /// Authored base intents: (TOML dotted source key, canonical token key).
205 - /// These are read straight from the theme; the token key is the CSS-var stem
206 - /// (`--{token}`) and the `rgb()` lookup key.
207 - pub const BASE_INTENTS: &[(&str, &str)] = &[
208 - ("surface.page", "surface-page"),
209 - ("surface.raised", "surface-raised"),
210 - ("surface.sunken", "surface-sunken"),
211 - ("surface.overlay", "surface-overlay"),
212 - ("content.primary", "content"),
213 - ("content.secondary", "content-secondary"),
214 - ("content.muted", "content-muted"),
215 - ("action.primary", "action"),
216 - ("status.danger", "danger"),
217 - ("status.success", "success"),
218 - ("status.warning", "warning"),
219 - ("status.info", "info"),
220 - ("line.border", "border"),
221 - ("category.one", "category-one"),
222 - ("category.two", "category-two"),
223 - ("category.three", "category-three"),
224 - ("category.four", "category-four"),
225 - ("category.five", "category-five"),
226 - ("category.six", "category-six"),
227 - ];
228 -
229 - /// A fully resolved intent layer: every token key → concrete `#rrggbb`.
230 - /// Includes both authored base intents and the computed derived intents.
231 - #[derive(Debug, Clone, Serialize)]
232 - #[serde(rename_all = "camelCase")]
233 - pub struct SemanticTokens {
234 - pub meta: ThemeMeta,
235 - /// token-key → resolved hex. Stable, deterministic ordering.
236 - pub intents: BTreeMap<String, String>,
237 - }
238 -
239 - impl SemanticTokens {
240 - /// Resolved hex for a token key, if present.
241 - pub fn hex(&self, key: &str) -> Option<&str> {
242 - self.intents.get(key).map(String::as_str)
243 - }
244 -
245 - /// Resolved RGB tuple for a token key (for egui / native consumers).
246 - pub fn rgb(&self, key: &str) -> Option<(u8, u8, u8)> {
247 - self.intents.get(key).and_then(|h| Rgb::from_hex(h)).map(Rgb::tuple)
248 - }
249 - }
250 -
251 - /// Resolve an authored theme into the full intent token set.
252 - ///
253 - /// 1. Copy each present base intent from the authored colors.
254 - /// 2. Compute the derived interactive states from the base intents, using the
255 - /// same math the apps used to apply individually (so output is identical).
256 - /// Each derived token is emitted only when its source intents exist, mirroring
257 - /// the skip-missing behavior of the rest of the crate.
258 - pub fn resolve(theme: &ThemeColors) -> SemanticTokens {
259 - let mut intents: BTreeMap<String, String> = BTreeMap::new();
260 -
261 - // 1. Base intents (authored). Copy only values that parse as a hex color and
262 - // re-emit them in canonical `#rrggbb` form, so an authored value can never
263 - // carry arbitrary bytes into the emitted CSS (the resolved tokens are inlined
264 - // raw into a `<style>` block by the web server). A malformed value is skipped,
265 - // mirroring the skip-missing behavior for absent intents.
266 - for (src, token) in BASE_INTENTS {
267 - if let Some(rgb) = theme.colors.get(*src).and_then(|v| Rgb::from_hex(v)) {
268 - intents.insert((*token).to_string(), rgb.to_hex());
269 - }
270 - }
271 -
272 - // Helper: parse an already-resolved token to Rgb.
273 - let get = |m: &BTreeMap<String, String>, k: &str| m.get(k).and_then(|h| Rgb::from_hex(h));
274 -
275 - // 2. Derived intents — perceptual (OKLab) steps + WCAG-picked text.
276 - // Lightness deltas are in OKLab L units; mix ratios interpolate in OKLab.
277 - let mut derived: Vec<(String, Rgb)> = Vec::new();
278 - if let Some(action) = get(&intents, "action") {
279 - derived.push(("action-hover".into(), lighten(action, 0.05)));
280 - derived.push(("content-on-action".into(), readable_on(action)));
281 - derived.push(("focus-ring".into(), action));
282 - }
283 - if let Some(page) = get(&intents, "surface-page") {
284 - // Modal scrim: a near-black tone carrying a faint hint of the theme's
285 - // hue, at 50% alpha. Anchored very dark (OKLab L=0.08) so it dims the
286 - // page on light *and* dark themes. Emitted as rgba (not a flat hex), so
287 - // it is inserted directly rather than through the hex loop below.
288 - let mut o = page.to_oklab();
289 - o.l = 0.08;
290 - let s = Rgb::from_oklab(o);
291 - intents.insert("overlay".into(), format!("rgba({}, {}, {}, 0.5)", s.r, s.g, s.b));
292 - }
293 - if let Some(sunken) = get(&intents, "surface-sunken") {
294 - derived.push(("hover-surface".into(), sunken));
295 - }
296 - if let Some(border) = get(&intents, "border") {
297 - derived.push(("border-strong".into(), darken(border, 0.05)));
298 - }
299 -
300 - for (token, rgb) in derived {
301 - intents.insert(token, rgb.to_hex());
302 - }
303 -
304 - SemanticTokens { meta: theme.meta.clone(), intents }
305 - }
306 -
307 - /// Emit the resolved intent layer as CSS declarations (no selector), one
308 - /// ` --token: #hex;` line each, in deterministic (BTreeMap) order.
309 - pub fn intent_css_declarations(tokens: &SemanticTokens) -> String {
310 - let mut out = String::new();
311 - for (token, hex) in &tokens.intents {
312 - out.push_str(" --");
313 - out.push_str(token);
314 - out.push_str(": ");
315 - out.push_str(hex);
316 - out.push_str(";\n");
317 - }
318 - out
319 - }
320 -
321 - /// Emit the resolved intent layer as a `:root { … }` block — the single TOML →
322 - /// CSS mapping every web surface injects.
323 - pub fn intent_css_vars(tokens: &SemanticTokens) -> String {
324 - format!(":root {{\n{}}}\n", intent_css_declarations(tokens))
325 - }
326 -
327 - // ============================================================================
328 - // Loading / parsing
329 - // ============================================================================
330 -
331 - /// Validate a theme ID contains only safe characters (alphanumeric, hyphens, underscores).
332 - pub fn validate_theme_id(id: &str) -> Result<(), String> {
333 - if !id
334 - .chars()
335 - .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
336 - {
337 - return Err(format!("Invalid theme ID: {}", id));
338 - }
339 - Ok(())
340 - }
341 -
342 - /// Parse the `[meta]` section into `ThemeMeta`.
343 - ///
344 - /// Falls back to the file ID as the name and `"dark"` as the variant.
345 - pub fn parse_meta(id: &str, table: &toml::Table, is_custom: bool) -> ThemeMeta {
346 - let meta = table.get("meta").and_then(|m| m.as_table());
347 - let name = meta
348 - .and_then(|m| m.get("name"))
349 - .and_then(|v| v.as_str())
350 - .unwrap_or(id)
351 - .to_string();
352 - let variant = meta
353 - .and_then(|m| m.get("variant"))
354 - .and_then(|v| v.as_str())
355 - .unwrap_or("dark")
356 - .to_string();
357 -
358 - ThemeMeta { id: id.to_string(), name, variant, is_custom }
359 - }
360 -
361 - /// Extract the intent color sections into a flat `HashMap` with dotted keys
362 - /// like `"surface.page"`, `"status.danger"`, `"category.one"`.
363 - pub fn extract_colors(table: &toml::Table) -> HashMap<String, String> {
364 - let mut colors = HashMap::new();
365 - for section in COLOR_SECTIONS {
366 - if let Some(sect) = table.get(*section).and_then(|s| s.as_table()) {
367 - for (key, val) in sect {
368 - if let Some(color) = val.as_str() {
369 - colors.insert(format!("{}.{}", section, key), color.to_string());
370 - }
371 - }
372 - }
373 - }
374 - colors
375 - }
376 -
377 - /// Scan directories for `.toml` theme files and return metadata for each.
378 - ///
379 - /// Directories are checked in order; later entries override earlier ones by ID.
380 - /// Each entry in `dirs` is `(path, is_custom)`.
381 - pub fn list_themes_from_dirs(dirs: &[(PathBuf, bool)]) -> Vec<ThemeMeta> {
382 - let mut seen: HashMap<String, ThemeMeta> = HashMap::new();
383 -
384 - for (dir, is_custom) in dirs {
385 - let entries = match std::fs::read_dir(dir) {
386 - Ok(e) => e,
387 - Err(_) => continue,
388 - };
389 -
390 - for entry in entries {
391 - let entry = match entry {
392 - Ok(e) => e,
393 - Err(_) => continue,
394 - };
395 - let path = entry.path();
396 - if path.extension().and_then(|e| e.to_str()) != Some("toml") {
397 - continue;
398 - }
399 -
400 - let id = path
401 - .file_stem()
402 - .and_then(|s| s.to_str())
403 - .unwrap_or_default()
404 - .to_string();
405 -
406 - let content = match std::fs::read_to_string(&path) {
407 - Ok(c) => c,
408 - Err(_) => continue,
409 - };
410 - let table: toml::Table = match content.parse() {
411 - Ok(t) => t,
412 - Err(_) => continue,
413 - };
414 -
415 - seen.insert(id.clone(), parse_meta(&id, &table, *is_custom));
416 - }
417 - }
418 -
419 - let mut themes: Vec<ThemeMeta> = seen.into_values().collect();
420 - themes.sort_by(|a, b| a.name.cmp(&b.name));
421 - themes
422 - }
423 -
424 - /// Find a theme file by ID in the given directories.
425 - ///
426 - /// Checks directories in reverse order so the highest-priority directory wins.
427 - /// Returns `(path, is_custom)` or `None` if not found.
428 - pub fn find_theme_path(dirs: &[(PathBuf, bool)], id: &str) -> Option<(PathBuf, bool)> {
429 - let filename = format!("{}.toml", id);
430 -
431 - for (dir, is_custom) in dirs.iter().rev() {
432 - let path = dir.join(&filename);
433 - if path.is_file() {
434 - return Some((path, *is_custom));
435 - }
436 - }
437 -
438 - None
439 - }
440 -
441 - /// Parse a complete theme (metadata + colors) from raw TOML content, with no
442 - /// filesystem access. For callers that embed themes at compile time.
443 - pub fn parse_theme_str(id: &str, content: &str, is_custom: bool) -> Result<ThemeColors, String> {
444 - validate_theme_id(id)?;
445 - let table: toml::Table = content
446 - .parse()
447 - .map_err(|e| format!("Failed to parse theme '{}': {}", id, e))?;
448 - let meta = parse_meta(id, &table, is_custom);
449 - let colors = extract_colors(&table);
450 - Ok(ThemeColors { meta, colors })
451 - }
452 -
453 - /// Load a complete theme (metadata + colors) by ID from the given directories.
454 - pub fn load_theme(dirs: &[(PathBuf, bool)], id: &str) -> Result<ThemeColors, String> {
455 - validate_theme_id(id)?;
456 -
457 - let (path, is_custom) =
458 - find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{}' not found", id))?;
459 -
460 - let content = std::fs::read_to_string(&path)
461 - .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;
462 -
463 - let table: toml::Table = content
464 - .parse()
465 - .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?;
466 -
467 - let meta = parse_meta(id, &table, is_custom);
468 - let colors = extract_colors(&table);
469 -
470 - Ok(ThemeColors { meta, colors })
471 - }
472 -
473 - /// Load a theme and resolve it to the full intent token set in one step.
474 - pub fn load_semantic(dirs: &[(PathBuf, bool)], id: &str) -> Result<SemanticTokens, String> {
475 - Ok(resolve(&load_theme(dirs, id)?))
476 - }
477 -
478 - /// Import a theme TOML file into the custom themes directory.
479 - ///
480 - /// Validates that the file is parseable TOML with at least one intent color
481 - /// section, then copies it to `custom_dir/{id}.toml`. Returns the theme metadata.
482 - pub fn import_theme(source_path: &Path, custom_dir: &Path) -> Result<ThemeMeta, String> {
483 - let content = std::fs::read_to_string(source_path)
484 - .map_err(|e| format!("Failed to read {}: {}", source_path.display(), e))?;
485 -
486 - let table: toml::Table = content
487 - .parse()
488 - .map_err(|e| format!("Invalid TOML: {}", e))?;
489 -
490 - let has_colors = COLOR_SECTIONS
491 - .iter()
492 - .any(|s| table.get(*s).and_then(|v| v.as_table()).is_some());
493 - if !has_colors {
494 - return Err(format!(
495 - "Theme file must have at least one color section ({})",
496 - COLOR_SECTIONS.join(", ")
497 - ));
498 - }
499 -
500 - let id = source_path
Lines truncated
@@ -1,94 +0,0 @@
1 - # Themes
2 -
3 - 16 TOML color themes shared across GoingsOn, Balanced Breakfast, and audiofiles.
4 -
5 - ## Bundled Themes
6 -
7 - | File | Name | Variant | Source |
8 - |------|------|---------|--------|
9 - | `ayu-light.toml` | Ayu Light | light | [ayu-theme](https://github.com/ayu-theme) |
10 - | `catppuccin-latte.toml` | Catppuccin Latte | light | [Catppuccin](https://github.com/catppuccin/catppuccin) |
11 - | `catppuccin-mocha.toml` | Catppuccin Mocha | dark | [Catppuccin](https://github.com/catppuccin/catppuccin) |
12 - | `dracula.toml` | Dracula | dark | [Dracula](https://github.com/dracula/dracula-theme) |
13 - | `everforest.toml` | Everforest | dark | [Everforest](https://github.com/sainnhe/everforest) |
14 - | `flatwhite.toml` | Flatwhite | light | [Flatwhite](https://github.com/biletskyy/flatwhite-syntax) |
15 - | `gruvbox-dark.toml` | Gruvbox Dark | dark | [Gruvbox](https://github.com/morhetz/gruvbox) |
16 - | `gruvbox-light.toml` | Gruvbox Light | light | [Gruvbox](https://github.com/morhetz/gruvbox) |
17 - | `high-contrast.toml` | High Contrast | high-contrast | Original |
18 - | `kanagawa.toml` | Kanagawa | dark | [Kanagawa](https://github.com/rebelot/kanagawa.nvim) |
19 - | `neobrute.toml` | Neobrute | dark | Original |
20 - | `nord.toml` | Nord | dark | [Nord](https://github.com/nordtheme/nord) |
21 - | `rosepine-dawn.toml` | Rose Pine Dawn | light | [Rose Pine](https://github.com/rose-pine/rose-pine-theme) |
22 - | `rosepine.toml` | Rose Pine | dark | [Rose Pine](https://github.com/rose-pine/rose-pine-theme) |
23 - | `solarized-dark.toml` | Solarized Dark | dark | [Solarized](https://github.com/altercation/solarized) |
24 - | `tokyonight.toml` | Tokyo Night | dark | [Tokyo Night](https://github.com/folke/tokyonight.nvim) |
25 -
26 - ## File Format
27 -
28 - Each theme is a TOML file with four color sections:
29 -
30 - ```toml
31 - # Attribution (optional)
32 - # Based on Catppuccin by Catppuccin Org -- MIT License
33 -
34 - [meta]
35 - name = "Theme Name" # Display name (required)
36 - variant = "dark" # "dark", "light", or "high-contrast"
37 -
38 - [background]
39 - primary = "#181825" # Main background
40 - secondary = "#11111b" # Sidebar / panel background
41 - tertiary = "#313244" # Hover / selection background
42 - surface = "#1e1e2e" # Card / elevated surface
43 -
44 - [foreground]
45 - primary = "#cdd6f4" # Main text
46 - secondary = "#bac2de" # Secondary text
47 - muted = "#9399b2" # Placeholder / disabled text
48 -
49 - [accent]
50 - red = "#f38ba8" # Error, destructive actions
51 - green = "#a6e3a1" # Success, positive actions
52 - blue = "#89b4fa" # Links, primary accent
53 - yellow = "#f9e2af" # Warnings
54 - purple = "#cba6f7" # Tags, special elements
55 - cyan = "#89dceb" # Info, secondary accent
56 -
57 - [border]
58 - default = "#45475a" # Default border color
59 - ```
60 -
61 - ### Required Fields
62 -
63 - - `[meta].name` -- falls back to the filename if missing
64 - - `[meta].variant` -- falls back to `"dark"` if missing
65 - - At least `[background].primary` and `[foreground].primary` for a usable theme
66 -
67 - All other fields are optional. Apps should fall back gracefully when a color key is missing.
68 -
69 - ### Theme ID
70 -
71 - The filename without `.toml` is the theme ID. IDs must contain only alphanumeric characters, hyphens, and underscores. Examples: `catppuccin-mocha`, `gruvbox-light`, `high-contrast`.
72 -
73 - ## How Apps Load Themes
74 -
75 - | App | Method | Crate |
76 - |-----|--------|-------|
77 - | GoingsOn | Runtime from disk via Tauri resource glob | `theme-common` |
78 - | Balanced Breakfast | Runtime from disk via Tauri resource glob | `theme-common` |
79 - | audiofiles | Compile-time embed via `include_str!` | Direct TOML parsing |
80 -
81 - GoingsOn and Balanced Breakfast include this directory as a Tauri resource (`../../../MNW/shared/themes/*.toml` in `tauri.conf.json`). The `theme-common` crate handles parsing and color extraction at runtime.
82 -
83 - audiofiles embeds theme files at compile time and parses TOML directly without the `theme-common` crate.
84 -
85 - ## Adding a New Theme
86 -
87 - 1. Create `your-theme-name.toml` in this directory
88 - 2. Add an attribution comment at the top if based on an existing theme
89 - 3. Fill in all sections (`meta`, `background`, `foreground`, `accent`, `border`)
90 - 4. The theme will be available automatically on next build (GO/BB pick it up via the resource glob, AF picks it up via compile-time include)
91 -
92 - ## License
93 -
94 - Individual theme color palettes are attributed to their original creators. The TOML files themselves are part of this project under PolyForm Noncommercial 1.0.0.
@@ -1,52 +0,0 @@
1 - # Based on Akari by Shu Kutsuzawa (cappyzawa) — MIT License
2 - # https://github.com/cappyzawa/akari-theme
3 - #
4 - # Akari Dawn — inspired by Japanese alleys lit by round lanterns.
5 - # Design principles from the source: light is singular (lantern-orange
6 - # is the only primary accent), blue is air not light (night sky),
7 - # purple stays muted, green is life, black is warm grey. Alloy adopts
8 - # this as its default light theme.
9 - #
10 - # Note on surface tiers: theme-common's convention is that `raised`
11 - # reads as elevated (lighter in light mode) and `sunken` as recessed
12 - # (darker). Akari's own Helix theme uses different semantics (a darker
13 - # "surface" for statusline / cursorline). The tiers below reinterpret
14 - # Akari's palette to theme-common's L-ordering while staying inside
15 - # the source's tonal range — page and sunken take Akari's own values;
16 - # raised and overlay are computed warmer/lighter tones that read as
17 - # "paper caught in more lamp light."
18 -
19 - [meta]
20 - name = "Akari Dawn"
21 - variant = "light"
22 -
23 - [surface]
24 - page = "#e4ded6"
25 - raised = "#ede7de"
26 - sunken = "#cfc4b6"
27 - overlay = "#f0ece4"
28 -
29 - [content]
30 - primary = "#1a1816"
31 - secondary = "#222d38"
32 - muted = "#514b45"
33 -
34 - [action]
35 - primary = "#8a4530"
36 -
37 - [status]
38 - danger = "#6a2828"
39 - success = "#3a5830"
40 - warning = "#b07840"
41 - info = "#304050"
42 -
43 - [line]
44 - border = "#cabeae"
45 -
46 - [category]
47 - one = "#8a4530"
48 - two = "#3a5830"
49 - three = "#304050"
50 - four = "#b07840"
51 - five = "#806080"
52 - six = "#305858"
@@ -1,42 +0,0 @@
1 - # Based on Akari by Shu Kutsuzawa (cappyzawa) — MIT License
2 - # https://github.com/cappyzawa/akari-theme
3 - #
4 - # Akari Night — dark counterpart to Akari Dawn. Same design principles:
5 - # lantern-orange is the only primary light source, blue is night sky
6 - # (not accent light), green is life, purple stays quiet. Alloy adopts
7 - # this as its default dark theme.
8 -
9 - [meta]
10 - name = "Akari Night"
11 - variant = "dark"
12 -
13 - [surface]
14 - page = "#25231f"
15 - raised = "#2e2c27"
16 - sunken = "#1c1a17"
17 - overlay = "#35322c"
18 -
19 - [content]
20 - primary = "#e6ded3"
21 - secondary = "#c0b8ad"
22 - muted = "#8e847a"
23 -
24 - [action]
25 - primary = "#e26a3b"
26 -
27 - [status]
28 - danger = "#d25046"
29 - success = "#7faf6a"
30 - warning = "#d4a05a"
31 - info = "#7a8fa2"
32 -
33 - [line]
34 - border = "#3d3a35"
35 -
36 - [category]
37 - one = "#e26a3b"
38 - two = "#7faf6a"
39 - three = "#7a8fa2"
40 - four = "#d4a05a"
41 - five = "#8e7ba0"
42 - six = "#6f8f8a"
@@ -1,39 +0,0 @@
1 - # audiofiles — the app's titular default skin. Cross-available as a universal
2 - # theme. Built from a 5-tone earthy palette: Ash Grey #CCDAD1 / #9CAEA9,
3 - # Grey Olive #788585, Dim Grey #6F6866, Deep Mocha #38302E. Accents are
4 - # desaturated naturals tuned to harmonize with the sage-to-mocha range.
5 -
6 - [meta]
7 - name = "audiofiles"
8 - variant = "light"
9 -
10 - [surface]
11 - page = "#CCDAD1"
12 - raised = "#DAE3DC"
13 - sunken = "#9CAEA9"
14 - overlay = "#B4C5BB"
15 -
16 - [content]
17 - primary = "#38302E"
18 - secondary = "#6F6866"
19 - muted = "#788585"
20 -
21 - [action]
22 - primary = "#5E7C8E"
23 -
24 - [status]
25 - danger = "#B05F4E"
26 - success = "#6F8A5C"
27 - warning = "#C19A53"
28 - info = "#5F8C82"
29 -
30 - [line]
31 - border = "#9CAEA9"
32 -
33 - [category]
34 - one = "#B05F4E"
35 - two = "#6F8A5C"
36 - three = "#5E7C8E"
37 - four = "#C19A53"
38 - five = "#836A80"
39 - six = "#5F8C82"
@@ -1,37 +0,0 @@
1 - # Based on Ayu by Konstantin Pschera — MIT License
2 - # https://github.com/ayu-theme
3 -
4 - [meta]
5 - name = "Ayu Light"
6 - variant = "light"
7 -
8 - [surface]
9 - page = "#e7eaed"
10 - raised = "#f2f4f6"
11 - sunken = "#d0d4d8"
12 - overlay = "#dde1e5"
13 -
14 - [content]
15 - primary = "#5c6166"
16 - secondary = "#6b7580"
17 - muted = "#8b9199"
18 -
19 - [action]
20 - primary = "#399ee6"
21 -
22 - [status]
23 - danger = "#f07171"
24 - success = "#86b300"
25 - warning = "#ffaa33"
26 - info = "#55b4d4"
27 -
28 - [line]
29 - border = "#828c9a"
30 -
31 - [category]
32 - one = "#f07171"
33 - two = "#86b300"
34 - three = "#399ee6"
35 - four = "#ffaa33"
36 - five = "#a37acc"
37 - six = "#55b4d4"
@@ -1,37 +0,0 @@
1 - # Based on Ayu by dempfi — MIT License
2 - # https://github.com/ayu-theme/ayu-colors
3 -
4 - [meta]
5 - name = "Ayu Mirage"
6 - variant = "dark"
7 -
8 - [surface]
9 - page = "#1f2430"
10 - raised = "#242936"
11 - sunken = "#272d38"
12 - overlay = "#191e29"
13 -
14 - [content]
15 - primary = "#cccac2"
16 - secondary = "#b8b4aa"
17 - muted = "#707a8c"
18 -
19 - [action]
20 - primary = "#5ccfe6"
21 -
22 - [status]
23 - danger = "#f28779"
24 - success = "#bae67e"
25 - warning = "#ffd580"
26 - info = "#95e6cb"
27 -
28 - [line]
29 - border = "#33415e"
30 -
31 - [category]
32 - one = "#f28779"
33 - two = "#bae67e"
34 - three = "#5ccfe6"
35 - four = "#ffd580"
36 - five = "#d4bfff"
37 - six = "#95e6cb"
@@ -1,37 +0,0 @@
1 - # Based on Carbonfox by EdenEast — MIT License
2 - # https://github.com/EdenEast/nightfox.nvim
3 -
4 - [meta]
5 - name = "Carbonfox"
6 - variant = "dark"
7 -
8 - [surface]
9 - page = "#161616"
10 - raised = "#252525"
11 - sunken = "#1e1e1e"
12 - overlay = "#101010"
13 -
14 - [content]
15 - primary = "#f2f4f8"
16 - secondary = "#dde1e6"
17 - muted = "#878d96"
18 -
19 - [action]
20 - primary = "#78a9ff"
21 -
22 - [status]
23 - danger = "#ee5396"
24 - success = "#25be6a"
25 - warning = "#08bdba"
26 - info = "#33b1ff"
27 -
28 - [line]
29 - border = "#393939"
30 -
31 - [category]
32 - one = "#ee5396"
33 - two = "#25be6a"
34 - three = "#78a9ff"
35 - four = "#08bdba"
36 - five = "#be95ff"
37 - six = "#33b1ff"
@@ -1,37 +0,0 @@
1 - # Based on Catppuccin by Catppuccin Org — MIT License
2 - # https://github.com/catppuccin/catppuccin
3 -
4 - [meta]
5 - name = "Catppuccin Frappé"
6 - variant = "dark"
7 -
8 - [surface]
9 - page = "#303446"
10 - raised = "#232634"
11 - sunken = "#414559"
12 - overlay = "#292c3c"
13 -
14 - [content]
15 - primary = "#c6d0f5"
16 - secondary = "#b5bfe2"
17 - muted = "#a5adce"
18 -
19 - [action]
20 - primary = "#8caaee"
21 -
22 - [status]
23 - danger = "#e78284"
24 - success = "#a6d189"
25 - warning = "#e5c890"
26 - info = "#81c8be"
27 -
28 - [line]
29 - border = "#626880"
30 -
31 - [category]
32 - one = "#e78284"
33 - two = "#a6d189"
34 - three = "#8caaee"
35 - four = "#e5c890"
36 - five = "#ca9ee6"
37 - six = "#81c8be"
@@ -1,37 +0,0 @@
1 - # Based on Catppuccin by Catppuccin Org — MIT License
2 - # https://github.com/catppuccin/catppuccin
3 -
4 - [meta]
5 - name = "Catppuccin Latte"
6 - variant = "light"
7 -
8 - [surface]
9 - page = "#e6e9ef"
10 - raised = "#eff1f5"
11 - sunken = "#ccd0da"
12 - overlay = "#dce0e8"
13 -
14 - [content]
15 - primary = "#4c4f69"
16 - secondary = "#5c5f77"
17 - muted = "#6c6f82"
18 -
19 - [action]
20 - primary = "#1e66f5"
21 -
22 - [status]
23 - danger = "#d20f39"
24 - success = "#40a02b"
25 - warning = "#df8e1d"
26 - info = "#04a5e5"
27 -
28 - [line]
29 - border = "#bcc0cc"
30 -
31 - [category]
32 - one = "#d20f39"
33 - two = "#40a02b"
34 - three = "#1e66f5"
35 - four = "#df8e1d"
36 - five = "#8839ef"
37 - six = "#04a5e5"
@@ -1,37 +0,0 @@
1 - # Based on Catppuccin by Catppuccin Org — MIT License
2 - # https://github.com/catppuccin/catppuccin
3 -
4 - [meta]
5 - name = "Catppuccin Macchiato"
6 - variant = "dark"
7 -
8 - [surface]
9 - page = "#24273a"
10 - raised = "#181926"
11 - sunken = "#363a4f"
12 - overlay = "#1e2030"
13 -
14 - [content]
15 - primary = "#cad3f5"
16 - secondary = "#b8c0e0"
17 - muted = "#a5adcb"
18 -
19 - [action]
20 - primary = "#8aadf4"
21 -
22 - [status]
23 - danger = "#ed8796"
24 - success = "#a6da95"
25 - warning = "#eed49f"
26 - info = "#8bd5ca"
27 -
28 - [line]
29 - border = "#5b6078"
30 -
31 - [category]
32 - one = "#ed8796"
33 - two = "#a6da95"
34 - three = "#8aadf4"
35 - four = "#eed49f"
36 - five = "#c6a0f6"
37 - six = "#8bd5ca"
@@ -1,37 +0,0 @@
1 - # Based on Catppuccin by Catppuccin Org — MIT License
2 - # https://github.com/catppuccin/catppuccin
3 -
4 - [meta]
5 - name = "Catppuccin Mocha"
6 - variant = "dark"
7 -
8 - [surface]
9 - page = "#181825"
10 - raised = "#1e1e2e"
11 - sunken = "#313244"
12 - overlay = "#11111b"
13 -
14 - [content]
15 - primary = "#cdd6f4"
16 - secondary = "#bac2de"
17 - muted = "#9399b2"
18 -
19 - [action]
20 - primary = "#89b4fa"
21 -
22 - [status]
23 - danger = "#f38ba8"
24 - success = "#a6e3a1"
25 - warning = "#f9e2af"
26 - info = "#89dceb"
27 -
28 - [line]
29 - border = "#45475a"
30 -
31 - [category]
32 - one = "#f38ba8"
33 - two = "#a6e3a1"
34 - three = "#89b4fa"
35 - four = "#f9e2af"
36 - five = "#cba6f7"
37 - six = "#89dceb"
@@ -1,37 +0,0 @@
1 - # Based on Dawnfox by EdenEast — MIT License
2 - # https://github.com/EdenEast/nightfox.nvim
3 -
4 - [meta]
5 - name = "Dawnfox"
6 - variant = "light"
7 -
8 - [surface]
9 - page = "#faf4ed"
10 - raised = "#f5efe8"
11 - sunken = "#ebe0df"
12 - overlay = "#ebe5df"
13 -
14 - [content]
15 - primary = "#575279"
16 - secondary = "#625c87"
17 - muted = "#a8a3b3"
18 -
19 - [action]
20 - primary = "#286983"
21 -
22 - [status]
23 - danger = "#b4637a"
24 - success = "#618774"
25 - warning = "#ea9d34"
26 - info = "#56949f"
27 -
28 - [line]
29 - border = "#bdbfc9"
30 -
31 - [category]
32 - one = "#b4637a"
33 - two = "#618774"
34 - three = "#286983"
35 - four = "#ea9d34"
36 - five = "#907aa9"
37 - six = "#56949f"
@@ -1,37 +0,0 @@
1 - # Based on Dracula by Dracula Theme — MIT License
2 - # https://github.com/dracula/dracula-theme
3 -
4 - [meta]
5 - name = "Dracula"
6 - variant = "dark"
7 -
8 - [surface]
9 - page = "#222430"
10 - raised = "#282A36"
11 - sunken = "#44475a"
12 - overlay = "#191A21"
13 -
14 - [content]
15 - primary = "#f8f8f2"
16 - secondary = "#f8f8f2"
17 - muted = "#6272A4"
18 -
19 - [action]
20 - primary = "#8be9fd"
21 -
22 - [status]
23 - danger = "#ff5555"
24 - success = "#50fa7b"
25 - warning = "#f1fa8c"
26 - info = "#8be9fd"
27 -
28 - [line]
29 - border = "#44475a"
30 -
31 - [category]
32 - one = "#ff5555"
33 - two = "#50fa7b"
34 - three = "#8be9fd"
35 - four = "#f1fa8c"
36 - five = "#BD93F9"
37 - six = "#8be9fd"