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