termusiclib/config/v2/tui/theme/
mod.rs

1#![allow(clippy::module_name_repetitions)]
2
3use std::{fs::File, io::BufReader, num::ParseIntError, path::Path};
4
5use serde::{Deserialize, Serialize};
6use tuirealm::props::Color;
7
8use crate::config::yaml_theme::{
9    YAMLTheme, YAMLThemeBright, YAMLThemeCursor, YAMLThemeNormal, YAMLThemePrimary,
10};
11
12use styles::ColorTermusic;
13
14pub mod styles;
15
16// TODO: combine Theme & Color?
17
18#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
19#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
20pub struct ThemeWrap {
21    pub style: styles::Styles,
22    // On full-on default, also set the names to "Termusic Default"
23    // this function is only used if this property does not exist at all
24    #[serde(default = "ThemeColors::full_default")]
25    pub theme: ThemeColors,
26}
27
28impl ThemeWrap {
29    #[must_use]
30    pub fn get_color_from_theme(&self, color: ColorTermusic) -> Color {
31        // first step to get the theme path of what color to use
32        let val = match color {
33            ColorTermusic::Reset => return Color::Reset,
34            ColorTermusic::Foreground => &self.theme.primary.foreground,
35            ColorTermusic::Background => &self.theme.primary.background,
36            ColorTermusic::Black => &self.theme.normal.black,
37            ColorTermusic::Red => &self.theme.normal.red,
38            ColorTermusic::Green => &self.theme.normal.green,
39            ColorTermusic::Yellow => &self.theme.normal.yellow,
40            ColorTermusic::Blue => &self.theme.normal.blue,
41            ColorTermusic::Magenta => &self.theme.normal.magenta,
42            ColorTermusic::Cyan => &self.theme.normal.cyan,
43            ColorTermusic::White => &self.theme.normal.white,
44            ColorTermusic::LightBlack => &self.theme.bright.black,
45            ColorTermusic::LightRed => &self.theme.bright.red,
46            ColorTermusic::LightGreen => &self.theme.bright.green,
47            ColorTermusic::LightYellow => &self.theme.bright.yellow,
48            ColorTermusic::LightBlue => &self.theme.bright.blue,
49            ColorTermusic::LightMagenta => &self.theme.bright.magenta,
50            ColorTermusic::LightCyan => &self.theme.bright.cyan,
51            ColorTermusic::LightWhite => &self.theme.bright.white,
52        };
53
54        // finally resolve if that theme color is native or a rgb(hex) value
55        val.resolve_color(color)
56    }
57
58    #[inline]
59    #[must_use]
60    pub fn library_foreground(&self) -> Color {
61        self.get_color_from_theme(self.style.library.foreground_color)
62    }
63
64    #[inline]
65    #[must_use]
66    pub fn library_background(&self) -> Color {
67        self.get_color_from_theme(self.style.library.background_color)
68    }
69
70    #[inline]
71    #[must_use]
72    pub fn library_highlight(&self) -> Color {
73        self.get_color_from_theme(self.style.library.highlight_color)
74    }
75
76    #[inline]
77    #[must_use]
78    pub fn library_border(&self) -> Color {
79        self.get_color_from_theme(self.style.library.border_color)
80    }
81
82    #[inline]
83    #[must_use]
84    pub fn playlist_foreground(&self) -> Color {
85        self.get_color_from_theme(self.style.playlist.foreground_color)
86    }
87
88    #[inline]
89    #[must_use]
90    pub fn playlist_background(&self) -> Color {
91        self.get_color_from_theme(self.style.playlist.background_color)
92    }
93
94    #[inline]
95    #[must_use]
96    pub fn playlist_highlight(&self) -> Color {
97        self.get_color_from_theme(self.style.playlist.highlight_color)
98    }
99
100    #[inline]
101    #[must_use]
102    pub fn playlist_border(&self) -> Color {
103        self.get_color_from_theme(self.style.playlist.border_color)
104    }
105
106    #[inline]
107    #[must_use]
108    pub fn progress_foreground(&self) -> Color {
109        self.get_color_from_theme(self.style.progress.foreground_color)
110    }
111
112    #[inline]
113    #[must_use]
114    pub fn progress_background(&self) -> Color {
115        self.get_color_from_theme(self.style.progress.background_color)
116    }
117
118    #[inline]
119    #[must_use]
120    pub fn progress_border(&self) -> Color {
121        self.get_color_from_theme(self.style.progress.border_color)
122    }
123
124    #[inline]
125    #[must_use]
126    pub fn lyric_foreground(&self) -> Color {
127        self.get_color_from_theme(self.style.lyric.foreground_color)
128    }
129
130    #[inline]
131    #[must_use]
132    pub fn lyric_background(&self) -> Color {
133        self.get_color_from_theme(self.style.lyric.background_color)
134    }
135
136    #[inline]
137    #[must_use]
138    pub fn lyric_border(&self) -> Color {
139        self.get_color_from_theme(self.style.lyric.border_color)
140    }
141
142    #[inline]
143    #[must_use]
144    pub fn important_popup_foreground(&self) -> Color {
145        self.get_color_from_theme(self.style.important_popup.foreground_color)
146    }
147
148    #[inline]
149    #[must_use]
150    pub fn important_popup_background(&self) -> Color {
151        self.get_color_from_theme(self.style.important_popup.background_color)
152    }
153
154    #[inline]
155    #[must_use]
156    pub fn important_popup_border(&self) -> Color {
157        self.get_color_from_theme(self.style.important_popup.border_color)
158    }
159
160    #[inline]
161    #[must_use]
162    pub fn fallback_foreground(&self) -> Color {
163        self.get_color_from_theme(self.style.fallback.foreground_color)
164    }
165
166    #[inline]
167    #[must_use]
168    pub fn fallback_background(&self) -> Color {
169        self.get_color_from_theme(self.style.fallback.background_color)
170    }
171
172    #[inline]
173    #[must_use]
174    pub fn fallback_highlight(&self) -> Color {
175        self.get_color_from_theme(self.style.fallback.highlight_color)
176    }
177
178    #[inline]
179    #[must_use]
180    pub fn fallback_border(&self) -> Color {
181        self.get_color_from_theme(self.style.fallback.border_color)
182    }
183}
184
185/// Error for when [`ThemeColor`] parsing fails
186#[derive(Debug, Clone, PartialEq, thiserror::Error)]
187pub enum ThemeColorParseError {
188    #[error("Failed to parse hex color: {0}")]
189    HexParseError(#[from] ThemeColorHexParseError),
190    #[error("Failed to parse color, expected prefix \"#\" or \"0x\" and length 6 or \"native\"")]
191    UnknownValue(String),
192}
193
194#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
195#[serde(try_from = "String")]
196#[serde(into = "String")]
197pub enum ThemeColor {
198    /// Native theme, let the terminal decide the colors
199    Native,
200    /// Set explicit RGB colors
201    Hex(ThemeColorHex),
202}
203
204impl ThemeColor {
205    /// Create a new hex instance with the given values.
206    ///
207    /// Those values are explicit as this means it can be known to work (unlike a string).
208    #[must_use]
209    pub const fn new_hex(r: u8, g: u8, b: u8) -> Self {
210        Self::Hex(ThemeColorHex::new(r, g, b))
211    }
212
213    /// Create a new instance for native colors.
214    #[must_use]
215    pub const fn new_native() -> Self {
216        Self::Native
217    }
218
219    /// Try to parse a hex or "native" string.
220    fn from_string(val: &str) -> Result<Self, ThemeColorParseError> {
221        if val == "native" {
222            return Ok(Self::Native);
223        }
224
225        let res = match ThemeColorHex::try_from(val) {
226            Ok(v) => v,
227            Err(err) => {
228                return Err(match err {
229                    // map unknown prefix error to more descriptive error for native / hex
230                    ThemeColorHexParseError::UnknownPrefix(_) => {
231                        ThemeColorParseError::UnknownValue(val.to_string())
232                    }
233                    v => ThemeColorParseError::HexParseError(v),
234                });
235            }
236        };
237
238        Ok(Self::Hex(res))
239    }
240
241    /// Output the current value as its string representation
242    #[allow(clippy::inherent_to_string)] // not wanting to implement "Display"
243    fn to_string(self) -> String {
244        match self {
245            ThemeColor::Native => "native".to_string(),
246            ThemeColor::Hex(theme_color_hex) => theme_color_hex.to_hex(),
247        }
248    }
249
250    /// Resolve the current instance to either native coloring (requires `style`) or a rgb color
251    #[must_use]
252    pub fn resolve_color(&self, style: ColorTermusic) -> Color {
253        let hex = match self {
254            ThemeColor::Native => return style.into(),
255            ThemeColor::Hex(theme_color_hex) => theme_color_hex,
256        };
257
258        (*hex).into()
259    }
260}
261
262impl TryFrom<String> for ThemeColor {
263    type Error = ThemeColorParseError;
264
265    fn try_from(value: String) -> Result<Self, Self::Error> {
266        Self::from_string(&value)
267    }
268}
269
270impl TryFrom<&str> for ThemeColor {
271    type Error = ThemeColorParseError;
272
273    fn try_from(value: &str) -> Result<Self, Self::Error> {
274        Self::from_string(value)
275    }
276}
277
278impl From<ThemeColor> for String {
279    fn from(val: ThemeColor) -> Self {
280        ThemeColor::to_string(val)
281    }
282}
283
284/// Error for when [`ThemeColor`] parsing fails
285#[derive(Debug, Clone, PartialEq, thiserror::Error)]
286pub enum ThemeColorHexParseError {
287    #[error("Failed to parse color because of {0}")]
288    ParseIntError(#[from] ParseIntError),
289    #[error(
290        "Failed to parse color because of incorrect length {0}, expected prefix \"#\" or \"0x\" and length 6"
291    )]
292    IncorrectLength(usize),
293    #[error("Failed to parse color becazse of unknown prefix \"{0}\", expected \"#\" or \"0x\"")]
294    UnknownPrefix(String),
295}
296
297/// The rgb colors
298#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq)]
299#[serde(try_from = "String")]
300#[serde(into = "String")]
301pub struct ThemeColorHex {
302    pub r: u8,
303    pub g: u8,
304    pub b: u8,
305}
306
307impl ThemeColorHex {
308    /// Create a new instance with those values
309    #[must_use]
310    pub const fn new(r: u8, g: u8, b: u8) -> Self {
311        Self { r, g, b }
312    }
313
314    /// Convert from a prefix + 6 length string
315    pub fn from_hex(val: &str) -> Result<Self, ThemeColorHexParseError> {
316        let Some(without_prefix) = val.strip_prefix('#').or(val.strip_prefix("0x")) else {
317            return Err(ThemeColorHexParseError::UnknownPrefix(val.to_string()));
318        };
319
320        // not in a format we support
321        if without_prefix.len() != 6 {
322            return Err(ThemeColorHexParseError::IncorrectLength(
323                without_prefix.len(),
324            ));
325        }
326
327        let r = u8::from_str_radix(&without_prefix[0..=1], 16)
328            .map_err(ThemeColorHexParseError::ParseIntError)?;
329        let g = u8::from_str_radix(&without_prefix[2..=3], 16)
330            .map_err(ThemeColorHexParseError::ParseIntError)?;
331        let b = u8::from_str_radix(&without_prefix[4..=5], 16)
332            .map_err(ThemeColorHexParseError::ParseIntError)?;
333
334        Ok(Self { r, g, b })
335    }
336
337    /// Convert to hex prefix + 6 length string
338    #[inline]
339    #[must_use]
340    pub fn to_hex(&self) -> String {
341        format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
342    }
343}
344
345impl TryFrom<String> for ThemeColorHex {
346    type Error = ThemeColorHexParseError;
347
348    fn try_from(value: String) -> Result<Self, Self::Error> {
349        Self::from_hex(&value)
350    }
351}
352
353impl TryFrom<&str> for ThemeColorHex {
354    type Error = ThemeColorHexParseError;
355
356    fn try_from(value: &str) -> Result<Self, Self::Error> {
357        Self::from_hex(value)
358    }
359}
360
361impl From<ThemeColorHex> for String {
362    fn from(val: ThemeColorHex) -> Self {
363        ThemeColorHex::to_hex(&val)
364    }
365}
366
367impl From<ThemeColorHex> for Color {
368    fn from(val: ThemeColorHex) -> Self {
369        Color::Rgb(val.r, val.g, val.b)
370    }
371}
372
373#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
374#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
375pub struct ThemeColors {
376    /// The Filename of the current theme, if a file is used.
377    /// This value is skipped if empty.
378    ///
379    /// This is used for example to pre-select in the config editor if available.
380    #[serde(skip_serializing_if = "Option::is_none")]
381    pub file_name: Option<String>,
382    pub name: String,
383    pub author: String,
384    pub primary: ThemePrimary,
385    pub cursor: ThemeCursor,
386    pub normal: ThemeNormal,
387    pub bright: ThemeBright,
388}
389
390impl Default for ThemeColors {
391    fn default() -> Self {
392        Self {
393            file_name: None,
394            name: default_name(),
395            author: default_author(),
396            primary: ThemePrimary::default(),
397            cursor: ThemeCursor::default(),
398            normal: ThemeNormal::default(),
399            bright: ThemeBright::default(),
400        }
401    }
402}
403
404impl ThemeColors {
405    /// Get the full default theme, including names.
406    ///
407    /// This function is different from [`Self::default`] as the trait impl is also used for filling empty places
408    #[must_use]
409    pub fn full_default() -> Self {
410        Self {
411            name: "Termusic Default".to_string(),
412            author: "Termusic Developers".to_string(),
413            ..Default::default()
414        }
415    }
416
417    /// Get a full native theme.
418    #[must_use]
419    pub fn full_native() -> Self {
420        Self {
421            file_name: None,
422            name: "Native".to_string(),
423            author: "Termusic Developers".to_string(),
424            primary: ThemePrimary::native(),
425            cursor: ThemeCursor::native(),
426            normal: ThemeNormal::native(),
427            bright: ThemeBright::native(),
428        }
429    }
430}
431
432/// Error for when [`ThemeColors`] parsing fails
433#[derive(Debug, Clone, PartialEq, thiserror::Error)]
434pub enum ThemeColorsParseError {
435    #[error("Failed to parse Theme: {0}")]
436    ThemeColor(#[from] ThemeColorParseError),
437}
438
439impl TryFrom<YAMLTheme> for ThemeColors {
440    type Error = ThemeColorsParseError;
441
442    fn try_from(value: YAMLTheme) -> Result<Self, Self::Error> {
443        let colors = value.colors;
444        Ok(Self {
445            file_name: None,
446            name: colors.name.unwrap_or_else(default_name),
447            author: colors.author.unwrap_or_else(default_author),
448            primary: colors.primary.try_into()?,
449            cursor: colors.cursor.try_into()?,
450            normal: colors.normal.try_into()?,
451            bright: colors.bright.try_into()?,
452        })
453    }
454}
455
456impl ThemeColors {
457    /// Load a YAML Theme and then convert it to a [`ThemeColors`] instance
458    pub fn from_yaml_file(path: &Path) -> anyhow::Result<Self> {
459        let parsed: YAMLTheme = serde_yaml::from_reader(BufReader::new(File::open(path)?))?;
460
461        let mut theme = Self::try_from(parsed)?;
462
463        let file_name = path.file_stem();
464        theme.file_name = file_name.map(|v| v.to_string_lossy().to_string());
465
466        Ok(theme)
467    }
468}
469
470#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
471pub struct ThemePrimary {
472    pub background: ThemeColor,
473    pub foreground: ThemeColor,
474}
475
476impl Default for ThemePrimary {
477    fn default() -> Self {
478        Self {
479            background: ThemeColor::new_hex(0x10, 0x14, 0x21),
480            foreground: ThemeColor::new_hex(0xff, 0xfb, 0xf6),
481        }
482    }
483}
484
485impl ThemePrimary {
486    fn native() -> Self {
487        Self {
488            background: ThemeColor::new_native(),
489            foreground: ThemeColor::new_native(),
490        }
491    }
492}
493
494impl TryFrom<YAMLThemePrimary> for ThemePrimary {
495    type Error = ThemeColorsParseError;
496
497    fn try_from(value: YAMLThemePrimary) -> Result<Self, Self::Error> {
498        Ok(Self {
499            background: value.background.try_into()?,
500            foreground: value.foreground.try_into()?,
501        })
502    }
503}
504
505#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
506#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
507pub struct ThemeCursor {
508    pub text: ThemeColor,
509    pub cursor: ThemeColor,
510}
511
512impl Default for ThemeCursor {
513    fn default() -> Self {
514        Self {
515            text: ThemeColor::new_hex(0x1e, 0x1e, 0x1e),
516            cursor: default_fff(),
517        }
518    }
519}
520
521impl ThemeCursor {
522    fn native() -> Self {
523        Self {
524            text: ThemeColor::new_native(),
525            cursor: ThemeColor::new_native(),
526        }
527    }
528}
529
530impl TryFrom<YAMLThemeCursor> for ThemeCursor {
531    type Error = ThemeColorsParseError;
532
533    fn try_from(value: YAMLThemeCursor) -> Result<Self, Self::Error> {
534        Ok(Self {
535            text: value.text.try_into()?,
536            cursor: value.cursor.try_into()?,
537        })
538    }
539}
540
541#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
542#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
543pub struct ThemeNormal {
544    pub black: ThemeColor,
545    pub red: ThemeColor,
546    pub green: ThemeColor,
547    pub yellow: ThemeColor,
548    pub blue: ThemeColor,
549    pub magenta: ThemeColor,
550    pub cyan: ThemeColor,
551    pub white: ThemeColor,
552}
553
554impl Default for ThemeNormal {
555    fn default() -> Self {
556        Self {
557            black: ThemeColor::new_hex(0x2e, 0x2e, 0x2e),
558            red: ThemeColor::new_hex(0xeb, 0x41, 0x29),
559            green: ThemeColor::new_hex(0xab, 0xe0, 0x47),
560            yellow: ThemeColor::new_hex(0xf6, 0xc7, 0x44),
561            blue: ThemeColor::new_hex(0x47, 0xa0, 0xf3),
562            magenta: ThemeColor::new_hex(0x7b, 0x5c, 0xb0),
563            cyan: ThemeColor::new_hex(0x64, 0xdb, 0xed),
564            white: ThemeColor::new_hex(0xe5, 0xe9, 0xf0),
565        }
566    }
567}
568
569impl ThemeNormal {
570    fn native() -> Self {
571        Self {
572            black: ThemeColor::new_native(),
573            red: ThemeColor::new_native(),
574            green: ThemeColor::new_native(),
575            yellow: ThemeColor::new_native(),
576            blue: ThemeColor::new_native(),
577            magenta: ThemeColor::new_native(),
578            cyan: ThemeColor::new_native(),
579            white: ThemeColor::new_native(),
580        }
581    }
582}
583
584impl TryFrom<YAMLThemeNormal> for ThemeNormal {
585    type Error = ThemeColorsParseError;
586
587    fn try_from(value: YAMLThemeNormal) -> Result<Self, Self::Error> {
588        Ok(Self {
589            black: value.black.try_into()?,
590            red: value.red.try_into()?,
591            green: value.green.try_into()?,
592            yellow: value.yellow.try_into()?,
593            blue: value.blue.try_into()?,
594            magenta: value.magenta.try_into()?,
595            cyan: value.cyan.try_into()?,
596            white: value.white.try_into()?,
597        })
598    }
599}
600
601#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
602#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
603pub struct ThemeBright {
604    pub black: ThemeColor,
605    pub red: ThemeColor,
606    pub green: ThemeColor,
607    pub yellow: ThemeColor,
608    pub blue: ThemeColor,
609    pub magenta: ThemeColor,
610    pub cyan: ThemeColor,
611    pub white: ThemeColor,
612}
613
614impl Default for ThemeBright {
615    fn default() -> Self {
616        Self {
617            black: ThemeColor::new_hex(0x56, 0x56, 0x56),
618            red: ThemeColor::new_hex(0xec, 0x53, 0x57),
619            green: ThemeColor::new_hex(0xc0, 0xe1, 0x7d),
620            yellow: ThemeColor::new_hex(0xf9, 0xda, 0x6a),
621            blue: ThemeColor::new_hex(0x49, 0xa4, 0xf8),
622            magenta: ThemeColor::new_hex(0xa4, 0x7d, 0xe9),
623            cyan: ThemeColor::new_hex(0x99, 0xfa, 0xf2),
624            white: default_fff(),
625        }
626    }
627}
628
629impl ThemeBright {
630    fn native() -> Self {
631        Self {
632            black: ThemeColor::new_native(),
633            red: ThemeColor::new_native(),
634            green: ThemeColor::new_native(),
635            yellow: ThemeColor::new_native(),
636            blue: ThemeColor::new_native(),
637            magenta: ThemeColor::new_native(),
638            cyan: ThemeColor::new_native(),
639            white: ThemeColor::new_native(),
640        }
641    }
642}
643
644impl TryFrom<YAMLThemeBright> for ThemeBright {
645    type Error = ThemeColorsParseError;
646
647    fn try_from(value: YAMLThemeBright) -> Result<Self, Self::Error> {
648        Ok(Self {
649            black: value.black.try_into()?,
650            red: value.red.try_into()?,
651            green: value.green.try_into()?,
652            yellow: value.yellow.try_into()?,
653            blue: value.blue.try_into()?,
654            magenta: value.magenta.try_into()?,
655            cyan: value.cyan.try_into()?,
656            white: value.white.try_into()?,
657        })
658    }
659}
660
661#[inline]
662fn default_name() -> String {
663    "empty name".to_string()
664}
665
666#[inline]
667fn default_author() -> String {
668    "empty author".to_string()
669}
670
671#[inline]
672fn default_fff() -> ThemeColor {
673    ThemeColor::new_hex(0xFF, 0xFF, 0xFF)
674}
675
676mod v1_interop {
677    use super::{
678        ThemeBright, ThemeColor, ThemeColorHex, ThemeColors, ThemeCursor, ThemeNormal,
679        ThemePrimary, ThemeWrap,
680    };
681    use crate::config::v1;
682
683    impl From<v1::AlacrittyColor> for ThemeColorHex {
684        fn from(value: v1::AlacrittyColor) -> Self {
685            Self {
686                r: value.r,
687                g: value.g,
688                b: value.b,
689            }
690        }
691    }
692
693    impl From<v1::AlacrittyColor> for ThemeColor {
694        fn from(value: v1::AlacrittyColor) -> Self {
695            Self::Hex(value.into())
696        }
697    }
698
699    impl From<&v1::Alacritty> for ThemeColors {
700        fn from(value: &v1::Alacritty) -> Self {
701            Self {
702                file_name: None,
703                name: value.name.clone(),
704                author: value.author.clone(),
705                primary: ThemePrimary {
706                    background: value.background.into(),
707                    foreground: value.foreground.into(),
708                },
709                cursor: ThemeCursor {
710                    text: value.text.into(),
711                    cursor: value.cursor.into(),
712                },
713                normal: ThemeNormal {
714                    black: value.black.into(),
715                    red: value.red.into(),
716                    green: value.green.into(),
717                    yellow: value.yellow.into(),
718                    blue: value.blue.into(),
719                    magenta: value.magenta.into(),
720                    cyan: value.cyan.into(),
721                    white: value.white.into(),
722                },
723                bright: ThemeBright {
724                    black: value.light_black.into(),
725                    red: value.light_red.into(),
726                    green: value.light_green.into(),
727                    yellow: value.light_yellow.into(),
728                    blue: value.light_blue.into(),
729                    magenta: value.light_magenta.into(),
730                    cyan: value.light_cyan.into(),
731                    white: value.light_white.into(),
732                },
733            }
734        }
735    }
736
737    impl From<&v1::Settings> for ThemeWrap {
738        fn from(value: &v1::Settings) -> Self {
739            Self {
740                theme: (&value.style_color_symbol.alacritty_theme).into(),
741                style: value.into(),
742            }
743        }
744    }
745
746    #[cfg(test)]
747    mod tests {
748        use super::*;
749
750        #[test]
751        fn should_convert_default_without_error() {
752            let converted: ThemeColors = (&v1::StyleColorSymbol::default().alacritty_theme).into();
753
754            assert_eq!(
755                converted,
756                ThemeColors {
757                    file_name: None,
758                    name: "default".into(),
759                    author: "Larry Hao".into(),
760                    primary: ThemePrimary {
761                        background: "#101421".try_into().unwrap(),
762                        foreground: "#fffbf6".try_into().unwrap()
763                    },
764                    cursor: ThemeCursor {
765                        text: "#1E1E1E".try_into().unwrap(),
766                        cursor: "#FFFFFF".try_into().unwrap()
767                    },
768                    normal: ThemeNormal {
769                        black: "#2e2e2e".try_into().unwrap(),
770                        red: "#eb4129".try_into().unwrap(),
771                        green: "#abe047".try_into().unwrap(),
772                        yellow: "#f6c744".try_into().unwrap(),
773                        blue: "#47a0f3".try_into().unwrap(),
774                        magenta: "#7b5cb0".try_into().unwrap(),
775                        cyan: "#64dbed".try_into().unwrap(),
776                        white: "#e5e9f0".try_into().unwrap()
777                    },
778                    bright: ThemeBright {
779                        black: "#565656".try_into().unwrap(),
780                        red: "#ec5357".try_into().unwrap(),
781                        green: "#c0e17d".try_into().unwrap(),
782                        yellow: "#f9da6a".try_into().unwrap(),
783                        blue: "#49a4f8".try_into().unwrap(),
784                        magenta: "#a47de9".try_into().unwrap(),
785                        cyan: "#99faf2".try_into().unwrap(),
786                        white: "#ffffff".try_into().unwrap()
787                    }
788                }
789            );
790        }
791    }
792}
793
794#[cfg(test)]
795mod tests {
796    use super::ThemeColors;
797
798    mod theme_color {
799        use super::super::ThemeColor;
800
801        #[test]
802        fn should_parse_hex() {
803            assert_eq!(
804                ThemeColor::new_hex(1, 2, 3),
805                ThemeColor::try_from("#010203").unwrap()
806            );
807            assert_eq!(
808                ThemeColor::new_hex(1, 2, 3),
809                ThemeColor::try_from("0x010203").unwrap()
810            );
811        }
812
813        #[test]
814        fn should_parse_native() {
815            assert_eq!(
816                ThemeColor::new_native(),
817                ThemeColor::try_from("native").unwrap()
818            );
819        }
820
821        #[test]
822        fn should_serialize() {
823            assert_eq!(ThemeColor::new_hex(1, 2, 3).to_string(), "#010203");
824            assert_eq!(ThemeColor::new_native().to_string(), "native");
825        }
826    }
827
828    mod theme_color_hex {
829        use super::super::ThemeColorHex;
830
831        #[test]
832        fn should_parse_hashtag() {
833            assert_eq!(
834                ThemeColorHex::new(1, 2, 3),
835                ThemeColorHex::from_hex("#010203").unwrap()
836            );
837            assert_eq!(
838                ThemeColorHex::new(255, 255, 255),
839                ThemeColorHex::from_hex("#ffffff").unwrap()
840            );
841            assert_eq!(
842                ThemeColorHex::new(0, 0, 0),
843                ThemeColorHex::from_hex("#000000").unwrap()
844            );
845        }
846
847        #[test]
848        fn should_parse_0x() {
849            assert_eq!(
850                ThemeColorHex::new(1, 2, 3),
851                ThemeColorHex::from_hex("0x010203").unwrap()
852            );
853            assert_eq!(
854                ThemeColorHex::new(255, 255, 255),
855                ThemeColorHex::from_hex("0xffffff").unwrap()
856            );
857            assert_eq!(
858                ThemeColorHex::new(0, 0, 0),
859                ThemeColorHex::from_hex("0x000000").unwrap()
860            );
861        }
862    }
863
864    #[test]
865    fn should_default() {
866        // Test that there are no panics in the defaults, this should be able to be omitted once it is const
867        let _ = ThemeColors::default();
868    }
869}