| 1 |
# makeover |
| 2 |
|
| 3 |
Shared theme loading for the make-family apps. Parses theme metadata and color values from `.toml` files on disk, resolves them into intent-based tokens, and derives the rest perceptually (OKLab) with WCAG contrast checks. |
| 4 |
|
| 5 |
The crate ships the theme set it loads, in `themes/`. Consumers get working themes from a clean checkout without depending on any sibling repo. |
| 6 |
|
| 7 |
Used by MNW server, GoingsOn, Balanced Breakfast, audiofiles, and Alloy. |
| 8 |
|
| 9 |
The crate ships 31 themes. `bundled_themes_dir()` hands back the directory; `embedded_themes()` hands back the same set as `(id, toml_source)` pairs with no filesystem path involved, for consumers that embed at compile time or bundle assets at build time. |
| 10 |
|
| 11 |
## Usage |
| 12 |
|
| 13 |
```rust |
| 14 |
use makeover::{load_theme, list_themes_from_dirs, bundled_themes_dir}; |
| 15 |
use std::path::PathBuf; |
| 16 |
|
| 17 |
// Set up theme directories (later entries override earlier ones) |
| 18 |
let bundled = bundled_themes_dir().expect("makeover ships themes/"); |
| 19 |
let custom = PathBuf::from("/path/to/user/custom-themes"); |
| 20 |
let dirs = vec![(bundled, false), (custom, true)]; |
| 21 |
|
| 22 |
// List available themes (sorted by name) |
| 23 |
let themes = list_themes_from_dirs(&dirs); |
| 24 |
for t in &themes { |
| 25 |
println!("{} ({}, {})", t.name, t.id, t.variant); |
| 26 |
} |
| 27 |
|
| 28 |
// Load a specific theme by ID |
| 29 |
let theme = load_theme(&dirs, "catppuccin-mocha").unwrap(); |
| 30 |
println!("Name: {}", theme.meta.name); // "Catppuccin Mocha" |
| 31 |
println!("Variant: {}", theme.meta.variant); // "dark" |
| 32 |
println!("BG: {}", theme.colors["surface.page"]); // "#1e1e2e" |
| 33 |
|
| 34 |
// Build-from-source fallback: the themes this crate ships |
| 35 |
if let Some(dir) = bundled_themes_dir() { |
| 36 |
// dir = <makeover checkout>/themes |
| 37 |
} |
| 38 |
``` |
| 39 |
|
| 40 |
## Theme File Format |
| 41 |
|
| 42 |
Colors are declared by **intent** (the role a color plays), not by hue. See `themes/` for the 31 bundled themes. |
| 43 |
|
| 44 |
```toml |
| 45 |
[meta] |
| 46 |
name = "Nord" # Display name; falls back to the filename |
| 47 |
variant = "dark" # "dark", "light", or "high-contrast" (default: "dark") |
| 48 |
|
| 49 |
[surface] # Container backgrounds by elevation |
| 50 |
page = "#2e3440" |
| 51 |
raised = "#3b4252" |
| 52 |
sunken = "#434c5e" |
| 53 |
overlay = "#3b4252" |
| 54 |
|
| 55 |
[content] # Text and ink by emphasis |
| 56 |
primary = "#d8dee9" |
| 57 |
secondary = "#e5e9f0" |
| 58 |
muted = "#616e88" |
| 59 |
|
| 60 |
[action] # Interactive / brand color |
| 61 |
primary = "#81a1c1" |
| 62 |
|
| 63 |
[status] # State semantics |
| 64 |
danger = "#bf616a" |
| 65 |
success = "#a3be8c" |
| 66 |
warning = "#ebcb8b" |
| 67 |
info = "#88c0d0" |
| 68 |
|
| 69 |
[line] |
| 70 |
border = "#4c566a" |
| 71 |
|
| 72 |
[category] # Optional: tag and label colors |
| 73 |
``` |
| 74 |
|
| 75 |
### Derived tokens |
| 76 |
|
| 77 |
Interactive states are not authored. `resolve()` derives hover, active, |
| 78 |
selection, row striping, and contrast pairings perceptually in OKLab, so theme |
| 79 |
files stay small and every consuming app derives them identically rather than |
| 80 |
each recomputing its own. |
| 81 |
|
| 82 |
`intent_css_vars()` renders a resolved theme as a `:root { … }` block for web |
| 83 |
consumers; native consumers read RGB tuples off the same resolved tokens. |
| 84 |
|
| 85 |
### Theme ID |
| 86 |
|
| 87 |
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. |
| 88 |
|
| 89 |
## API |
| 90 |
|
| 91 |
|
| 92 |
|
| 93 |
| `list_themes_from_dirs(dirs)` | Scan directories for `.toml` files, return sorted `Vec<ThemeMeta>` | |
| 94 |
| `load_theme(dirs, id)` | Load a theme by ID, returning `ThemeColors` (metadata + color map) | |
| 95 |
| `find_theme_path(dirs, id)` | Find the file path for a theme ID (highest-priority directory wins) | |
| 96 |
| `parse_meta(id, table, is_custom)` | Parse `[meta]` from a TOML table into `ThemeMeta` | |
| 97 |
| `extract_colors(table)` | Flatten color sections into a `HashMap<String, String>` | |
| 98 |
| `validate_theme_id(id)` | Check that an ID contains only safe characters | |
| 99 |
| `bundled_themes_dir()` | The `themes/` directory this crate ships, for build-from-source fallback | |
| 100 |
| `embedded_themes()` | The shipped themes as `(id, toml_source)` pairs, embedded at compile time (no path needed) | |
| 101 |
| `parse_theme_str(id, source, is_custom)` | Parse a theme from a string, for use with `embedded_themes()` | |
| 102 |
| `resolve(theme)` | Resolve authored intents into the full token set, deriving interactive states | |
| 103 |
| `intent_css_vars(tokens)` | Render resolved tokens as a `:root { … }` CSS block | |
| 104 |
|
| 105 |
## Directory Priority |
| 106 |
|
| 107 |
`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`). |
| 108 |
|
| 109 |
Typical setup for a Tauri app: |
| 110 |
1. Bundled themes, packaged by the app or from `bundled_themes_dir()` (is_custom = false) |
| 111 |
2. User themes from an app data directory (is_custom = true) |
| 112 |
|
| 113 |
## License |
| 114 |
|
| 115 |
MIT. |
| 116 |
|
| 117 |
The themes in `themes/` adapt palettes from third-party projects (Catppuccin, Dracula, Nord, gruvbox, Tokyo Night, Rosé Pine, and others). Each upstream, its license, and the exact copyright line that license requires reproducing are recorded in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md), verified against the upstream LICENSE files themselves. Every adapted theme file also carries that information in a header comment, so credit travels with the file. |
| 118 |
|
| 119 |
If an attribution is wrong or you would prefer your work not be included, write to info@makenot.work and it will be corrected or removed. |
| 120 |
|