1use std::fmt;
2use std::path::{Path, PathBuf};
3
4const ITERM2_REPO_URL: &str =
5 "https://raw.githubusercontent.com/mbadolato/iTerm2-Color-Schemes/master/schemes";
6const ITERM2_API_URL: &str =
7 "https://api.github.com/repos/mbadolato/iTerm2-Color-Schemes/contents/schemes";
8
9pub const ITERM2_GALLERY_URL: &str = "https://iterm2colorschemes.com/";
10
11static VARIANT_PAIRS: &[(&str, &str)] = &[
12 ("3024 Night", "3024 Day"),
13 ("Aizen Dark", "Aizen Light"),
14 ("Atom One Dark", "Atom One Light"),
15 ("Belafonte Night", "Belafonte Day"),
16 ("Bluloco Dark", "Bluloco Light"),
17 ("Builtin Dark", "Builtin Light"),
18 ("Builtin Tango Dark", "Builtin Tango Light"),
19 ("Farmhouse Dark", "Farmhouse Light"),
20 ("Flexoki Dark", "Flexoki Light"),
21 ("GitHub Dark", "GitHub"),
22 ("GitHub Dark Colorblind", "GitHub Light Colorblind"),
23 ("GitHub Dark Default", "GitHub Light Default"),
24 ("GitHub Dark High Contrast", "GitHub Light High Contrast"),
25 ("GitLab Dark", "GitLab Light"),
26 ("Gruvbox Dark", "Gruvbox Light"),
27 ("Gruvbox Dark Hard", "Gruvbox Light Hard"),
28 ("Gruvbox Material Dark", "Gruvbox Material Light"),
29 ("Iceberg Dark", "Iceberg Light"),
30 ("Melange Dark", "Melange Light"),
31 ("Neobones Dark", "Neobones Light"),
32 ("Nvim Dark", "Nvim Light"),
33 ("One Double Dark", "One Double Light"),
34 ("One Half Dark", "One Half Light"),
35 ("Pencil Dark", "Pencil Light"),
36 ("Raycast Dark", "Raycast Light"),
37 ("Selenized Dark", "Selenized Light"),
38 ("Seoulbones Dark", "Seoulbones Light"),
39 ("Tinacious Design Dark", "Tinacious Design Light"),
40 ("Violet Dark", "Violet Light"),
41 ("Xcode Dark", "Xcode Light"),
42 ("Xcode Dark hc", "Xcode Light hc"),
43 ("Zenbones Dark", "Zenbones Light"),
44 ("Zenwritten Dark", "Zenwritten Light"),
45 ("iTerm2 Dark Background", "iTerm2 Light Background"),
46 ("iTerm2 Solarized Dark", "iTerm2 Solarized Light"),
47 ("iTerm2 Tango Dark", "iTerm2 Tango Light"),
48 ("Adwaita Dark", "Adwaita"),
49 ("Night Owl", "Light Owl"),
50 ("Nord", "Nord Light"),
51 ("Onenord", "Onenord Light"),
52 ("Pro", "Pro Light"),
53 ("Terminal Basic Dark", "Terminal Basic"),
54 ("No Clown Fiesta", "No Clown Fiesta Light"),
55 ("Rose Pine Moon", "Rose Pine Dawn"),
56 ("Rose Pine", "Rose Pine Dawn"),
57 ("TokyoNight Night", "TokyoNight Day"),
58 ("TokyoNight Moon", "TokyoNight Day"),
59 ("TokyoNight Storm", "TokyoNight Day"),
60 ("TokyoNight", "TokyoNight Day"),
61 ("Ayu", "Ayu Light"),
62 ("Ayu Mirage", "Ayu Light"),
63 ("Everforest Dark Hard", "Everforest Light Med"),
64 ("Tomorrow Night", "Tomorrow"),
65 ("Tomorrow Night Blue", "Tomorrow"),
66 ("Tomorrow Night Bright", "Tomorrow"),
67 ("Tomorrow Night Burns", "Tomorrow"),
68 ("Tomorrow Night Eighties", "Tomorrow"),
69 ("Catppuccin Frappe", "Catppuccin Latte"),
70 ("Catppuccin Macchiato", "Catppuccin Latte"),
71 ("Catppuccin Mocha", "Catppuccin Latte"),
72];
73
74pub fn lookup_variant_pair(name: &str) -> Option<(&'static str, &'static str)> {
75 let lower = name.to_lowercase();
76 for &(dark, light) in VARIANT_PAIRS {
77 if dark.to_lowercase() == lower || light.to_lowercase() == lower {
78 return Some((dark, light));
79 }
80 }
81 None
82}
83
84#[derive(Debug)]
85pub enum Iterm2Error {
86 NetworkError(String),
87 ParseError(String),
88 NotFound(String),
89 IoError(String),
90}
91
92impl fmt::Display for Iterm2Error {
93 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 match self {
95 Self::NetworkError(msg) => write!(f, "Network error: {}", msg),
96 Self::ParseError(msg) => write!(f, "Parse error: {}", msg),
97 Self::NotFound(msg) => write!(f, "Not found: {}", msg),
98 Self::IoError(msg) => write!(f, "IO error: {}", msg),
99 }
100 }
101}
102
103impl std::error::Error for Iterm2Error {}
104
105const MIN_CONTRAST_RATIO: f64 = 4.6;
106
107#[derive(Debug, Clone)]
108pub struct Iterm2Color {
109 pub r: f64,
110 pub g: f64,
111 pub b: f64,
112}
113
114impl Iterm2Color {
115 pub fn to_hex(&self) -> String {
116 let r = (self.r * 255.0).round() as u8;
117 let g = (self.g * 255.0).round() as u8;
118 let b = (self.b * 255.0).round() as u8;
119 format!("#{:02x}{:02x}{:02x}", r, g, b)
120 }
121
122 fn blend(&self, other: &Iterm2Color, ratio: f64) -> Iterm2Color {
123 Iterm2Color {
124 r: self.r * (1.0 - ratio) + other.r * ratio,
125 g: self.g * (1.0 - ratio) + other.g * ratio,
126 b: self.b * (1.0 - ratio) + other.b * ratio,
127 }
128 }
129
130 fn linearize(val: f64) -> f64 {
131 if val <= 0.03928 {
132 val / 12.92
133 } else {
134 ((val + 0.055) / 1.055).powf(2.4)
135 }
136 }
137
138 fn luminance(&self) -> f64 {
139 0.2126 * Self::linearize(self.r)
140 + 0.7152 * Self::linearize(self.g)
141 + 0.0722 * Self::linearize(self.b)
142 }
143
144 fn contrast_ratio(&self, other: &Iterm2Color) -> f64 {
145 let l1 = self.luminance();
146 let l2 = other.luminance();
147 let lighter = l1.max(l2);
148 let darker = l1.min(l2);
149 (lighter + 0.05) / (darker + 0.05)
150 }
151
152 fn lighten(&self, amount: f64) -> Iterm2Color {
153 Iterm2Color {
154 r: self.r + (1.0 - self.r) * amount,
155 g: self.g + (1.0 - self.g) * amount,
156 b: self.b + (1.0 - self.b) * amount,
157 }
158 }
159
160 fn darken(&self, amount: f64) -> Iterm2Color {
161 Iterm2Color {
162 r: self.r * (1.0 - amount),
163 g: self.g * (1.0 - amount),
164 b: self.b * (1.0 - amount),
165 }
166 }
167
168 fn ensure_contrast(&self, bg: &Iterm2Color, min_ratio: f64) -> Iterm2Color {
169 if self.contrast_ratio(bg) >= min_ratio {
170 return self.clone();
171 }
172
173 let lighten_result = self.adjust_for_contrast(bg, min_ratio, true);
174 let darken_result = self.adjust_for_contrast(bg, min_ratio, false);
175
176 let lighten_passes = lighten_result.contrast_ratio(bg) >= min_ratio;
177 let darken_passes = darken_result.contrast_ratio(bg) >= min_ratio;
178
179 match (lighten_passes, darken_passes) {
180 (true, false) => lighten_result,
181 (false, true) => darken_result,
182 (true, true) => {
183 let lighten_dist = self.color_distance(&lighten_result);
184 let darken_dist = self.color_distance(&darken_result);
185 if lighten_dist <= darken_dist {
186 lighten_result
187 } else {
188 darken_result
189 }
190 }
191 (false, false) => {
192 if lighten_result.contrast_ratio(bg) > darken_result.contrast_ratio(bg) {
193 lighten_result
194 } else {
195 darken_result
196 }
197 }
198 }
199 }
200
201 fn adjust_for_contrast(&self, bg: &Iterm2Color, min_ratio: f64, lighten: bool) -> Iterm2Color {
202 let mut low = 0.0;
203 let mut high = 1.0;
204 let mut best = self.clone();
205
206 for _ in 0..20 {
207 let mid = (low + high) / 2.0;
208 let adjusted = if lighten {
209 self.lighten(mid)
210 } else {
211 self.darken(mid)
212 };
213
214 if adjusted.contrast_ratio(bg) >= min_ratio {
215 best = adjusted;
216 high = mid;
217 } else {
218 low = mid;
219 }
220 }
221
222 best
223 }
224
225 fn color_distance(&self, other: &Iterm2Color) -> f64 {
226 let dr = self.r - other.r;
227 let dg = self.g - other.g;
228 let db = self.b - other.b;
229 (dr * dr + dg * dg + db * db).sqrt()
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq)]
234pub enum SchemeVariant {
235 Dark,
236 Light,
237 Unknown,
238}
239
240#[derive(Debug)]
241pub struct Iterm2Scheme {
242 pub background: Iterm2Color,
243 pub foreground: Iterm2Color,
244 pub selection_bg: Iterm2Color,
245 pub selection_fg: Iterm2Color,
246 pub ansi: [Iterm2Color; 16],
247}
248
249fn detect_variant(name: &str) -> SchemeVariant {
250 if let Some((dark, light)) = lookup_variant_pair(name) {
251 let lower = name.to_lowercase();
252 if dark.to_lowercase() == lower {
253 return SchemeVariant::Dark;
254 } else if light.to_lowercase() == lower {
255 return SchemeVariant::Light;
256 }
257 }
258
259 let lower = name.to_lowercase();
260 if lower.contains("light") || lower.contains("day") || lower.contains("dawn") {
261 SchemeVariant::Light
262 } else if lower.contains("dark") || lower.contains("night") || lower.contains("moon") {
263 SchemeVariant::Dark
264 } else {
265 SchemeVariant::Unknown
266 }
267}
268
269fn find_counterpart_name(name: &str) -> Option<String> {
270 if let Some((dark, light)) = lookup_variant_pair(name) {
271 let lower = name.to_lowercase();
272 if dark.to_lowercase() == lower {
273 return Some(light.to_string());
274 } else {
275 return Some(dark.to_string());
276 }
277 }
278
279 let variant = detect_variant(name);
280 match variant {
281 SchemeVariant::Dark => {
282 let lower = name.to_lowercase();
283 if let Some(pos) = lower.find("dark") {
284 let mut result = name.to_string();
285 let replacement = if &name[pos..pos + 4] == "Dark" {
286 "Light"
287 } else {
288 "light"
289 };
290 result.replace_range(pos..pos + 4, replacement);
291 Some(result)
292 } else {
293 None
294 }
295 }
296 SchemeVariant::Light => {
297 let lower = name.to_lowercase();
298 if let Some(pos) = lower.find("light") {
299 let mut result = name.to_string();
300 let replacement = if &name[pos..pos + 5] == "Light" {
301 "Dark"
302 } else {
303 "dark"
304 };
305 result.replace_range(pos..pos + 5, replacement);
306 Some(result)
307 } else {
308 None
309 }
310 }
311 SchemeVariant::Unknown => None,
312 }
313}
314
315fn find_variant_names(name: &str) -> Vec<String> {
316 let variant = detect_variant(name);
317
318 if variant == SchemeVariant::Unknown {
319 vec![
320 format!("{} Light", name),
321 format!("{} Dark", name),
322 format!("{}-light", name),
323 format!("{}-dark", name),
324 ]
325 } else {
326 vec![]
327 }
328}
329
330fn parse_color_dict(dict: &plist::Dictionary) -> Option<Iterm2Color> {
331 let r = dict.get("Red Component")?.as_real()?;
332 let g = dict.get("Green Component")?.as_real()?;
333 let b = dict.get("Blue Component")?.as_real()?;
334 Some(Iterm2Color { r, g, b })
335}
336
337fn extract_color(root: &plist::Dictionary, key: &str) -> Option<Iterm2Color> {
338 let dict = root.get(key)?.as_dictionary()?;
339 parse_color_dict(dict)
340}
341
342pub fn parse_scheme(plist_content: &[u8]) -> Result<Iterm2Scheme, Iterm2Error> {
343 let value: plist::Value =
344 plist::from_bytes(plist_content).map_err(|e| Iterm2Error::ParseError(e.to_string()))?;
345
346 let root = value
347 .as_dictionary()
348 .ok_or_else(|| Iterm2Error::ParseError("Expected dictionary at root".to_string()))?;
349
350 let background = extract_color(root, "Background Color")
351 .ok_or_else(|| Iterm2Error::ParseError("Missing Background Color".to_string()))?;
352
353 let foreground = extract_color(root, "Foreground Color")
354 .ok_or_else(|| Iterm2Error::ParseError("Missing Foreground Color".to_string()))?;
355
356 let selection_bg = extract_color(root, "Selection Color")
357 .unwrap_or_else(|| background.blend(&foreground, 0.3));
358
359 let selection_fg =
360 extract_color(root, "Selected Text Color").unwrap_or_else(|| foreground.clone());
361
362 let mut ansi = Vec::with_capacity(16);
363 for i in 0..16 {
364 let key = format!("Ansi {} Color", i);
365 let color = extract_color(root, &key).unwrap_or(if i < 8 {
366 Iterm2Color {
367 r: 0.5,
368 g: 0.5,
369 b: 0.5,
370 }
371 } else {
372 Iterm2Color {
373 r: 0.7,
374 g: 0.7,
375 b: 0.7,
376 }
377 });
378 ansi.push(color);
379 }
380
381 Ok(Iterm2Scheme {
382 background,
383 foreground,
384 selection_bg,
385 selection_fg,
386 ansi: ansi.try_into().unwrap(),
387 })
388}
389
390impl Iterm2Scheme {
391 fn to_colors_toml(&self) -> String {
392 let bg = &self.background;
393 let fg = self.foreground.ensure_contrast(bg, MIN_CONTRAST_RATIO);
394 let dialog_bg = bg.blend(&self.ansi[8], 0.15);
395 let border_color = bg.blend(&self.ansi[8], 0.4);
396
397 let accent = self.ansi[4]
398 .ensure_contrast(bg, MIN_CONTRAST_RATIO)
399 .ensure_contrast(&dialog_bg, MIN_CONTRAST_RATIO);
400 let accent_secondary = self.ansi[5]
401 .ensure_contrast(bg, MIN_CONTRAST_RATIO)
402 .ensure_contrast(&dialog_bg, MIN_CONTRAST_RATIO);
403 let highlight = self.ansi[3].ensure_contrast(bg, MIN_CONTRAST_RATIO);
404 let success = self.ansi[2].ensure_contrast(bg, MIN_CONTRAST_RATIO);
405 let danger = self.ansi[1].ensure_contrast(bg, MIN_CONTRAST_RATIO);
406
407 let muted = self.derive_muted_color(bg);
408 let warning = self.derive_warning_color(bg);
409
410 let graph_line = accent.clone();
411
412 let selection_fg = self
413 .selection_fg
414 .ensure_contrast(&self.selection_bg, MIN_CONTRAST_RATIO);
415
416 format!(
417 r##"bg = "{}"
418dialog_bg = "{}"
419fg = "{}"
420accent = "{}"
421accent_secondary = "{}"
422highlight = "{}"
423muted = "{}"
424success = "{}"
425warning = "{}"
426danger = "{}"
427border = "{}"
428selection_bg = "{}"
429selection_fg = "{}"
430graph_line = "{}""##,
431 bg.to_hex(),
432 dialog_bg.to_hex(),
433 fg.to_hex(),
434 accent.to_hex(),
435 accent_secondary.to_hex(),
436 highlight.to_hex(),
437 muted.to_hex(),
438 success.to_hex(),
439 warning.to_hex(),
440 danger.to_hex(),
441 border_color.to_hex(),
442 self.selection_bg.to_hex(),
443 selection_fg.to_hex(),
444 graph_line.to_hex(),
445 )
446 }
447
448 fn derive_muted_color(&self, bg: &Iterm2Color) -> Iterm2Color {
449 let candidates = [&self.ansi[8], &self.foreground.blend(bg, 0.5)];
450
451 for candidate in candidates {
452 let adjusted = candidate.ensure_contrast(bg, MIN_CONTRAST_RATIO);
453 if adjusted.contrast_ratio(bg) >= MIN_CONTRAST_RATIO {
454 return adjusted;
455 }
456 }
457
458 self.foreground.blend(bg, 0.4)
459 }
460
461 fn derive_warning_color(&self, bg: &Iterm2Color) -> Iterm2Color {
462 let candidates = [&self.ansi[11], &self.ansi[3], &self.ansi[9]];
463
464 for candidate in candidates {
465 let adjusted = candidate.ensure_contrast(bg, MIN_CONTRAST_RATIO);
466 if adjusted.contrast_ratio(bg) >= MIN_CONTRAST_RATIO {
467 return adjusted;
468 }
469 }
470
471 self.ansi[3].ensure_contrast(bg, MIN_CONTRAST_RATIO)
472 }
473}
474
475pub struct ImportResult {
476 pub path: PathBuf,
477 pub dark_source: Option<String>,
478 pub light_source: Option<String>,
479}
480
481fn try_fetch_scheme(name: &str) -> Option<Iterm2Scheme> {
482 let url = format!("{}/{}.itermcolors", ITERM2_REPO_URL, name);
483
484 let response = ureq::get(&url).call().ok()?;
485
486 let bytes = response.into_body().read_to_vec().ok()?;
487
488 parse_scheme(&bytes).ok()
489}
490
491pub fn fetch_scheme(name: &str) -> Result<Iterm2Scheme, Iterm2Error> {
492 let url = format!("{}/{}.itermcolors", ITERM2_REPO_URL, name);
493
494 let response = ureq::get(&url)
495 .call()
496 .map_err(|e| Iterm2Error::NetworkError(e.to_string()))?;
497
498 if response.status() == 404 {
499 return Err(Iterm2Error::NotFound(format!(
500 "Scheme '{}' not found. Browse available themes at: {}",
501 name, ITERM2_GALLERY_URL
502 )));
503 }
504
505 let bytes = response
506 .into_body()
507 .read_to_vec()
508 .map_err(|e| Iterm2Error::NetworkError(e.to_string()))?;
509
510 parse_scheme(&bytes)
511}
512
513#[derive(Debug, serde::Deserialize)]
514struct GitHubFile {
515 name: String,
516 #[serde(rename = "type")]
517 file_type: String,
518}
519
520pub fn list_available_schemes() -> Result<Vec<String>, Iterm2Error> {
521 let response = ureq::get(ITERM2_API_URL)
522 .header("User-Agent", "jolt-theme-importer")
523 .call()
524 .map_err(|e: ureq::Error| Iterm2Error::NetworkError(e.to_string()))?;
525
526 let body = response
527 .into_body()
528 .read_to_string()
529 .map_err(|e: ureq::Error| Iterm2Error::NetworkError(e.to_string()))?;
530
531 let files: Vec<GitHubFile> =
532 serde_json::from_str(&body).map_err(|e| Iterm2Error::ParseError(e.to_string()))?;
533
534 let schemes: Vec<String> = files
535 .into_iter()
536 .filter(|f| f.file_type == "file" && f.name.ends_with(".itermcolors"))
537 .map(|f| f.name.trim_end_matches(".itermcolors").to_string())
538 .collect();
539
540 Ok(schemes)
541}
542
543fn derive_base_name(name: &str) -> String {
544 let lower = name.to_lowercase();
545
546 for suffix in [" light", " dark", "-light", "-dark"] {
547 if lower.ends_with(suffix) {
548 return name[..name.len() - suffix.len()].to_string();
549 }
550 }
551
552 name.to_string()
553}
554
555pub fn import_scheme(
556 name: &str,
557 custom_name: Option<&str>,
558 themes_dir: &Path,
559) -> Result<ImportResult, Iterm2Error> {
560 let primary = fetch_scheme(name)?;
561 let primary_variant = detect_variant(name);
562
563 let mut dark_scheme: Option<Iterm2Scheme> = None;
564 let mut light_scheme: Option<Iterm2Scheme> = None;
565 let mut dark_source: Option<String> = None;
566 let mut light_source: Option<String> = None;
567
568 match primary_variant {
569 SchemeVariant::Dark => {
570 dark_scheme = Some(primary);
571 dark_source = Some(name.to_string());
572
573 if let Some(counterpart) = find_counterpart_name(name) {
574 if let Some(light) = try_fetch_scheme(&counterpart) {
575 light_scheme = Some(light);
576 light_source = Some(counterpart);
577 }
578 }
579 }
580 SchemeVariant::Light => {
581 light_scheme = Some(primary);
582 light_source = Some(name.to_string());
583
584 if let Some(counterpart) = find_counterpart_name(name) {
585 if let Some(dark) = try_fetch_scheme(&counterpart) {
586 dark_scheme = Some(dark);
587 dark_source = Some(counterpart);
588 }
589 }
590 }
591 SchemeVariant::Unknown => {
592 dark_scheme = Some(primary);
593 dark_source = Some(name.to_string());
594
595 for variant_name in find_variant_names(name) {
596 if let Some(scheme) = try_fetch_scheme(&variant_name) {
597 let variant = detect_variant(&variant_name);
598 if variant == SchemeVariant::Light && light_scheme.is_none() {
599 light_scheme = Some(scheme);
600 light_source = Some(variant_name);
601 break;
602 }
603 }
604 }
605 }
606 }
607
608 let base_name = custom_name
609 .map(|s| s.to_string())
610 .unwrap_or_else(|| derive_base_name(name));
611
612 let file_name = base_name.to_lowercase().replace(' ', "-");
613
614 let mut toml_content = format!("name = \"{}\"\n", base_name);
615
616 if let Some(ref dark) = dark_scheme {
617 toml_content.push_str("\n[dark]\n");
618 toml_content.push_str(&dark.to_colors_toml());
619 toml_content.push('\n');
620 }
621
622 if let Some(ref light) = light_scheme {
623 toml_content.push_str("\n[light]\n");
624 toml_content.push_str(&light.to_colors_toml());
625 toml_content.push('\n');
626 }
627
628 std::fs::create_dir_all(themes_dir).map_err(|e| Iterm2Error::IoError(e.to_string()))?;
629
630 let theme_path = themes_dir.join(format!("{}.toml", file_name));
631
632 std::fs::write(&theme_path, toml_content).map_err(|e| Iterm2Error::IoError(e.to_string()))?;
633
634 Ok(ImportResult {
635 path: theme_path,
636 dark_source,
637 light_source,
638 })
639}
640
641pub fn search_schemes(query: &str) -> Result<Vec<String>, Iterm2Error> {
642 let all_schemes = list_available_schemes()?;
643 let query_lower = query.to_lowercase();
644
645 let matches: Vec<String> = all_schemes
646 .into_iter()
647 .filter(|s| s.to_lowercase().contains(&query_lower))
648 .collect();
649
650 Ok(matches)
651}
652
653pub fn find_variant_suggestions(
654 name: &str,
655 target_variant: SchemeVariant,
656) -> Result<Vec<String>, Iterm2Error> {
657 let base_name = derive_base_name(name);
658 let all_schemes = list_available_schemes()?;
659 let base_lower = base_name.to_lowercase();
660
661 let mut suggestions: Vec<String> = all_schemes
662 .into_iter()
663 .filter(|s| {
664 let s_lower = s.to_lowercase();
665 if s_lower == name.to_lowercase() {
666 return false;
667 }
668
669 let matches_base = s_lower.contains(&base_lower)
670 || base_lower.contains(&derive_base_name(s).to_lowercase());
671
672 if !matches_base {
673 return false;
674 }
675
676 let variant = detect_variant(s);
677 variant == target_variant || variant == SchemeVariant::Unknown
678 })
679 .collect();
680
681 suggestions.sort();
682 Ok(suggestions)
683}
684
685#[cfg(test)]
686mod tests {
687 use super::*;
688
689 #[test]
690 fn test_color_to_hex() {
691 let color = Iterm2Color {
692 r: 1.0,
693 g: 0.5,
694 b: 0.0,
695 };
696 assert_eq!(color.to_hex(), "#ff8000");
697 }
698
699 #[test]
700 fn test_color_blend() {
701 let black = Iterm2Color {
702 r: 0.0,
703 g: 0.0,
704 b: 0.0,
705 };
706 let white = Iterm2Color {
707 r: 1.0,
708 g: 1.0,
709 b: 1.0,
710 };
711 let gray = black.blend(&white, 0.5);
712 assert!((gray.r - 0.5).abs() < 0.01);
713 assert!((gray.g - 0.5).abs() < 0.01);
714 assert!((gray.b - 0.5).abs() < 0.01);
715 }
716
717 #[test]
718 fn test_detect_variant() {
719 assert_eq!(detect_variant("Gruvbox Dark"), SchemeVariant::Dark);
720 assert_eq!(detect_variant("Gruvbox Light"), SchemeVariant::Light);
721 assert_eq!(detect_variant("Dracula"), SchemeVariant::Unknown);
722 assert_eq!(detect_variant("One Dark"), SchemeVariant::Dark);
723 assert_eq!(detect_variant("Solarized Light"), SchemeVariant::Light);
724 }
725
726 #[test]
727 fn test_find_counterpart_name() {
728 assert_eq!(
729 find_counterpart_name("Gruvbox Dark"),
730 Some("Gruvbox Light".to_string())
731 );
732 assert_eq!(
733 find_counterpart_name("Gruvbox Light"),
734 Some("Gruvbox Dark".to_string())
735 );
736 assert_eq!(
737 find_counterpart_name("Gruvbox Dark Hard"),
738 Some("Gruvbox Light Hard".to_string())
739 );
740 assert_eq!(find_counterpart_name("Dracula"), None);
741 }
742
743 #[test]
744 fn test_derive_base_name() {
745 assert_eq!(derive_base_name("Gruvbox Dark"), "Gruvbox");
746 assert_eq!(derive_base_name("Gruvbox Light"), "Gruvbox");
747 assert_eq!(derive_base_name("Gruvbox Dark Hard"), "Gruvbox Dark Hard");
748 assert_eq!(derive_base_name("One Dark"), "One");
749 assert_eq!(derive_base_name("Dracula"), "Dracula");
750 }
751
752 #[test]
753 fn test_lookup_variant_pair() {
754 let mocha = lookup_variant_pair("Catppuccin Mocha");
755 assert!(mocha.is_some());
756 assert_eq!(mocha.unwrap().1, "Catppuccin Latte");
757
758 let latte = lookup_variant_pair("Catppuccin Latte");
759 assert!(latte.is_some());
760 assert_eq!(latte.unwrap().1, "Catppuccin Latte");
761
762 assert_eq!(
763 lookup_variant_pair("Tomorrow Night"),
764 Some(("Tomorrow Night", "Tomorrow"))
765 );
766 assert_eq!(lookup_variant_pair("Nord"), Some(("Nord", "Nord Light")));
767 assert_eq!(lookup_variant_pair("Dracula"), None);
768 }
769
770 #[test]
771 fn test_find_counterpart_via_lookup() {
772 assert_eq!(
773 find_counterpart_name("Catppuccin Mocha"),
774 Some("Catppuccin Latte".to_string())
775 );
776 let latte_counterpart = find_counterpart_name("Catppuccin Latte");
777 assert!(latte_counterpart.is_some());
778 assert!(latte_counterpart.unwrap().starts_with("Catppuccin"));
779
780 assert_eq!(
781 find_counterpart_name("Tomorrow Night"),
782 Some("Tomorrow".to_string())
783 );
784 assert_eq!(
785 find_counterpart_name("Nord"),
786 Some("Nord Light".to_string())
787 );
788 assert_eq!(
789 find_counterpart_name("Nord Light"),
790 Some("Nord".to_string())
791 );
792 }
793
794 #[test]
795 fn test_detect_variant_via_lookup() {
796 assert_eq!(detect_variant("Catppuccin Mocha"), SchemeVariant::Dark);
797 assert_eq!(detect_variant("Catppuccin Latte"), SchemeVariant::Light);
798 assert_eq!(detect_variant("Nord"), SchemeVariant::Dark);
799 assert_eq!(detect_variant("Tomorrow"), SchemeVariant::Light);
800 }
801}