max / makeover
6 files changed,
+1157 insertions,
-0 deletions
| @@ -0,0 +1,504 @@ | |||
| 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.3.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
| @@ -0,0 +1,11 @@ | |||
| 1 | + | [package] | |
| 2 | + | name = "theme-common" | |
| 3 | + | version = "0.3.0" | |
| 4 | + | edition = "2024" | |
| 5 | + | ||
| 6 | + | [dependencies] | |
| 7 | + | serde = { version = "1", features = ["derive"] } | |
| 8 | + | toml = "0.8" | |
| 9 | + | ||
| 10 | + | [dev-dependencies] | |
| 11 | + | tempfile = "3" |
| @@ -0,0 +1,110 @@ | |||
| 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/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 Shared/themes/ relative to CARGO_MANIFEST_DIR | |
| 31 | + | if let Some(dev_dir) = dev_themes_dir(Path::new(env!("CARGO_MANIFEST_DIR")), 2) { | |
| 32 | + | // dev_dir = .../Code/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 `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 `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 |
| @@ -0,0 +1,26 @@ | |||
| 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) | |
| @@ -0,0 +1,10 @@ | |||
| 1 | + | # theme-common — Todo | |
| 2 | + | ||
| 3 | + | Done: Core implementation. Active: None. Next: None planned. | |
| 4 | + | ||
| 5 | + | v0.3.0. 26 tests. | |
| 6 | + | ||
| 7 | + | ## Deferred | |
| 8 | + | ||
| 9 | + | - [ ] Delete theme by ID (remove from custom dir) | |
| 10 | + | - [ ] Theme preview colors (return a small palette for UI previews without loading full theme) |
| @@ -0,0 +1,585 @@ | |||
| 1 | + | //! Shared theme loading logic for TOML-based theme files. | |
| 2 | + | //! | |
| 3 | + | //! Used by GoingsOn, Balanced Breakfast (Tauri apps), and audiofiles (egui). | |
| 4 | + | //! audiofiles embeds themes at compile time but uses `ThemeMeta`, `parse_meta`, | |
| 5 | + | //! and `extract_colors` from this crate. | |
| 6 | + | //! | |
| 7 | + | //! Theme files are TOML with this structure: | |
| 8 | + | //! ```text | |
| 9 | + | //! [meta] | |
| 10 | + | //! name = "Theme Name" | |
| 11 | + | //! variant = "dark" # or "light" | |
| 12 | + | //! | |
| 13 | + | //! [background] | |
| 14 | + | //! primary = "#1e1e2e" | |
| 15 | + | //! | |
| 16 | + | //! [foreground] | |
| 17 | + | //! primary = "#cdd6f4" | |
| 18 | + | //! | |
| 19 | + | //! [accent] | |
| 20 | + | //! primary = "#89b4fa" | |
| 21 | + | //! | |
| 22 | + | //! [border] | |
| 23 | + | //! primary = "#45475a" | |
| 24 | + | //! ``` | |
| 25 | + | ||
| 26 | + | use serde::Serialize; | |
| 27 | + | use std::collections::HashMap; | |
| 28 | + | use std::path::{Path, PathBuf}; | |
| 29 | + | ||
| 30 | + | /// Theme metadata parsed from the `[meta]` section. | |
| 31 | + | #[derive(Debug, Clone, Serialize)] | |
| 32 | + | #[serde(rename_all = "camelCase")] | |
| 33 | + | pub struct ThemeMeta { | |
| 34 | + | pub id: String, | |
| 35 | + | pub name: String, | |
| 36 | + | pub variant: String, | |
| 37 | + | pub is_custom: bool, | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | /// A fully loaded theme: metadata plus flattened color map. | |
| 41 | + | #[derive(Debug, Serialize)] | |
| 42 | + | #[serde(rename_all = "camelCase")] | |
| 43 | + | pub struct ThemeColors { | |
| 44 | + | pub meta: ThemeMeta, | |
| 45 | + | pub colors: HashMap<String, String>, | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | /// Validate a theme ID contains only safe characters (alphanumeric, hyphens, underscores). | |
| 49 | + | pub fn validate_theme_id(id: &str) -> Result<(), String> { | |
| 50 | + | if !id | |
| 51 | + | .chars() | |
| 52 | + | .all(|c| c.is_alphanumeric() || c == '-' || c == '_') | |
| 53 | + | { | |
| 54 | + | return Err(format!("Invalid theme ID: {}", id)); | |
| 55 | + | } | |
| 56 | + | Ok(()) | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | /// Parse the `[meta]` section from a TOML table into `ThemeMeta`. | |
| 60 | + | /// | |
| 61 | + | /// Falls back to the file ID as the name and `"dark"` as the variant. | |
| 62 | + | pub fn parse_meta(id: &str, table: &toml::Table, is_custom: bool) -> ThemeMeta { | |
| 63 | + | let meta = table.get("meta").and_then(|m| m.as_table()); | |
| 64 | + | let name = meta | |
| 65 | + | .and_then(|m| m.get("name")) | |
| 66 | + | .and_then(|v| v.as_str()) | |
| 67 | + | .unwrap_or(id) | |
| 68 | + | .to_string(); | |
| 69 | + | let variant = meta | |
| 70 | + | .and_then(|m| m.get("variant")) | |
| 71 | + | .and_then(|v| v.as_str()) | |
| 72 | + | .unwrap_or("dark") | |
| 73 | + | .to_string(); | |
| 74 | + | ||
| 75 | + | ThemeMeta { | |
| 76 | + | id: id.to_string(), | |
| 77 | + | name, | |
| 78 | + | variant, | |
| 79 | + | is_custom, | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | ||
| 83 | + | /// Extract color sections (background, foreground, accent, border) from a TOML | |
| 84 | + | /// table into a flat `HashMap` with keys like `"background.primary"`. | |
| 85 | + | pub fn extract_colors(table: &toml::Table) -> HashMap<String, String> { | |
| 86 | + | let mut colors = HashMap::new(); | |
| 87 | + | for section in &["background", "foreground", "accent", "border"] { | |
| 88 | + | if let Some(sect) = table.get(*section).and_then(|s| s.as_table()) { | |
| 89 | + | for (key, val) in sect { | |
| 90 | + | if let Some(color) = val.as_str() { | |
| 91 | + | colors.insert(format!("{}.{}", section, key), color.to_string()); | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | colors | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | /// Scan directories for `.toml` theme files and return metadata for each. | |
| 100 | + | /// | |
| 101 | + | /// Directories are checked in order; later entries override earlier ones by ID. | |
| 102 | + | /// Each entry in `dirs` is `(path, is_custom)`. | |
| 103 | + | pub fn list_themes_from_dirs(dirs: &[(PathBuf, bool)]) -> Vec<ThemeMeta> { | |
| 104 | + | let mut seen: HashMap<String, ThemeMeta> = HashMap::new(); | |
| 105 | + | ||
| 106 | + | for (dir, is_custom) in dirs { | |
| 107 | + | let entries = match std::fs::read_dir(dir) { | |
| 108 | + | Ok(e) => e, | |
| 109 | + | Err(_) => continue, | |
| 110 | + | }; | |
| 111 | + | ||
| 112 | + | for entry in entries { | |
| 113 | + | let entry = match entry { | |
| 114 | + | Ok(e) => e, | |
| 115 | + | Err(_) => continue, | |
| 116 | + | }; | |
| 117 | + | let path = entry.path(); | |
| 118 | + | if path.extension().and_then(|e| e.to_str()) != Some("toml") { | |
| 119 | + | continue; | |
| 120 | + | } | |
| 121 | + | ||
| 122 | + | let id = path | |
| 123 | + | .file_stem() | |
| 124 | + | .and_then(|s| s.to_str()) | |
| 125 | + | .unwrap_or_default() | |
| 126 | + | .to_string(); | |
| 127 | + | ||
| 128 | + | let content = match std::fs::read_to_string(&path) { | |
| 129 | + | Ok(c) => c, | |
| 130 | + | Err(_) => continue, | |
| 131 | + | }; | |
| 132 | + | let table: toml::Table = match content.parse() { | |
| 133 | + | Ok(t) => t, | |
| 134 | + | Err(_) => continue, | |
| 135 | + | }; | |
| 136 | + | ||
| 137 | + | seen.insert(id.clone(), parse_meta(&id, &table, *is_custom)); | |
| 138 | + | } | |
| 139 | + | } | |
| 140 | + | ||
| 141 | + | let mut themes: Vec<ThemeMeta> = seen.into_values().collect(); | |
| 142 | + | themes.sort_by(|a, b| a.name.cmp(&b.name)); | |
| 143 | + | themes | |
| 144 | + | } | |
| 145 | + | ||
| 146 | + | /// Find a theme file by ID in the given directories. | |
| 147 | + | /// | |
| 148 | + | /// Checks directories in reverse order so the highest-priority directory wins. | |
| 149 | + | /// Returns `(path, is_custom)` or `None` if not found. | |
| 150 | + | pub fn find_theme_path(dirs: &[(PathBuf, bool)], id: &str) -> Option<(PathBuf, bool)> { | |
| 151 | + | let filename = format!("{}.toml", id); | |
| 152 | + | ||
| 153 | + | for (dir, is_custom) in dirs.iter().rev() { | |
| 154 | + | let path = dir.join(&filename); | |
| 155 | + | if path.is_file() { | |
| 156 | + | return Some((path, *is_custom)); | |
| 157 | + | } | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | None | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | /// Load a complete theme (metadata + colors) by ID from the given directories. | |
| 164 | + | pub fn load_theme(dirs: &[(PathBuf, bool)], id: &str) -> Result<ThemeColors, String> { | |
| 165 | + | validate_theme_id(id)?; | |
| 166 | + | ||
| 167 | + | let (path, is_custom) = | |
| 168 | + | find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{}' not found", id))?; | |
| 169 | + | ||
| 170 | + | let content = std::fs::read_to_string(&path) | |
| 171 | + | .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; | |
| 172 | + | ||
| 173 | + | let table: toml::Table = content | |
| 174 | + | .parse() | |
| 175 | + | .map_err(|e| format!("Failed to parse {}: {}", path.display(), e))?; | |
| 176 | + | ||
| 177 | + | let meta = parse_meta(id, &table, is_custom); | |
| 178 | + | let colors = extract_colors(&table); | |
| 179 | + | ||
| 180 | + | Ok(ThemeColors { meta, colors }) | |
| 181 | + | } | |
| 182 | + | ||
| 183 | + | /// Import a theme TOML file into the custom themes directory. | |
| 184 | + | /// | |
| 185 | + | /// Validates that the file is parseable TOML with the expected color sections, | |
| 186 | + | /// then copies it to `custom_dir/{id}.toml` where `id` is the file stem. | |
| 187 | + | /// Creates `custom_dir` if it doesn't exist. Returns the theme metadata. | |
| 188 | + | pub fn import_theme(source_path: &Path, custom_dir: &Path) -> Result<ThemeMeta, String> { | |
| 189 | + | let content = std::fs::read_to_string(source_path) | |
| 190 | + | .map_err(|e| format!("Failed to read {}: {}", source_path.display(), e))?; | |
| 191 | + | ||
| 192 | + | let table: toml::Table = content | |
| 193 | + | .parse() | |
| 194 | + | .map_err(|e| format!("Invalid TOML: {}", e))?; | |
| 195 | + | ||
| 196 | + | // Verify it has at least one color section | |
| 197 | + | let has_colors = ["background", "foreground", "accent", "border"] | |
| 198 | + | .iter() | |
| 199 | + | .any(|s| table.get(*s).and_then(|v| v.as_table()).is_some()); | |
| 200 | + | if !has_colors { | |
| 201 | + | return Err("Theme file must have at least one color section (background, foreground, accent, or border)".to_string()); | |
| 202 | + | } | |
| 203 | + | ||
| 204 | + | let id = source_path | |
| 205 | + | .file_stem() | |
| 206 | + | .and_then(|s| s.to_str()) | |
| 207 | + | .ok_or("Invalid file name")? | |
| 208 | + | .to_string(); | |
| 209 | + | validate_theme_id(&id)?; | |
| 210 | + | ||
| 211 | + | std::fs::create_dir_all(custom_dir) | |
| 212 | + | .map_err(|e| format!("Failed to create {}: {}", custom_dir.display(), e))?; | |
| 213 | + | ||
| 214 | + | let dest = custom_dir.join(format!("{}.toml", id)); | |
| 215 | + | std::fs::copy(source_path, &dest) | |
| 216 | + | .map_err(|e| format!("Failed to copy theme: {}", e))?; | |
| 217 | + | ||
| 218 | + | Ok(parse_meta(&id, &table, true)) | |
| 219 | + | } | |
| 220 | + | ||
| 221 | + | /// Export a theme to a user-chosen path. | |
| 222 | + | /// | |
| 223 | + | /// Finds the theme by ID in the given directories and copies the TOML file | |
| 224 | + | /// to `dest_path`. | |
| 225 | + | pub fn export_theme(dirs: &[(PathBuf, bool)], id: &str, dest_path: &Path) -> Result<(), String> { | |
| 226 | + | validate_theme_id(id)?; | |
| 227 | + | ||
| 228 | + | let (source, _) = | |
| 229 | + | find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{}' not found", id))?; | |
| 230 | + | ||
| 231 | + | std::fs::copy(&source, dest_path) | |
| 232 | + | .map_err(|e| format!("Failed to export theme: {}", e))?; | |
| 233 | + | ||
| 234 | + | Ok(()) | |
| 235 | + | } | |
| 236 | + | ||
| 237 | + | /// Construct a dev fallback theme directory path. | |
| 238 | + | /// | |
| 239 | + | /// Given a `CARGO_MANIFEST_DIR`, walks up `levels` parent directories and | |
| 240 | + | /// appends `"themes"`. Returns the path if the directory exists. | |
| 241 | + | pub fn dev_themes_dir(manifest_dir: &Path, levels: usize) -> Option<PathBuf> { | |
| 242 | + | let mut path = manifest_dir.to_path_buf(); | |
| 243 | + | for _ in 0..levels { | |
| 244 | + | path = path.parent()?.to_path_buf(); | |
| 245 | + | } | |
| 246 | + | let themes = path.join("MNW").join("shared").join("themes"); | |
| 247 | + | if themes.is_dir() { | |
| 248 | + | Some(themes) | |
| 249 | + | } else { | |
| 250 | + | None | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | ||
| 254 | + | #[cfg(test)] | |
| 255 | + | mod tests { | |
| 256 | + | use super::*; | |
| 257 | + | use std::fs; | |
| 258 | + | ||
| 259 | + | #[test] | |
| 260 | + | fn validate_theme_id_alphanumeric() { | |
| 261 | + | assert!(validate_theme_id("darkmode").is_ok()); | |
| 262 | + | assert!(validate_theme_id("Theme123").is_ok()); | |
| 263 | + | } | |
| 264 | + | ||
| 265 | + | #[test] | |
| 266 | + | fn validate_theme_id_hyphens_underscores() { | |
| 267 | + | assert!(validate_theme_id("dark-mode").is_ok()); | |
| 268 | + | assert!(validate_theme_id("my_theme_v2").is_ok()); | |
| 269 | + | assert!(validate_theme_id("a-b_c-d").is_ok()); | |
| 270 | + | } | |
| 271 | + | ||
| 272 | + | #[test] | |
| 273 | + | fn validate_theme_id_rejects_spaces() { | |
| 274 | + | assert!(validate_theme_id("has space").is_err()); | |
| 275 | + | } | |
| 276 | + | ||
| 277 | + | #[test] | |
| 278 | + | fn validate_theme_id_rejects_path_traversal() { | |
| 279 | + | assert!(validate_theme_id("../etc/passwd").is_err()); | |
| 280 | + | assert!(validate_theme_id("foo/bar").is_err()); | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | #[test] | |
| 284 | + | fn validate_theme_id_rejects_special_chars() { | |
| 285 | + | assert!(validate_theme_id("evil<script>").is_err()); | |
| 286 | + | assert!(validate_theme_id("theme;drop").is_err()); | |
| 287 | + | assert!(validate_theme_id("theme.toml").is_err()); | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | #[test] | |
| 291 | + | fn validate_theme_id_empty_is_valid() { | |
| 292 | + | assert!( | |
| 293 | + | validate_theme_id("").is_ok(), | |
| 294 | + | "empty string has no invalid chars" | |
| 295 | + | ); | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | #[test] | |
| 299 | + | fn parse_meta_with_name_and_variant() { | |
| 300 | + | let toml_str = r#" | |
| 301 | + | [meta] | |
| 302 | + | name = "Solarized Dark" | |
| 303 | + | variant = "light" | |
| 304 | + | "#; | |
| 305 | + | let table: toml::Table = toml_str.parse().unwrap(); | |
| 306 | + | let meta = parse_meta("solarized", &table, false); | |
| 307 | + | assert_eq!(meta.id, "solarized"); | |
| 308 | + | assert_eq!(meta.name, "Solarized Dark"); | |
| 309 | + | assert_eq!(meta.variant, "light"); | |
| 310 | + | assert!(!meta.is_custom); | |
| 311 | + | } | |
| 312 | + | ||
| 313 | + | #[test] | |
| 314 | + | fn parse_meta_defaults_to_id_and_dark() { | |
| 315 | + | let table: toml::Table = "".parse().unwrap(); | |
| 316 | + | let meta = parse_meta("fallback", &table, true); | |
| 317 | + | assert_eq!(meta.name, "fallback"); | |
| 318 | + | assert_eq!(meta.variant, "dark"); | |
| 319 | + | assert!(meta.is_custom); | |
| 320 | + | } | |
| 321 | + | ||
| 322 | + | #[test] | |
| 323 | + | fn parse_meta_missing_variant_defaults_dark() { | |
| 324 | + | let toml_str = r#" | |
| 325 | + | [meta] | |
| 326 | + | name = "Minimal" | |
| 327 | + | "#; | |
| 328 | + | let table: toml::Table = toml_str.parse().unwrap(); | |
| 329 | + | let meta = parse_meta("minimal", &table, false); | |
| 330 | + | assert_eq!(meta.name, "Minimal"); | |
| 331 | + | assert_eq!(meta.variant, "dark"); | |
| 332 | + | } | |
| 333 | + | ||
| 334 | + | #[test] | |
| 335 | + | fn extract_colors_from_toml() { | |
| 336 | + | let toml_str = r##" | |
| 337 | + | [background] | |
| 338 | + | primary = "#1e1e2e" | |
| 339 | + | secondary = "#313244" | |
| 340 | + | ||
| 341 | + | [foreground] | |
| 342 | + | primary = "#cdd6f4" | |
| 343 | + | ||
| 344 | + | [accent] | |
| 345 | + | primary = "#89b4fa" | |
| 346 | + | ||
| 347 | + | [border] | |
| 348 | + | primary = "#45475a" | |
| 349 | + | "##; | |
| 350 | + | let table: toml::Table = toml_str.parse().unwrap(); | |
| 351 | + | let colors = extract_colors(&table); | |
| 352 | + | assert_eq!(colors.get("background.primary").unwrap(), "#1e1e2e"); | |
| 353 | + | assert_eq!(colors.get("background.secondary").unwrap(), "#313244"); | |
| 354 | + | assert_eq!(colors.get("foreground.primary").unwrap(), "#cdd6f4"); | |
| 355 | + | assert_eq!(colors.get("accent.primary").unwrap(), "#89b4fa"); | |
| 356 | + | assert_eq!(colors.get("border.primary").unwrap(), "#45475a"); | |
| 357 | + | assert_eq!(colors.len(), 5); | |
| 358 | + | } | |
| 359 | + | ||
| 360 | + | #[test] | |
| 361 | + | fn extract_colors_empty_table() { | |
| 362 | + | let table: toml::Table = "".parse().unwrap(); | |
| 363 | + | let colors = extract_colors(&table); | |
| 364 | + | assert!(colors.is_empty()); | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | #[test] | |
| 368 | + | fn list_themes_from_dirs_finds_toml_files() { | |
| 369 | + | let dir = tempfile::tempdir().unwrap(); | |
| 370 | + | let theme_content = r#" | |
| 371 | + | [meta] | |
| 372 | + | name = "Test Theme" | |
| 373 | + | variant = "dark" | |
| 374 | + | "#; | |
| 375 | + | fs::write(dir.path().join("test-theme.toml"), theme_content).unwrap(); | |
| 376 | + | fs::write(dir.path().join("not-a-theme.txt"), "ignored").unwrap(); | |
| 377 | + | ||
| 378 | + | let dirs = vec![(dir.path().to_path_buf(), false)]; | |
| 379 | + | let themes = list_themes_from_dirs(&dirs); | |
| 380 | + | assert_eq!(themes.len(), 1); | |
| 381 | + | assert_eq!(themes[0].id, "test-theme"); | |
| 382 | + | assert_eq!(themes[0].name, "Test Theme"); | |
| 383 | + | } | |
| 384 | + | ||
| 385 | + | #[test] | |
| 386 | + | fn list_themes_later_dir_overrides() { | |
| 387 | + | let dir1 = tempfile::tempdir().unwrap(); | |
| 388 | + | let dir2 = tempfile::tempdir().unwrap(); | |
| 389 | + | ||
| 390 | + | let theme1 = "[meta]\nname = \"Original\"\nvariant = \"dark\"\n"; | |
| 391 | + | let theme2 = "[meta]\nname = \"Override\"\nvariant = \"light\"\n"; | |
| 392 | + | ||
| 393 | + | fs::write(dir1.path().join("same-id.toml"), theme1).unwrap(); | |
| 394 | + | fs::write(dir2.path().join("same-id.toml"), theme2).unwrap(); | |
| 395 | + | ||
| 396 | + | let dirs = vec![ | |
| 397 | + | (dir1.path().to_path_buf(), false), | |
| 398 | + | (dir2.path().to_path_buf(), true), | |
| 399 | + | ]; | |
| 400 | + | let themes = list_themes_from_dirs(&dirs); | |
| 401 | + | assert_eq!(themes.len(), 1); | |
| 402 | + | assert_eq!(themes[0].name, "Override"); | |
| 403 | + | assert!(themes[0].is_custom); | |
| 404 | + | } | |
| 405 | + | ||
| 406 | + | #[test] | |
| 407 | + | fn find_theme_path_checks_reverse_priority() { | |
| 408 | + | let dir1 = tempfile::tempdir().unwrap(); | |
| 409 | + | let dir2 = tempfile::tempdir().unwrap(); | |
| 410 | + | ||
| 411 | + | fs::write(dir1.path().join("shared.toml"), "[meta]\n").unwrap(); | |
| 412 | + | fs::write(dir2.path().join("shared.toml"), "[meta]\n").unwrap(); | |
| 413 | + | ||
| 414 | + | let dirs = vec![ | |
| 415 | + | (dir1.path().to_path_buf(), false), | |
| 416 | + | (dir2.path().to_path_buf(), true), | |
| 417 | + | ]; | |
| 418 | + | let (path, is_custom) = find_theme_path(&dirs, "shared").unwrap(); | |
| 419 | + | assert!(is_custom); | |
| 420 | + | assert_eq!(path, dir2.path().join("shared.toml")); | |
| 421 | + | } | |
| 422 | + | ||
| 423 | + | #[test] | |
| 424 | + | fn find_theme_path_returns_none_when_missing() { | |
| 425 | + | let dir = tempfile::tempdir().unwrap(); | |
| 426 | + | let dirs = vec![(dir.path().to_path_buf(), false)]; | |
| 427 | + | assert!(find_theme_path(&dirs, "nonexistent").is_none()); | |
| 428 | + | } | |
| 429 | + | ||
| 430 | + | #[test] | |
| 431 | + | fn load_theme_full_round_trip() { | |
| 432 | + | let dir = tempfile::tempdir().unwrap(); | |
| 433 | + | let theme_content = r##" | |
| 434 | + | [meta] | |
| 435 | + | name = "Full Theme" | |
| 436 | + | variant = "light" | |
| 437 | + | ||
| 438 | + | [background] | |
| 439 | + | primary = "#ffffff" | |
| 440 | + | ||
| 441 | + | [foreground] | |
| 442 | + | primary = "#000000" | |
| 443 | + | ||
| 444 | + | [accent] | |
| 445 | + | primary = "#ff0000" | |
| 446 | + | "##; | |
| 447 | + | fs::write(dir.path().join("full.toml"), theme_content).unwrap(); | |
| 448 | + | ||
| 449 | + | let dirs = vec![(dir.path().to_path_buf(), false)]; | |
| 450 | + | let theme = load_theme(&dirs, "full").unwrap(); | |
| 451 | + | assert_eq!(theme.meta.name, "Full Theme"); | |
| 452 | + | assert_eq!(theme.meta.variant, "light"); | |
| 453 | + | assert_eq!(theme.colors.get("background.primary").unwrap(), "#ffffff"); | |
| 454 | + | assert_eq!(theme.colors.get("foreground.primary").unwrap(), "#000000"); | |
| 455 | + | assert_eq!(theme.colors.get("accent.primary").unwrap(), "#ff0000"); | |
| 456 | + | } | |
| 457 | + | ||
| 458 | + | #[test] | |
| 459 | + | fn load_theme_rejects_invalid_id() { | |
| 460 | + | let dirs = vec![]; | |
| 461 | + | assert!(load_theme(&dirs, "../evil").is_err()); | |
| 462 | + | } | |
| 463 | + | ||
| 464 | + | #[test] | |
| 465 | + | fn load_theme_not_found() { | |
| 466 | + | let dir = tempfile::tempdir().unwrap(); | |
| 467 | + | let dirs = vec![(dir.path().to_path_buf(), false)]; | |
| 468 | + | assert!(load_theme(&dirs, "missing").is_err()); | |
| 469 | + | } | |
| 470 | + | ||
| 471 | + | #[test] | |
| 472 | + | fn dev_themes_dir_walks_parents() { | |
| 473 | + | // Use a real path that exists | |
| 474 | + | let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); | |
| 475 | + | // Walking up from theme-common/ to Code/ (3 levels), MNW/shared/themes/ should exist | |
| 476 | + | let result = dev_themes_dir(&manifest, 3); | |
| 477 | + | // This test is environment-dependent, just verify it returns something valid | |
| 478 | + | if let Some(path) = result { | |
| 479 | + | assert!(path.ends_with("themes")); | |
| 480 | + | } | |
| 481 | + | } | |
| 482 | + | ||
| 483 | + | #[test] | |
| 484 | + | fn import_theme_valid() { | |
| 485 | + | let src_dir = tempfile::tempdir().unwrap(); | |
| 486 | + | let custom_dir = tempfile::tempdir().unwrap(); | |
| 487 | + | ||
| 488 | + | let content = r##" | |
| 489 | + | [meta] | |
| 490 | + | name = "Imported Theme" | |
| 491 | + | variant = "dark" | |
| 492 | + | ||
| 493 | + | [background] | |
| 494 | + | primary = "#1a1b26" | |
| 495 | + | ||
| 496 | + | [foreground] | |
| 497 | + | primary = "#c0caf5" | |
| 498 | + | "##; | |
| 499 | + | let src = src_dir.path().join("my-theme.toml"); | |
| 500 | + | fs::write(&src, content).unwrap(); |
Lines truncated