Skip to main content

max / makeover

Apply rustfmt across the crate Formatting only, no behavior change. cargo check --all-targets passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-21 23:46 UTC
Commit: 143bdeadda03480e9f0cf7da9739c3b10e0ec9f9
Parent: 2dee801
2 files changed, +233 insertions, -58 deletions
@@ -1,6 +1,10 @@
1 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");
2 + let id = std::env::args()
3 + .nth(1)
4 + .unwrap_or_else(|| "makenotwork".into());
5 + let dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
6 + .join("..")
7 + .join("themes");
4 8 let t = makeover::load_semantic(&[(dir, false)], &id).unwrap();
5 9 for (k, v) in &t.intents {
6 10 println!(" --{k}: {v};");
M src/lib.rs +227 -56
@@ -117,19 +117,31 @@ pub struct Oklab {
117 117
118 118 fn srgb_to_linear(c: u8) -> f32 {
119 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) }
120 + if c <= 0.04045 {
121 + c / 12.92
122 + } else {
123 + ((c + 0.055) / 1.055).powf(2.4)
124 + }
121 125 }
122 126
123 127 fn linear_to_srgb(c: f32) -> u8 {
124 128 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 };
129 + let v = if c <= 0.0031308 {
130 + c * 12.92
131 + } else {
132 + 1.055 * c.powf(1.0 / 2.4) - 0.055
133 + };
126 134 (v * 255.0).round().clamp(0.0, 255.0) as u8
127 135 }
128 136
129 137 impl Rgb {
130 138 /// Convert to OKLab (Ottosson's sRGB matrices).
131 139 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));
140 + let (r, g, b) = (
141 + srgb_to_linear(self.r),
142 + srgb_to_linear(self.g),
143 + srgb_to_linear(self.b),
144 + );
133 145 let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
134 146 let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
135 147 let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
@@ -170,9 +182,17 @@ pub fn wcag_contrast(a: Rgb, b: Rgb) -> f32 {
170 182 /// Pick black or white for legible text on `bg`, by the higher WCAG contrast
171 183 /// ratio (so the choice meets AA wherever the background allows it).
172 184 pub fn readable_on(bg: Rgb) -> Rgb {
173 - let white = Rgb { r: 255, g: 255, b: 255 };
185 + let white = Rgb {
186 + r: 255,
187 + g: 255,
188 + b: 255,
189 + };
174 190 let black = Rgb { r: 0, g: 0, b: 0 };
175 - if wcag_contrast(white, bg) >= wcag_contrast(black, bg) { white } else { black }
191 + if wcag_contrast(white, bg) >= wcag_contrast(black, bg) {
192 + white
193 + } else {
194 + black
195 + }
176 196 }
177 197
178 198 /// Shift OKLab lightness by `delta` (perceptually uniform). Positive lightens.
@@ -213,22 +233,86 @@ pub fn mix(a: Rgb, b: Rgb, t: f32) -> Rgb {
213 233 /// keeps it, which is the case that matters: a console app cannot fall back to
214 234 /// 24-bit color there.
215 235 pub const ANSI_16: [Rgb; 16] = [
216 - Rgb { r: 0x00, g: 0x00, b: 0x00 },
217 - Rgb { r: 0xaa, g: 0x00, b: 0x00 },
218 - Rgb { r: 0x00, g: 0xaa, b: 0x00 },
219 - Rgb { r: 0xaa, g: 0x55, b: 0x00 },
220 - Rgb { r: 0x00, g: 0x00, b: 0xaa },
221 - Rgb { r: 0xaa, g: 0x00, b: 0xaa },
222 - Rgb { r: 0x00, g: 0xaa, b: 0xaa },
223 - Rgb { r: 0xaa, g: 0xaa, b: 0xaa },
224 - Rgb { r: 0x55, g: 0x55, b: 0x55 },
225 - Rgb { r: 0xff, g: 0x55, b: 0x55 },
226 - Rgb { r: 0x55, g: 0xff, b: 0x55 },
227 - Rgb { r: 0xff, g: 0xff, b: 0x55 },
228 - Rgb { r: 0x55, g: 0x55, b: 0xff },
229 - Rgb { r: 0xff, g: 0x55, b: 0xff },
230 - Rgb { r: 0x55, g: 0xff, b: 0xff },
231 - Rgb { r: 0xff, g: 0xff, b: 0xff },
236 + Rgb {
237 + r: 0x00,
238 + g: 0x00,
239 + b: 0x00,
240 + },
241 + Rgb {
242 + r: 0xaa,
243 + g: 0x00,
244 + b: 0x00,
245 + },
246 + Rgb {
247 + r: 0x00,
248 + g: 0xaa,
249 + b: 0x00,
250 + },
251 + Rgb {
252 + r: 0xaa,
253 + g: 0x55,
254 + b: 0x00,
255 + },
256 + Rgb {
257 + r: 0x00,
258 + g: 0x00,
259 + b: 0xaa,
260 + },
261 + Rgb {
262 + r: 0xaa,
263 + g: 0x00,
264 + b: 0xaa,
265 + },
266 + Rgb {
267 + r: 0x00,
268 + g: 0xaa,
269 + b: 0xaa,
270 + },
271 + Rgb {
272 + r: 0xaa,
273 + g: 0xaa,
274 + b: 0xaa,
275 + },
276 + Rgb {
277 + r: 0x55,
278 + g: 0x55,
279 + b: 0x55,
280 + },
281 + Rgb {
282 + r: 0xff,
283 + g: 0x55,
284 + b: 0x55,
285 + },
286 + Rgb {
287 + r: 0x55,
288 + g: 0xff,
289 + b: 0x55,
290 + },
291 + Rgb {
292 + r: 0xff,
293 + g: 0xff,
294 + b: 0x55,
295 + },
296 + Rgb {
297 + r: 0x55,
298 + g: 0x55,
299 + b: 0xff,
300 + },
301 + Rgb {
302 + r: 0xff,
303 + g: 0x55,
304 + b: 0xff,
305 + },
306 + Rgb {
307 + r: 0x55,
308 + g: 0xff,
309 + b: 0xff,
310 + },
311 + Rgb {
312 + r: 0xff,
313 + g: 0xff,
314 + b: 0xff,
315 + },
232 316 ];
233 317
234 318 /// The contrast ratio two colors must clear to read as separate areas.
@@ -297,8 +381,7 @@ pub fn quantize_against(fg: Rgb, bg: Rgb, palette: &[Rgb]) -> usize {
297 381
298 382 let mut order: Vec<usize> = (0..palette.len()).collect();
299 383 order.sort_by(|a, b| {
300 - oklab_distance(fg, palette[*a])
301 - .total_cmp(&oklab_distance(fg, palette[*b]))
384 + oklab_distance(fg, palette[*a]).total_cmp(&oklab_distance(fg, palette[*b]))
302 385 });
303 386
304 387 order
@@ -310,8 +393,7 @@ pub fn quantize_against(fg: Rgb, bg: Rgb, palette: &[Rgb]) -> usize {
310 393 .iter()
311 394 .copied()
312 395 .max_by(|a, b| {
313 - wcag_contrast(palette[*a], shown)
314 - .total_cmp(&wcag_contrast(palette[*b], shown))
396 + wcag_contrast(palette[*a], shown).total_cmp(&wcag_contrast(palette[*b], shown))
315 397 })
316 398 .expect("the palette is not empty")
317 399 })
@@ -364,7 +446,10 @@ impl SemanticTokens {
364 446
365 447 /// Resolved RGB tuple for a token key (for egui / native consumers).
366 448 pub fn rgb(&self, key: &str) -> Option<(u8, u8, u8)> {
367 - self.intents.get(key).and_then(|h| Rgb::from_hex(h)).map(Rgb::tuple)
449 + self.intents
450 + .get(key)
451 + .and_then(|h| Rgb::from_hex(h))
452 + .map(Rgb::tuple)
368 453 }
369 454 }
370 455
@@ -408,7 +493,10 @@ pub fn resolve(theme: &ThemeColors) -> SemanticTokens {
408 493 let mut o = page.to_oklab();
409 494 o.l = 0.08;
410 495 let s = Rgb::from_oklab(o);
411 - intents.insert("overlay".into(), format!("rgba({}, {}, {}, 0.5)", s.r, s.g, s.b));
496 + intents.insert(
497 + "overlay".into(),
498 + format!("rgba({}, {}, {}, 0.5)", s.r, s.g, s.b),
499 + );
412 500 }
413 501 if let Some(sunken) = get(&intents, "surface-sunken") {
414 502 derived.push(("hover-surface".into(), sunken));
@@ -421,7 +509,10 @@ pub fn resolve(theme: &ThemeColors) -> SemanticTokens {
421 509 intents.insert(token, rgb.to_hex());
422 510 }
423 511
424 - SemanticTokens { meta: theme.meta.clone(), intents }
512 + SemanticTokens {
513 + meta: theme.meta.clone(),
514 + intents,
515 + }
425 516 }
426 517
427 518 /// Emit the resolved intent layer as CSS declarations (no selector), one
@@ -475,7 +566,12 @@ pub fn parse_meta(id: &str, table: &toml::Table, is_custom: bool) -> ThemeMeta {
475 566 .unwrap_or("dark")
476 567 .to_string();
477 568
478 - ThemeMeta { id: id.to_string(), name, variant, is_custom }
569 + ThemeMeta {
570 + id: id.to_string(),
571 + name,
572 + variant,
573 + is_custom,
574 + }
479 575 }
480 576
481 577 /// Extract the intent color sections into a flat `HashMap` with dotted keys
@@ -628,8 +724,7 @@ pub fn import_theme(source_path: &Path, custom_dir: &Path) -> Result<ThemeMeta,
628 724 .map_err(|e| format!("Failed to create {}: {}", custom_dir.display(), e))?;
629 725
630 726 let dest = custom_dir.join(format!("{}.toml", id));
631 - std::fs::copy(source_path, &dest)
632 - .map_err(|e| format!("Failed to copy theme: {}", e))?;
727 + std::fs::copy(source_path, &dest).map_err(|e| format!("Failed to copy theme: {}", e))?;
633 728
634 729 Ok(parse_meta(&id, &table, true))
635 730 }
@@ -646,8 +741,7 @@ pub fn delete_theme(custom_dir: &Path, id: &str) -> Result<(), String> {
646 741 return Err(format!("Custom theme '{}' not found", id));
647 742 }
648 743
649 - std::fs::remove_file(&path)
650 - .map_err(|e| format!("Failed to delete {}: {}", path.display(), e))
744 + std::fs::remove_file(&path).map_err(|e| format!("Failed to delete {}: {}", path.display(), e))
651 745 }
652 746
653 747 /// A four-color preview for theme thumbnails: the representative swatch from
@@ -705,8 +799,7 @@ pub fn export_theme(dirs: &[(PathBuf, bool)], id: &str, dest_path: &Path) -> Res
705 799 let (source, _) =
706 800 find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{}' not found", id))?;
707 801
708 - std::fs::copy(&source, dest_path)
709 - .map_err(|e| format!("Failed to export theme: {}", e))?;
802 + std::fs::copy(&source, dest_path).map_err(|e| format!("Failed to export theme: {}", e))?;
710 803
711 804 Ok(())
712 805 }
@@ -797,7 +890,11 @@ mod tests {
797 890 #[test]
798 891 fn quantize_picks_the_obvious_entry() {
799 892 let black = Rgb { r: 0, g: 0, b: 0 };
800 - let white = Rgb { r: 255, g: 255, b: 255 };
893 + let white = Rgb {
894 + r: 255,
895 + g: 255,
896 + b: 255,
897 + };
801 898 assert_eq!(quantize(black, &ANSI_16), 0);
802 899 assert_eq!(quantize(white, &ANSI_16), 15);
803 900 }
@@ -865,7 +962,9 @@ mod tests {
865 962
866 963 #[test]
867 964 fn parse_meta_with_name_and_variant() {
868 - let table: toml::Table = "[meta]\nname = \"Nord\"\nvariant = \"light\"\n".parse().unwrap();
965 + let table: toml::Table = "[meta]\nname = \"Nord\"\nvariant = \"light\"\n"
966 + .parse()
967 + .unwrap();
869 968 let meta = parse_meta("nord", &table, false);
870 969 assert_eq!(meta.id, "nord");
871 970 assert_eq!(meta.name, "Nord");
@@ -886,9 +985,31 @@ mod tests {
886 985
887 986 #[test]
888 987 fn rgb_hex_roundtrip() {
889 - assert_eq!(Rgb::from_hex("#6196FF").unwrap(), Rgb { r: 0x61, g: 0x96, b: 0xff });
890 - assert_eq!(Rgb::from_hex("#abc").unwrap(), Rgb { r: 0xaa, g: 0xbb, b: 0xcc });
891 - assert_eq!(Rgb { r: 0x61, g: 0x96, b: 0xff }.to_hex(), "#6196ff");
988 + assert_eq!(
989 + Rgb::from_hex("#6196FF").unwrap(),
990 + Rgb {
991 + r: 0x61,
992 + g: 0x96,
993 + b: 0xff
994 + }
995 + );
996 + assert_eq!(
997 + Rgb::from_hex("#abc").unwrap(),
998 + Rgb {
999 + r: 0xaa,
1000 + g: 0xbb,
1001 + b: 0xcc
1002 + }
1003 + );
1004 + assert_eq!(
1005 + Rgb {
1006 + r: 0x61,
1007 + g: 0x96,
1008 + b: 0xff
1009 + }
1010 + .to_hex(),
1011 + "#6196ff"
1012 + );
892 1013 assert!(Rgb::from_hex("not-a-color").is_none());
893 1014 }
894 1015
@@ -906,7 +1027,11 @@ mod tests {
906 1027
907 1028 #[test]
908 1029 fn wcag_contrast_known_pairs() {
909 - let white = Rgb { r: 255, g: 255, b: 255 };
1030 + let white = Rgb {
1031 + r: 255,
1032 + g: 255,
1033 + b: 255,
1034 + };
910 1035 let black = Rgb { r: 0, g: 0, b: 0 };
911 1036 assert!((wcag_contrast(white, black) - 21.0).abs() < 0.01);
912 1037 assert!((wcag_contrast(white, white) - 1.0).abs() < 0.01);
@@ -914,8 +1039,22 @@ mod tests {
914 1039
915 1040 #[test]
916 1041 fn readable_on_picks_by_wcag() {
917 - assert_eq!(readable_on(Rgb { r: 255, g: 255, b: 255 }), Rgb { r: 0, g: 0, b: 0 });
918 - assert_eq!(readable_on(Rgb { r: 0, g: 0, b: 0 }), Rgb { r: 255, g: 255, b: 255 });
1042 + assert_eq!(
1043 + readable_on(Rgb {
1044 + r: 255,
1045 + g: 255,
1046 + b: 255
1047 + }),
1048 + Rgb { r: 0, g: 0, b: 0 }
1049 + );
1050 + assert_eq!(
1051 + readable_on(Rgb { r: 0, g: 0, b: 0 }),
1052 + Rgb {
1053 + r: 255,
1054 + g: 255,
1055 + b: 255
1056 + }
1057 + );
919 1058 // A light blue action -> black text reads better.
920 1059 let action = Rgb::from_hex("#6196ff").unwrap();
921 1060 assert_eq!(readable_on(action), Rgb { r: 0, g: 0, b: 0 });
@@ -1014,8 +1153,14 @@ six = "#88c0d0"
1014 1153 let action = Rgb::from_hex("#81a1c1").unwrap();
1015 1154 let page = Rgb::from_hex("#2e3440").unwrap();
1016 1155 let _ = page;
1017 - assert_eq!(t.hex("action-hover").unwrap(), lighten(action, 0.05).to_hex());
1018 - assert_eq!(t.hex("content-on-action").unwrap(), readable_on(action).to_hex());
1156 + assert_eq!(
1157 + t.hex("action-hover").unwrap(),
1158 + lighten(action, 0.05).to_hex()
1159 + );
1160 + assert_eq!(
1161 + t.hex("content-on-action").unwrap(),
1162 + readable_on(action).to_hex()
1163 + );
1019 1164 assert_eq!(t.hex("focus-ring"), Some("#81a1c1"));
1020 1165 assert_eq!(t.hex("hover-surface"), Some("#434c5e")); // = surface.sunken
1021 1166 // Pruned by the usage audit (0 consumers): action-active, the *-surface
@@ -1032,12 +1177,21 @@ six = "#88c0d0"
1032 1177 let theme = parse_theme_str("nord", nord_toml(), false).unwrap();
1033 1178 let t = resolve(&theme);
1034 1179 let overlay = t.hex("overlay").unwrap();
1035 - assert!(overlay.starts_with("rgba("), "overlay is translucent: {overlay}");
1180 + assert!(
1181 + overlay.starts_with("rgba("),
1182 + "overlay is translucent: {overlay}"
1183 + );
1036 1184 assert!(overlay.ends_with(", 0.5)"));
1037 1185 // The scrim tone is anchored very dark regardless of theme.
1038 - let inner = overlay.trim_start_matches("rgba(").trim_end_matches(", 0.5)");
1186 + let inner = overlay
1187 + .trim_start_matches("rgba(")
1188 + .trim_end_matches(", 0.5)");
1039 1189 let parts: Vec<u8> = inner.split(", ").map(|p| p.parse().unwrap()).collect();
1040 - let scrim = Rgb { r: parts[0], g: parts[1], b: parts[2] };
1190 + let scrim = Rgb {
1191 + r: parts[0],
1192 + g: parts[1],
1193 + b: parts[2],
1194 + };
1041 1195 assert!(scrim.to_oklab().l < 0.2, "scrim must be near-black");
1042 1196 }
1043 1197
@@ -1053,7 +1207,10 @@ six = "#88c0d0"
1053 1207 )
1054 1208 .unwrap();
1055 1209 let t = resolve(&theme);
1056 - assert!(t.hex("surface-page").is_none(), "non-hex base intent leaked");
1210 + assert!(
1211 + t.hex("surface-page").is_none(),
1212 + "non-hex base intent leaked"
1213 + );
1057 1214 assert_eq!(t.hex("content").unwrap(), "#111111");
1058 1215 // The injected markup appears in no resolved value.
1059 1216 assert!(!t.intents.values().any(|v| v.contains('<')));
@@ -1072,7 +1229,10 @@ six = "#88c0d0"
1072 1229 assert!(t.hex("action").is_none());
1073 1230 assert!(t.hex("action-hover").is_none());
1074 1231 assert!(t.hex("selection").is_none());
1075 - assert_eq!(t.hex("border-strong").unwrap(), darken(Rgb::from_hex("#222222").unwrap(), 0.05).to_hex());
1232 + assert_eq!(
1233 + t.hex("border-strong").unwrap(),
1234 + darken(Rgb::from_hex("#222222").unwrap(), 0.05).to_hex()
1235 + );
1076 1236 }
1077 1237
1078 1238 #[test]
@@ -1130,7 +1290,10 @@ six = "#88c0d0"
1130 1290 let d2 = tempfile::tempdir().unwrap();
1131 1291 fs::write(d1.path().join("s.toml"), "[meta]\n").unwrap();
1132 1292 fs::write(d2.path().join("s.toml"), "[meta]\n").unwrap();
1133 - let dirs = vec![(d1.path().to_path_buf(), false), (d2.path().to_path_buf(), true)];
1293 + let dirs = vec![
1294 + (d1.path().to_path_buf(), false),
1295 + (d2.path().to_path_buf(), true),
1296 + ];
1134 1297 let (path, is_custom) = find_theme_path(&dirs, "s").unwrap();
1135 1298 assert!(is_custom);
1136 1299 assert_eq!(path, d2.path().join("s.toml"));
@@ -1229,8 +1392,13 @@ six = "#88c0d0"
1229 1392 fn adapted_themes_carry_inline_attribution() {
1230 1393 // Each adapted file must name its upstream in-file, so the credit
1231 1394 // survives someone copying a single .toml out of the crate.
1232 - const ORIGINALS: [&str; 5] =
1233 - ["makenotwork", "goingson", "audiofiles", "high-contrast", "neobrute"];
1395 + const ORIGINALS: [&str; 5] = [
1396 + "makenotwork",
1397 + "goingson",
1398 + "audiofiles",
1399 + "high-contrast",
1400 + "neobrute",
1401 + ];
1234 1402 for (id, source) in embedded_themes() {
1235 1403 if ORIGINALS.contains(&id) {
1236 1404 continue;
@@ -1259,8 +1427,7 @@ six = "#88c0d0"
1259 1427 Some(path.file_stem()?.to_str()?.to_string())
1260 1428 })
1261 1429 .collect();
1262 - let mut embedded: Vec<String> =
1263 - embedded_themes().map(|(id, _)| id.to_string()).collect();
1430 + let mut embedded: Vec<String> = embedded_themes().map(|(id, _)| id.to_string()).collect();
1264 1431 on_disk.sort();
1265 1432 embedded.sort();
1266 1433 assert_eq!(embedded, on_disk, "embedded theme set drifted from themes/");
@@ -1287,7 +1454,11 @@ six = "#88c0d0"
1287 1454 let dir = bundled_themes_dir().unwrap();
1288 1455 let dirs = vec![(dir.clone(), false)];
1289 1456 let themes = list_themes_from_dirs(&dirs);
1290 - assert!(themes.len() >= 30, "expected the full theme set, got {}", themes.len());
1457 + assert!(
1458 + themes.len() >= 30,
1459 + "expected the full theme set, got {}",
1460 + themes.len()
1461 + );
1291 1462 for meta in &themes {
1292 1463 load_theme(&dirs, &meta.id)
1293 1464 .unwrap_or_else(|e| panic!("shipped theme `{}` failed to load: {e}", meta.id));