1use crate::{Error, Rgb, Variant};
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeMap;
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug, Deserialize)]
9struct RawPalette {
10 name: String,
11 description: String,
12 colors: Colors,
13 base: Base,
14 layers: RawLayers,
15 state: RawState,
16 semantic: RawSemantic,
17 ansi: RawAnsi,
18}
19
20#[derive(Debug, Deserialize)]
21struct RawLayers {
22 base: ColorExpr,
23 surface: ColorExpr,
24 sunken: ColorExpr,
25 raised: ColorExpr,
26 border: ColorExpr,
27 inset: ColorExpr,
28}
29
30impl RawLayers {
31 fn resolve(&self, resolver: &Resolver) -> Result<Layers, Error> {
32 Ok(Layers {
33 base: resolve_expr(resolver, &self.base)?,
34 surface: resolve_expr(resolver, &self.surface)?,
35 sunken: resolve_expr(resolver, &self.sunken)?,
36 raised: resolve_expr(resolver, &self.raised)?,
37 border: resolve_expr(resolver, &self.border)?,
38 inset: resolve_expr(resolver, &self.inset)?,
39 })
40 }
41}
42
43#[derive(Debug, Deserialize)]
44struct RawState {
45 selection_bg: ColorExpr,
46 selection_fg: ColorExpr,
47 match_bg: ColorExpr,
48 cursor: ColorExpr,
49 cursor_text: ColorExpr,
50 info: ColorExpr,
51 hint: ColorExpr,
52 warning: ColorExpr,
53 error: ColorExpr,
54 active_bg: ColorExpr,
55 diff_added: ColorExpr,
56 diff_added_bg: ColorExpr,
57 diff_removed: ColorExpr,
58 diff_removed_bg: ColorExpr,
59 diff_changed: ColorExpr,
60 diff_moved: ColorExpr,
61 conflict: ColorExpr,
62}
63
64impl RawState {
65 fn resolve(&self, resolver: &Resolver) -> Result<State, Error> {
66 Ok(State {
67 selection_bg: resolve_expr(resolver, &self.selection_bg)?,
68 selection_fg: resolve_expr(resolver, &self.selection_fg)?,
69 match_bg: resolve_expr(resolver, &self.match_bg)?,
70 cursor: resolve_expr(resolver, &self.cursor)?,
71 cursor_text: resolve_expr(resolver, &self.cursor_text)?,
72 info: resolve_expr(resolver, &self.info)?,
73 hint: resolve_expr(resolver, &self.hint)?,
74 warning: resolve_expr(resolver, &self.warning)?,
75 error: resolve_expr(resolver, &self.error)?,
76 active_bg: resolve_expr(resolver, &self.active_bg)?,
77 diff_added: resolve_expr(resolver, &self.diff_added)?,
78 diff_added_bg: resolve_expr(resolver, &self.diff_added_bg)?,
79 diff_removed: resolve_expr(resolver, &self.diff_removed)?,
80 diff_removed_bg: resolve_expr(resolver, &self.diff_removed_bg)?,
81 diff_changed: resolve_expr(resolver, &self.diff_changed)?,
82 diff_moved: resolve_expr(resolver, &self.diff_moved)?,
83 conflict: resolve_expr(resolver, &self.conflict)?,
84 })
85 }
86}
87
88#[derive(Debug, Deserialize)]
90struct RawAnsiColors {
91 black: ColorExpr,
92 red: ColorExpr,
93 green: ColorExpr,
94 yellow: ColorExpr,
95 blue: ColorExpr,
96 magenta: ColorExpr,
97 cyan: ColorExpr,
98 white: ColorExpr,
99}
100
101impl RawAnsiColors {
102 fn resolve(&self, resolver: &impl ResolveRef) -> Result<Ansi, Error> {
103 Ok(Ansi {
104 black: resolve_expr(resolver, &self.black)?,
105 red: resolve_expr(resolver, &self.red)?,
106 green: resolve_expr(resolver, &self.green)?,
107 yellow: resolve_expr(resolver, &self.yellow)?,
108 blue: resolve_expr(resolver, &self.blue)?,
109 magenta: resolve_expr(resolver, &self.magenta)?,
110 cyan: resolve_expr(resolver, &self.cyan)?,
111 white: resolve_expr(resolver, &self.white)?,
112 })
113 }
114}
115
116#[derive(Debug, Deserialize)]
117struct RawAnsi {
118 #[serde(flatten)]
119 base: RawAnsiColors,
120 bright: RawAnsiColors,
121}
122
123#[derive(Debug, Deserialize)]
124struct RawSemantic {
125 text: ColorExpr,
126 comment: ColorExpr,
127 string: ColorExpr,
128 keyword: ColorExpr,
129 number: ColorExpr,
130 constant: ColorExpr,
131 r#type: ColorExpr,
132 function: ColorExpr,
133 variable: ColorExpr,
134 success: ColorExpr,
135 path: ColorExpr,
136 r#macro: ColorExpr,
137 escape: ColorExpr,
138 regexp: ColorExpr,
139 link: ColorExpr,
140 directory: ColorExpr,
141}
142
143impl RawSemantic {
144 fn resolve(&self, resolver: &Resolver) -> Result<Semantic, Error> {
145 Ok(Semantic {
146 text: resolve_expr(resolver, &self.text)?,
147 comment: resolve_expr(resolver, &self.comment)?,
148 string: resolve_expr(resolver, &self.string)?,
149 keyword: resolve_expr(resolver, &self.keyword)?,
150 number: resolve_expr(resolver, &self.number)?,
151 constant: resolve_expr(resolver, &self.constant)?,
152 r#type: resolve_expr(resolver, &self.r#type)?,
153 function: resolve_expr(resolver, &self.function)?,
154 variable: resolve_expr(resolver, &self.variable)?,
155 success: resolve_expr(resolver, &self.success)?,
156 path: resolve_expr(resolver, &self.path)?,
157 r#macro: resolve_expr(resolver, &self.r#macro)?,
158 escape: resolve_expr(resolver, &self.escape)?,
159 regexp: resolve_expr(resolver, &self.regexp)?,
160 link: resolve_expr(resolver, &self.link)?,
161 directory: resolve_expr(resolver, &self.directory)?,
162 })
163 }
164}
165
166#[derive(Debug, Clone, Deserialize, Serialize)]
168pub struct Lantern {
169 pub ember: String, pub near: String, pub mid: String, pub far: String, }
174
175#[derive(Debug, Clone, Deserialize, Serialize)]
176pub struct Colors {
177 pub lantern: Lantern,
178 pub life: String,
179 pub night: String,
180 pub rain: String,
181 pub muted: String,
182}
183
184#[derive(Debug, Clone, Deserialize, Serialize)]
185pub struct Base {
186 pub background: String,
187 pub foreground: String,
188}
189
190#[derive(Debug, Clone, Deserialize, Serialize)]
191pub struct Layers {
192 pub base: String,
193 pub surface: String,
194 pub sunken: String,
195 pub raised: String,
196 pub border: String,
197 pub inset: String,
198}
199
200#[derive(Debug, Clone, Deserialize, Serialize)]
201pub struct State {
202 pub selection_bg: String,
203 pub selection_fg: String,
204 pub match_bg: String,
205 pub cursor: String,
206 pub cursor_text: String,
207 pub info: String,
208 pub hint: String,
209 pub warning: String,
210 pub error: String,
211 pub active_bg: String,
212 pub diff_added: String,
213 pub diff_added_bg: String,
214 pub diff_removed: String,
215 pub diff_removed_bg: String,
216 pub diff_changed: String,
217 pub diff_moved: String,
218 pub conflict: String,
219}
220
221#[derive(Debug, Clone, Serialize)]
222pub struct Semantic {
223 pub text: String,
224 pub comment: String,
225 pub string: String,
226 pub keyword: String,
227 pub number: String,
228 pub constant: String,
229 pub r#type: String,
230 pub function: String,
231 pub variable: String,
232 pub success: String,
233 pub path: String,
234 pub r#macro: String,
235 pub escape: String,
236 pub regexp: String,
237 pub link: String,
238 pub directory: String,
239}
240
241#[derive(Debug, Clone, Serialize)]
242pub struct Ansi {
243 pub black: String,
244 pub red: String,
245 pub green: String,
246 pub yellow: String,
247 pub blue: String,
248 pub magenta: String,
249 pub cyan: String,
250 pub white: String,
251}
252
253impl Ansi {
254 fn to_map(&self) -> BTreeMap<&'static str, String> {
256 self.into_iter().map(|(k, v)| (k, v.to_string())).collect()
257 }
258}
259
260impl<'a> IntoIterator for &'a Ansi {
261 type Item = (&'static str, &'a str);
262 type IntoIter = std::array::IntoIter<Self::Item, 8>;
263
264 fn into_iter(self) -> Self::IntoIter {
265 [
266 ("black", self.black.as_str()),
267 ("red", self.red.as_str()),
268 ("green", self.green.as_str()),
269 ("yellow", self.yellow.as_str()),
270 ("blue", self.blue.as_str()),
271 ("magenta", self.magenta.as_str()),
272 ("cyan", self.cyan.as_str()),
273 ("white", self.white.as_str()),
274 ]
275 .into_iter()
276 }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq)]
286enum Section {
287 Colors,
288 Base,
289 Ansi,
290}
291
292impl Section {
293 const ALLOWED: &[&str] = &["colors", "base", "ansi"];
295
296 fn parse(s: &str) -> Result<Self, Error> {
297 match s {
298 "colors" => Ok(Self::Colors),
299 "base" => Ok(Self::Base),
300 "ansi" => Ok(Self::Ansi),
301 _ => Err(Error::InvalidColorExpr(format!(
302 "'{s}' cannot be referenced (allowed: {})",
303 Self::ALLOWED.join(", ")
304 ))),
305 }
306 }
307
308 const fn as_str(&self) -> &'static str {
309 match self {
310 Self::Colors => "colors",
311 Self::Base => "base",
312 Self::Ansi => "ansi",
313 }
314 }
315}
316
317#[derive(Debug, Clone, Deserialize)]
328#[serde(try_from = "String")]
329enum ColorExpr {
330 Literal(String),
332 Ref { section: Section, key: String },
334 Lighten(Box<ColorExpr>, f64),
336 Darken(Box<ColorExpr>, f64),
338 Brighten(Box<ColorExpr>, f64),
340 Mix(Box<ColorExpr>, Box<ColorExpr>, f64),
342}
343
344impl TryFrom<String> for ColorExpr {
345 type Error = Error;
346
347 fn try_from(s: String) -> Result<Self, Self::Error> {
348 parse_color_expr(&s)
349 }
350}
351
352fn strip_fn_call<'a>(s: &'a str, name: &str) -> Option<&'a str> {
354 s.strip_prefix(name)
355 .and_then(|r| r.strip_prefix('('))
356 .and_then(|r| r.strip_suffix(')'))
357}
358
359fn parse_color_expr(s: &str) -> Result<ColorExpr, Error> {
361 let s = s.trim();
362
363 if s.starts_with('#') {
365 return Ok(ColorExpr::Literal(s.to_string()));
366 }
367
368 if let Some(args) = strip_fn_call(s, "lighten") {
370 let (inner, factor) = parse_unary_fn_args(args)?;
371 return Ok(ColorExpr::Lighten(Box::new(inner), factor));
372 }
373 if let Some(args) = strip_fn_call(s, "darken") {
374 let (inner, factor) = parse_unary_fn_args(args)?;
375 return Ok(ColorExpr::Darken(Box::new(inner), factor));
376 }
377 if let Some(args) = strip_fn_call(s, "brighten") {
378 let (inner, amount) = parse_unary_fn_args(args)?;
379 return Ok(ColorExpr::Brighten(Box::new(inner), amount));
380 }
381 if let Some(args) = strip_fn_call(s, "mix") {
382 let (color1, color2, factor) = parse_mix_args(args)?;
383 return Ok(ColorExpr::Mix(Box::new(color1), Box::new(color2), factor));
384 }
385
386 let (section_str, key) = s
388 .split_once('.')
389 .ok_or_else(|| Error::InvalidColorExpr(s.to_string()))?;
390 let section = Section::parse(section_str)?;
391 Ok(ColorExpr::Ref {
392 section,
393 key: key.to_string(),
394 })
395}
396
397fn parse_unary_fn_args(args: &str) -> Result<(ColorExpr, f64), Error> {
399 let (color_str, factor_str) = args
400 .rsplit_once(',')
401 .ok_or_else(|| Error::InvalidColorExpr(format!("expected 'color, factor': {args}")))?;
402 let inner = parse_color_expr(color_str.trim())?;
403 let factor = factor_str
404 .trim()
405 .parse::<f64>()
406 .map_err(|_| Error::InvalidColorExpr(format!("invalid factor: {}", factor_str.trim())))?;
407 Ok((inner, factor))
408}
409
410fn parse_mix_args(args: &str) -> Result<(ColorExpr, ColorExpr, f64), Error> {
412 let (rest, factor_str) = args.rsplit_once(',').ok_or_else(|| {
414 Error::InvalidColorExpr(format!("expected 'color1, color2, factor': {args}"))
415 })?;
416 let factor = factor_str
417 .trim()
418 .parse::<f64>()
419 .map_err(|_| Error::InvalidColorExpr(format!("invalid factor: {}", factor_str.trim())))?;
420
421 let (color1_str, color2_str) = rest.rsplit_once(',').ok_or_else(|| {
423 Error::InvalidColorExpr(format!("expected 'color1, color2, factor': {args}"))
424 })?;
425 let color1 = parse_color_expr(color1_str.trim())?;
426 let color2 = parse_color_expr(color2_str.trim())?;
427
428 Ok((color1, color2, factor))
429}
430
431trait ResolveRef {
433 fn resolve_ref(&self, section: Section, key: &str) -> Result<String, Error>;
434}
435
436fn resolve_expr(resolver: &impl ResolveRef, expr: &ColorExpr) -> Result<String, Error> {
438 match expr {
439 ColorExpr::Literal(hex) => Ok(hex.clone()),
440 ColorExpr::Ref { section, key } => resolver.resolve_ref(*section, key),
441 ColorExpr::Lighten(inner, factor) => {
442 let hex = resolve_expr(resolver, inner)?;
443 Ok(hex.parse::<Rgb>()?.lighten(*factor).to_string())
444 }
445 ColorExpr::Darken(inner, factor) => {
446 let hex = resolve_expr(resolver, inner)?;
447 Ok(hex.parse::<Rgb>()?.darken(*factor).to_string())
448 }
449 ColorExpr::Brighten(inner, amount) => {
450 let hex = resolve_expr(resolver, inner)?;
451 Ok(hex.parse::<Rgb>()?.brighten(*amount).to_string())
452 }
453 ColorExpr::Mix(color1, color2, factor) => {
454 let rgb1: Rgb = resolve_expr(resolver, color1)?.parse()?;
455 let rgb2: Rgb = resolve_expr(resolver, color2)?.parse()?;
456 Ok(rgb1.mix(rgb2, *factor).to_string())
457 }
458 }
459}
460
461struct Resolver<'a> {
462 colors: BTreeMap<&'a str, &'a str>,
463 base: BTreeMap<&'a str, &'a str>,
464 ansi_map: BTreeMap<String, String>,
466 resolved_ansi: Ansi,
468 resolved_ansi_bright: Ansi,
470}
471
472impl<'a> Resolver<'a> {
473 fn new(raw: &'a RawPalette) -> Result<Self, Error> {
474 let colors: BTreeMap<&str, &str> = [
476 ("lantern.ember", raw.colors.lantern.ember.as_str()),
477 ("lantern.near", raw.colors.lantern.near.as_str()),
478 ("lantern.mid", raw.colors.lantern.mid.as_str()),
479 ("lantern.far", raw.colors.lantern.far.as_str()),
480 ("life", raw.colors.life.as_str()),
481 ("night", raw.colors.night.as_str()),
482 ("rain", raw.colors.rain.as_str()),
483 ("muted", raw.colors.muted.as_str()),
484 ]
485 .into_iter()
486 .collect();
487 let base: BTreeMap<&str, &str> = [
488 ("background", raw.base.background.as_str()),
489 ("foreground", raw.base.foreground.as_str()),
490 ]
491 .into_iter()
492 .collect();
493
494 let partial = PartialResolver {
496 colors: &colors,
497 base: &base,
498 ansi: None,
499 };
500 let resolved_ansi = raw.ansi.base.resolve(&partial)?;
501
502 let mut ansi_map: BTreeMap<String, String> = resolved_ansi
504 .to_map()
505 .into_iter()
506 .map(|(k, v)| (k.to_string(), v))
507 .collect();
508
509 let partial_with_ansi = PartialResolver {
511 colors: &colors,
512 base: &base,
513 ansi: Some(&ansi_map),
514 };
515 let resolved_ansi_bright = raw.ansi.bright.resolve(&partial_with_ansi)?;
516
517 for (k, v) in resolved_ansi_bright.to_map() {
519 ansi_map.insert(format!("bright.{k}"), v);
520 }
521
522 Ok(Self {
523 colors,
524 base,
525 ansi_map,
526 resolved_ansi,
527 resolved_ansi_bright,
528 })
529 }
530}
531
532impl ResolveRef for Resolver<'_> {
533 fn resolve_ref(&self, section: Section, key: &str) -> Result<String, Error> {
534 let ref_str = || format!("{}.{key}", section.as_str());
535 match section {
536 Section::Colors => self
537 .colors
538 .get(key)
539 .copied()
540 .map(str::to_string)
541 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
542 Section::Base => self
543 .base
544 .get(key)
545 .copied()
546 .map(str::to_string)
547 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
548 Section::Ansi => self
549 .ansi_map
550 .get(key)
551 .cloned()
552 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
553 }
554 }
555}
556
557struct PartialResolver<'a> {
560 colors: &'a BTreeMap<&'a str, &'a str>,
561 base: &'a BTreeMap<&'a str, &'a str>,
562 ansi: Option<&'a BTreeMap<String, String>>,
563}
564
565impl ResolveRef for PartialResolver<'_> {
566 fn resolve_ref(&self, section: Section, key: &str) -> Result<String, Error> {
567 let ref_str = || format!("{}.{key}", section.as_str());
568 match section {
569 Section::Colors => self
570 .colors
571 .get(key)
572 .copied()
573 .map(str::to_string)
574 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
575 Section::Base => self
576 .base
577 .get(key)
578 .copied()
579 .map(str::to_string)
580 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
581 Section::Ansi => self
582 .ansi
583 .and_then(|m| m.get(key).cloned())
584 .ok_or_else(|| Error::UnresolvedRef(ref_str())),
585 }
586 }
587}
588
589impl RawPalette {
590 fn resolve(&self, variant: Variant) -> Result<Palette, Error> {
591 let resolver = Resolver::new(self)?;
592 Ok(Palette {
593 variant,
594 name: self.name.clone(),
595 description: self.description.clone(),
596 colors: self.colors.clone(),
597 base: self.base.clone(),
598 layers: self.layers.resolve(&resolver)?,
599 state: self.state.resolve(&resolver)?,
600 semantic: self.semantic.resolve(&resolver)?,
601 ansi: resolver.resolved_ansi,
602 ansi_bright: resolver.resolved_ansi_bright,
603 })
604 }
605}
606
607#[derive(Debug, Serialize)]
608pub struct Palette {
609 pub variant: Variant,
610 pub name: String,
611 pub description: String,
612 pub colors: Colors,
613 pub base: Base,
614 pub layers: Layers,
615 pub state: State,
616 pub semantic: Semantic,
617 pub ansi: Ansi,
618 pub ansi_bright: Ansi,
619}
620
621impl Palette {
622 const NIGHT_TOML: &'static str = include_str!("../palette/akari-night.toml");
624
625 const DAWN_TOML: &'static str = include_str!("../palette/akari-dawn.toml");
627
628 #[must_use]
634 pub fn night() -> Self {
635 Self::from_str(Self::NIGHT_TOML, Variant::Night)
636 .expect("embedded Night palette should be valid")
637 }
638
639 #[must_use]
645 pub fn dawn() -> Self {
646 Self::from_str(Self::DAWN_TOML, Variant::Dawn)
647 .expect("embedded Dawn palette should be valid")
648 }
649
650 pub fn from_path(path: impl AsRef<Path>, variant: Variant) -> Result<Self, Error> {
652 let content = fs::read_to_string(path)?;
653 Self::from_str(&content, variant)
654 }
655
656 pub fn from_str(content: &str, variant: Variant) -> Result<Self, Error> {
658 let raw: RawPalette = toml::from_str(content)?;
659 raw.resolve(variant)
660 }
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666 use std::path::PathBuf;
667
668 fn palette_path() -> PathBuf {
669 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("palette/akari-night.toml")
670 }
671
672 #[test]
673 fn load_night_palette() {
674 let palette = Palette::from_path(palette_path(), Variant::Night).unwrap();
675 assert_eq!(palette.name, "akari-night");
676 assert_eq!(palette.variant, Variant::Night);
677 }
678
679 #[test]
680 fn colors_are_loaded() {
681 let palette = Palette::from_path(palette_path(), Variant::Night).unwrap();
682 assert_eq!(palette.colors.lantern.mid, "#E26A3B");
683 assert_eq!(palette.colors.lantern.ember, "#D65A3A");
684 assert_eq!(palette.colors.lantern.near, "#D25046");
685 assert_eq!(palette.colors.lantern.far, "#D4A05A");
686 }
687
688 #[test]
689 fn base_colors_are_loaded() {
690 let palette = Palette::from_path(palette_path(), Variant::Night).unwrap();
691 assert_eq!(palette.base.background, "#25231F");
692 assert_eq!(palette.base.foreground, "#E6DED3");
693 }
694
695 #[test]
696 fn semantic_references_resolved() {
697 let palette = Palette::from_path(palette_path(), Variant::Night).unwrap();
698 assert_eq!(palette.semantic.keyword, "#E26A3B");
700 assert_eq!(palette.semantic.string, "#7FAF6A");
702 }
703
704 #[test]
705 fn ansi_references_resolved() {
706 let palette = Palette::from_path(palette_path(), Variant::Night).unwrap();
707 assert_eq!(palette.ansi.green, "#7FAF6A");
709 assert_eq!(palette.ansi.white, "#E6DED3");
711 }
712
713 #[test]
714 fn missing_semantic_field_fails() {
715 use std::io::Write;
716 use tempfile::NamedTempFile;
717
718 let toml = r##"
719name = "test"
720description = "test"
721
722[colors.lantern]
723ember = "#D65A3A"
724near = "#D25046"
725mid = "#E26A3B"
726far = "#D4A05A"
727
728[colors]
729life = "#7FAF6A"
730night = "#5A6F82"
731rain = "#6F8F8A"
732muted = "#7C6A8A"
733
734[base]
735background = "#171B22"
736foreground = "#E6DED3"
737
738[layers]
739base = "#171B22"
740surface = "#1E2329"
741sunken = "#13171D"
742raised = "#252B33"
743border = "#2E353E"
744inset = "#3A424D"
745
746[state]
747selection_bg = "#3A424D"
748selection_fg = "#E6DED3"
749match_bg = "#4A3A2A"
750cursor = "#E26A3B"
751cursor_text = "#171B22"
752info = "#5A6F82"
753hint = "#7C6A8A"
754warning = "#D4A05A"
755error = "#D65A3A"
756active_bg = "#2A3540"
757diff_added = "#7FAF6A"
758diff_removed = "#D65A3A"
759diff_changed = "#D4A05A"
760
761[semantic]
762comment = "#7D8797"
763string = "colors.life"
764keyword = "colors.lantern.mid"
765number = "colors.lantern.far"
766constant = "colors.lantern.far"
767type = "colors.lantern.far"
768function = "colors.lantern.mid"
769variable = "base.foreground"
770success = "colors.life"
771
772[ansi]
773black = "#171B22"
774red = "colors.lantern.near"
775green = "colors.life"
776yellow = "colors.lantern.far"
777blue = "colors.night"
778magenta = "colors.muted"
779cyan = "colors.rain"
780white = "base.foreground"
781
782[ansi.bright]
783black = "#3A424D"
784red = "colors.lantern.mid"
785green = "colors.life"
786yellow = "colors.lantern.far"
787blue = "colors.night"
788magenta = "colors.muted"
789cyan = "colors.rain"
790white = "base.foreground"
791"##;
792
793 let mut file = NamedTempFile::new().unwrap();
794 file.write_all(toml.as_bytes()).unwrap();
795
796 let result = Palette::from_path(file.path(), Variant::Night);
797 assert!(result.is_err());
798 let err = result.unwrap_err();
799 assert!(matches!(err, Error::ParsePalette(_)));
800 }
801
802 #[test]
803 fn invalid_reference_fails() {
804 use std::io::Write;
805 use tempfile::NamedTempFile;
806
807 let toml = r##"
808name = "test"
809description = "test"
810
811[colors.lantern]
812ember = "#D65A3A"
813near = "#D25046"
814mid = "#E26A3B"
815far = "#D4A05A"
816
817[colors]
818life = "#7FAF6A"
819night = "#5A6F82"
820rain = "#6F8F8A"
821muted = "#7C6A8A"
822
823[base]
824background = "#171B22"
825foreground = "#E6DED3"
826
827[layers]
828base = "#171B22"
829surface = "#1E2329"
830sunken = "#13171D"
831raised = "#252B33"
832border = "#2E353E"
833inset = "#3A424D"
834
835[state]
836selection_bg = "#3A424D"
837selection_fg = "#E6DED3"
838match_bg = "#4A3A2A"
839cursor = "#E26A3B"
840cursor_text = "#171B22"
841info = "#5A6F82"
842hint = "#7C6A8A"
843warning = "#D4A05A"
844error = "#D65A3A"
845active_bg = "#2A3540"
846diff_added = "#7FAF6A"
847diff_added_bg = "#2A3A2A"
848diff_removed = "#D65A3A"
849diff_removed_bg = "#3A2A2A"
850diff_changed = "#D4A05A"
851diff_moved = "#5A6F82"
852conflict = "#D65A3A"
853
854[semantic]
855text = "base.foreground"
856comment = "#7D8797"
857string = "colors.nonexistent"
858keyword = "colors.lantern.mid"
859number = "colors.lantern.far"
860constant = "colors.lantern.far"
861type = "colors.lantern.far"
862function = "colors.lantern.mid"
863variable = "base.foreground"
864success = "colors.life"
865path = "ansi.green"
866macro = "ansi.bright.magenta"
867escape = "ansi.bright.magenta"
868regexp = "ansi.bright.green"
869link = "ansi.bright.blue"
870directory = "ansi.cyan"
871
872[ansi]
873black = "#171B22"
874red = "colors.lantern.near"
875green = "colors.life"
876yellow = "colors.lantern.far"
877blue = "colors.night"
878magenta = "colors.muted"
879cyan = "colors.rain"
880white = "base.foreground"
881
882[ansi.bright]
883black = "#3A424D"
884red = "colors.lantern.mid"
885green = "colors.life"
886yellow = "colors.lantern.far"
887blue = "colors.night"
888magenta = "colors.muted"
889cyan = "colors.rain"
890white = "base.foreground"
891"##;
892
893 let mut file = NamedTempFile::new().unwrap();
894 file.write_all(toml.as_bytes()).unwrap();
895
896 let result = Palette::from_path(file.path(), Variant::Night);
897 assert!(result.is_err());
898 let err = result.unwrap_err();
899 assert!(matches!(err, Error::UnresolvedRef(_)));
900 }
901
902 #[test]
903 fn parse_color_expr_literal() {
904 let expr = parse_color_expr("#E26A3B").unwrap();
905 assert!(matches!(expr, ColorExpr::Literal(s) if s == "#E26A3B"));
906 }
907
908 #[test]
909 fn parse_color_expr_reference() {
910 let expr = parse_color_expr("colors.lantern.mid").unwrap();
911 assert!(
912 matches!(expr, ColorExpr::Ref { section, key } if section == Section::Colors && key == "lantern.mid")
913 );
914 }
915
916 #[test]
917 fn parse_color_expr_lighten() {
918 let expr = parse_color_expr("lighten(colors.lantern.mid, 0.1)").unwrap();
919 match expr {
920 ColorExpr::Lighten(inner, factor) => {
921 assert!(
922 matches!(*inner, ColorExpr::Ref { section, key } if section == Section::Colors && key == "lantern.mid")
923 );
924 assert!((factor - 0.1).abs() < 0.001);
925 }
926 _ => panic!("expected Lighten"),
927 }
928 }
929
930 #[test]
931 fn parse_color_expr_darken() {
932 let expr = parse_color_expr("darken(base.background, 0.2)").unwrap();
933 match expr {
934 ColorExpr::Darken(inner, factor) => {
935 assert!(
936 matches!(*inner, ColorExpr::Ref { section, key } if section == Section::Base && key == "background")
937 );
938 assert!((factor - 0.2).abs() < 0.001);
939 }
940 _ => panic!("expected Darken"),
941 }
942 }
943
944 #[test]
945 fn parse_color_expr_nested() {
946 let expr = parse_color_expr("lighten(darken(colors.lantern.mid, 0.1), 0.2)").unwrap();
947 match expr {
948 ColorExpr::Lighten(inner, outer_factor) => {
949 assert!((outer_factor - 0.2).abs() < 0.001);
950 match *inner {
951 ColorExpr::Darken(innermost, inner_factor) => {
952 assert!(
953 matches!(*innermost, ColorExpr::Ref { section, key } if section == Section::Colors && key == "lantern.mid")
954 );
955 assert!((inner_factor - 0.1).abs() < 0.001);
956 }
957 _ => panic!("expected Darken"),
958 }
959 }
960 _ => panic!("expected Lighten"),
961 }
962 }
963
964 #[test]
965 fn parse_color_expr_mix() {
966 let expr = parse_color_expr("mix(base.background, colors.night, 0.15)").unwrap();
967 match expr {
968 ColorExpr::Mix(color1, color2, factor) => {
969 assert!(
970 matches!(*color1, ColorExpr::Ref { section, key } if section == Section::Base && key == "background")
971 );
972 assert!(
973 matches!(*color2, ColorExpr::Ref { section, key } if section == Section::Colors && key == "night")
974 );
975 assert!((factor - 0.15).abs() < 0.001);
976 }
977 _ => panic!("expected Mix"),
978 }
979 }
980
981 #[test]
982 fn parse_color_expr_rejects_non_referenceable_sections() {
983 let err = parse_color_expr("layers.base").unwrap_err();
985 assert!(
986 matches!(err, Error::InvalidColorExpr(msg) if msg.contains("cannot be referenced"))
987 );
988
989 let err = parse_color_expr("state.cursor").unwrap_err();
990 assert!(
991 matches!(err, Error::InvalidColorExpr(msg) if msg.contains("cannot be referenced"))
992 );
993
994 let err = parse_color_expr("semantic.keyword").unwrap_err();
995 assert!(
996 matches!(err, Error::InvalidColorExpr(msg) if msg.contains("cannot be referenced"))
997 );
998 }
999
1000 #[test]
1001 fn parse_color_expr_ansi_bright() {
1002 let expr = parse_color_expr("ansi.bright.red").unwrap();
1004 assert!(
1005 matches!(expr, ColorExpr::Ref { section, key } if section == Section::Ansi && key == "bright.red")
1006 );
1007 }
1008}