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("Failed to parse color because of incorrect length {0}, expected prefix \"#\" or \"0x\" and length 6")]
290    IncorrectLength(usize),
291    #[error("Failed to parse color becazse of unknown prefix \"{0}\", expected \"#\" or \"0x\"")]
292    UnknownPrefix(String),
293}
294
295/// The rgb colors
296#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq)]
297#[serde(try_from = "String")]
298#[serde(into = "String")]
299pub struct ThemeColorHex {
300    pub r: u8,
301    pub g: u8,
302    pub b: u8,
303}
304
305impl ThemeColorHex {
306    /// Create a new instance with those values
307    #[must_use]
308    pub const fn new(r: u8, g: u8, b: u8) -> Self {
309        Self { r, g, b }
310    }
311
312    /// Convert from a prefix + 6 length string
313    pub fn from_hex(val: &str) -> Result<Self, ThemeColorHexParseError> {
314        let Some(without_prefix) = val.strip_prefix('#').or(val.strip_prefix("0x")) else {
315            return Err(ThemeColorHexParseError::UnknownPrefix(val.to_string()));
316        };
317
318        // not in a format we support
319        if without_prefix.len() != 6 {
320            return Err(ThemeColorHexParseError::IncorrectLength(
321                without_prefix.len(),
322            ));
323        }
324
325        let r = u8::from_str_radix(&without_prefix[0..=1], 16)
326            .map_err(ThemeColorHexParseError::ParseIntError)?;
327        let g = u8::from_str_radix(&without_prefix[2..=3], 16)
328            .map_err(ThemeColorHexParseError::ParseIntError)?;
329        let b = u8::from_str_radix(&without_prefix[4..=5], 16)
330            .map_err(ThemeColorHexParseError::ParseIntError)?;
331
332        Ok(Self { r, g, b })
333    }
334
335    /// Convert to hex prefix + 6 length string
336    #[inline]
337    #[must_use]
338    pub fn to_hex(&self) -> String {
339        format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
340    }
341}
342
343impl TryFrom<String> for ThemeColorHex {
344    type Error = ThemeColorHexParseError;
345
346    fn try_from(value: String) -> Result<Self, Self::Error> {
347        Self::from_hex(&value)
348    }
349}
350
351impl TryFrom<&str> for ThemeColorHex {
352    type Error = ThemeColorHexParseError;
353
354    fn try_from(value: &str) -> Result<Self, Self::Error> {
355        Self::from_hex(value)
356    }
357}
358
359impl From<ThemeColorHex> for String {
360    fn from(val: ThemeColorHex) -> Self {
361        ThemeColorHex::to_hex(&val)
362    }
363}
364
365impl From<ThemeColorHex> for Color {
366    fn from(val: ThemeColorHex) -> Self {
367        Color::Rgb(val.r, val.g, val.b)
368    }
369}
370
371#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
372#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
373pub struct ThemeColors {
374    /// The Filename of the current theme, if a file is used.
375    /// This value is skipped if empty.
376    ///
377    /// This is used for example to pre-select in the config editor if available.
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub file_name: Option<String>,
380    pub name: String,
381    pub author: String,
382    pub primary: ThemePrimary,
383    pub cursor: ThemeCursor,
384    pub normal: ThemeNormal,
385    pub bright: ThemeBright,
386}
387
388impl Default for ThemeColors {
389    fn default() -> Self {
390        Self {
391            file_name: None,
392            name: default_name(),
393            author: default_author(),
394            primary: ThemePrimary::default(),
395            cursor: ThemeCursor::default(),
396            normal: ThemeNormal::default(),
397            bright: ThemeBright::default(),
398        }
399    }
400}
401
402impl ThemeColors {
403    /// Get the full default theme, including names.
404    ///
405    /// This function is different from [`Self::default`] as the trait impl is also used for filling empty places
406    #[must_use]
407    pub fn full_default() -> Self {
408        Self {
409            name: "Termusic Default".to_string(),
410            author: "Termusic Developers".to_string(),
411            ..Default::default()
412        }
413    }
414
415    /// Get a full native theme.
416    #[must_use]
417    pub fn full_native() -> Self {
418        Self {
419            file_name: None,
420            name: "Native".to_string(),
421            author: "Termusic Developers".to_string(),
422            primary: ThemePrimary::native(),
423            cursor: ThemeCursor::native(),
424            normal: ThemeNormal::native(),
425            bright: ThemeBright::native(),
426        }
427    }
428}
429
430/// Error for when [`ThemeColors`] parsing fails
431#[derive(Debug, Clone, PartialEq, thiserror::Error)]
432pub enum ThemeColorsParseError {
433    #[error("Failed to parse Theme: {0}")]
434    ThemeColor(#[from] ThemeColorParseError),
435}
436
437impl TryFrom<YAMLTheme> for ThemeColors {
438    type Error = ThemeColorsParseError;
439
440    fn try_from(value: YAMLTheme) -> Result<Self, Self::Error> {
441        let colors = value.colors;
442        Ok(Self {
443            file_name: None,
444            name: colors.name.unwrap_or_else(default_name),
445            author: colors.author.unwrap_or_else(default_author),
446            primary: colors.primary.try_into()?,
447            cursor: colors.cursor.try_into()?,
448            normal: colors.normal.try_into()?,
449            bright: colors.bright.try_into()?,
450        })
451    }
452}
453
454impl ThemeColors {
455    /// Load a YAML Theme and then convert it to a [`ThemeColors`] instance
456    pub fn from_yaml_file(path: &Path) -> anyhow::Result<Self> {
457        let parsed: YAMLTheme = serde_yaml::from_reader(BufReader::new(File::open(path)?))?;
458
459        let mut theme = Self::try_from(parsed)?;
460
461        let file_name = path.file_stem();
462        theme.file_name = file_name.map(|v| v.to_string_lossy().to_string());
463
464        Ok(theme)
465    }
466}
467
468#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
469pub struct ThemePrimary {
470    pub background: ThemeColor,
471    pub foreground: ThemeColor,
472}
473
474impl Default for ThemePrimary {
475    fn default() -> Self {
476        Self {
477            background: ThemeColor::new_hex(0x10, 0x14, 0x21),
478            foreground: ThemeColor::new_hex(0xff, 0xfb, 0xf6),
479        }
480    }
481}
482
483impl ThemePrimary {
484    fn native() -> Self {
485        Self {
486            background: ThemeColor::new_native(),
487            foreground: ThemeColor::new_native(),
488        }
489    }
490}
491
492impl TryFrom<YAMLThemePrimary> for ThemePrimary {
493    type Error = ThemeColorsParseError;
494
495    fn try_from(value: YAMLThemePrimary) -> Result<Self, Self::Error> {
496        Ok(Self {
497            background: value.background.try_into()?,
498            foreground: value.foreground.try_into()?,
499        })
500    }
501}
502
503#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
504#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
505pub struct ThemeCursor {
506    pub text: ThemeColor,
507    pub cursor: ThemeColor,
508}
509
510impl Default for ThemeCursor {
511    fn default() -> Self {
512        Self {
513            text: ThemeColor::new_hex(0x1e, 0x1e, 0x1e),
514            cursor: default_fff(),
515        }
516    }
517}
518
519impl ThemeCursor {
520    fn native() -> Self {
521        Self {
522            text: ThemeColor::new_native(),
523            cursor: ThemeColor::new_native(),
524        }
525    }
526}
527
528impl TryFrom<YAMLThemeCursor> for ThemeCursor {
529    type Error = ThemeColorsParseError;
530
531    fn try_from(value: YAMLThemeCursor) -> Result<Self, Self::Error> {
532        Ok(Self {
533            text: value.text.try_into()?,
534            cursor: value.cursor.try_into()?,
535        })
536    }
537}
538
539#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
540#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
541pub struct ThemeNormal {
542    pub black: ThemeColor,
543    pub red: ThemeColor,
544    pub green: ThemeColor,
545    pub yellow: ThemeColor,
546    pub blue: ThemeColor,
547    pub magenta: ThemeColor,
548    pub cyan: ThemeColor,
549    pub white: ThemeColor,
550}
551
552impl Default for ThemeNormal {
553    fn default() -> Self {
554        Self {
555            black: ThemeColor::new_hex(0x2e, 0x2e, 0x2e),
556            red: ThemeColor::new_hex(0xeb, 0x41, 0x29),
557            green: ThemeColor::new_hex(0xab, 0xe0, 0x47),
558            yellow: ThemeColor::new_hex(0xf6, 0xc7, 0x44),
559            blue: ThemeColor::new_hex(0x47, 0xa0, 0xf3),
560            magenta: ThemeColor::new_hex(0x7b, 0x5c, 0xb0),
561            cyan: ThemeColor::new_hex(0x64, 0xdb, 0xed),
562            white: ThemeColor::new_hex(0xe5, 0xe9, 0xf0),
563        }
564    }
565}
566
567impl ThemeNormal {
568    fn native() -> Self {
569        Self {
570            black: ThemeColor::new_native(),
571            red: ThemeColor::new_native(),
572            green: ThemeColor::new_native(),
573            yellow: ThemeColor::new_native(),
574            blue: ThemeColor::new_native(),
575            magenta: ThemeColor::new_native(),
576            cyan: ThemeColor::new_native(),
577            white: ThemeColor::new_native(),
578        }
579    }
580}
581
582impl TryFrom<YAMLThemeNormal> for ThemeNormal {
583    type Error = ThemeColorsParseError;
584
585    fn try_from(value: YAMLThemeNormal) -> Result<Self, Self::Error> {
586        Ok(Self {
587            black: value.black.try_into()?,
588            red: value.red.try_into()?,
589            green: value.green.try_into()?,
590            yellow: value.yellow.try_into()?,
591            blue: value.blue.try_into()?,
592            magenta: value.magenta.try_into()?,
593            cyan: value.cyan.try_into()?,
594            white: value.white.try_into()?,
595        })
596    }
597}
598
599#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
600#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
601pub struct ThemeBright {
602    pub black: ThemeColor,
603    pub red: ThemeColor,
604    pub green: ThemeColor,
605    pub yellow: ThemeColor,
606    pub blue: ThemeColor,
607    pub magenta: ThemeColor,
608    pub cyan: ThemeColor,
609    pub white: ThemeColor,
610}
611
612impl Default for ThemeBright {
613    fn default() -> Self {
614        Self {
615            black: ThemeColor::new_hex(0x56, 0x56, 0x56),
616            red: ThemeColor::new_hex(0xec, 0x53, 0x57),
617            green: ThemeColor::new_hex(0xc0, 0xe1, 0x7d),
618            yellow: ThemeColor::new_hex(0xf9, 0xda, 0x6a),
619            blue: ThemeColor::new_hex(0x49, 0xa4, 0xf8),
620            magenta: ThemeColor::new_hex(0xa4, 0x7d, 0xe9),
621            cyan: ThemeColor::new_hex(0x99, 0xfa, 0xf2),
622            white: default_fff(),
623        }
624    }
625}
626
627impl ThemeBright {
628    fn native() -> Self {
629        Self {
630            black: ThemeColor::new_native(),
631            red: ThemeColor::new_native(),
632            green: ThemeColor::new_native(),
633            yellow: ThemeColor::new_native(),
634            blue: ThemeColor::new_native(),
635            magenta: ThemeColor::new_native(),
636            cyan: ThemeColor::new_native(),
637            white: ThemeColor::new_native(),
638        }
639    }
640}
641
642impl TryFrom<YAMLThemeBright> for ThemeBright {
643    type Error = ThemeColorsParseError;
644
645    fn try_from(value: YAMLThemeBright) -> Result<Self, Self::Error> {
646        Ok(Self {
647            black: value.black.try_into()?,
648            red: value.red.try_into()?,
649            green: value.green.try_into()?,
650            yellow: value.yellow.try_into()?,
651            blue: value.blue.try_into()?,
652            magenta: value.magenta.try_into()?,
653            cyan: value.cyan.try_into()?,
654            white: value.white.try_into()?,
655        })
656    }
657}
658
659#[inline]
660fn default_name() -> String {
661    "empty name".to_string()
662}
663
664#[inline]
665fn default_author() -> String {
666    "empty author".to_string()
667}
668
669#[inline]
670fn default_fff() -> ThemeColor {
671    ThemeColor::new_hex(0xFF, 0xFF, 0xFF)
672}
673
674mod v1_interop {
675    use super::{
676        ThemeBright, ThemeColor, ThemeColorHex, ThemeColors, ThemeCursor, ThemeNormal,
677        ThemePrimary, ThemeWrap,
678    };
679    use crate::config::v1;
680
681    impl From<v1::AlacrittyColor> for ThemeColorHex {
682        fn from(value: v1::AlacrittyColor) -> Self {
683            Self {
684                r: value.r,
685                g: value.g,
686                b: value.b,
687            }
688        }
689    }
690
691    impl From<v1::AlacrittyColor> for ThemeColor {
692        fn from(value: v1::AlacrittyColor) -> Self {
693            Self::Hex(value.into())
694        }
695    }
696
697    impl From<&v1::Alacritty> for ThemeColors {
698        fn from(value: &v1::Alacritty) -> Self {
699            Self {
700                file_name: None,
701                name: value.name.clone(),
702                author: value.author.clone(),
703                primary: ThemePrimary {
704                    background: value.background.into(),
705                    foreground: value.foreground.into(),
706                },
707                cursor: ThemeCursor {
708                    text: value.text.into(),
709                    cursor: value.cursor.into(),
710                },
711                normal: ThemeNormal {
712                    black: value.black.into(),
713                    red: value.red.into(),
714                    green: value.green.into(),
715                    yellow: value.yellow.into(),
716                    blue: value.blue.into(),
717                    magenta: value.magenta.into(),
718                    cyan: value.cyan.into(),
719                    white: value.white.into(),
720                },
721                bright: ThemeBright {
722                    black: value.light_black.into(),
723                    red: value.light_red.into(),
724                    green: value.light_green.into(),
725                    yellow: value.light_yellow.into(),
726                    blue: value.light_blue.into(),
727                    magenta: value.light_magenta.into(),
728                    cyan: value.light_cyan.into(),
729                    white: value.light_white.into(),
730                },
731            }
732        }
733    }
734
735    impl From<&v1::Settings> for ThemeWrap {
736        fn from(value: &v1::Settings) -> Self {
737            Self {
738                theme: (&value.style_color_symbol.alacritty_theme).into(),
739                style: value.into(),
740            }
741        }
742    }
743
744    #[cfg(test)]
745    mod tests {
746        use super::*;
747
748        #[test]
749        fn should_convert_default_without_error() {
750            let converted: ThemeColors = (&v1::StyleColorSymbol::default().alacritty_theme).into();
751
752            assert_eq!(
753                converted,
754                ThemeColors {
755                    file_name: None,
756                    name: "default".into(),
757                    author: "Larry Hao".into(),
758                    primary: ThemePrimary {
759                        background: "#101421".try_into().unwrap(),
760                        foreground: "#fffbf6".try_into().unwrap()
761                    },
762                    cursor: ThemeCursor {
763                        text: "#1E1E1E".try_into().unwrap(),
764                        cursor: "#FFFFFF".try_into().unwrap()
765                    },
766                    normal: ThemeNormal {
767                        black: "#2e2e2e".try_into().unwrap(),
768                        red: "#eb4129".try_into().unwrap(),
769                        green: "#abe047".try_into().unwrap(),
770                        yellow: "#f6c744".try_into().unwrap(),
771                        blue: "#47a0f3".try_into().unwrap(),
772                        magenta: "#7b5cb0".try_into().unwrap(),
773                        cyan: "#64dbed".try_into().unwrap(),
774                        white: "#e5e9f0".try_into().unwrap()
775                    },
776                    bright: ThemeBright {
777                        black: "#565656".try_into().unwrap(),
778                        red: "#ec5357".try_into().unwrap(),
779                        green: "#c0e17d".try_into().unwrap(),
780                        yellow: "#f9da6a".try_into().unwrap(),
781                        blue: "#49a4f8".try_into().unwrap(),
782                        magenta: "#a47de9".try_into().unwrap(),
783                        cyan: "#99faf2".try_into().unwrap(),
784                        white: "#ffffff".try_into().unwrap()
785                    }
786                }
787            );
788        }
789    }
790}
791
792#[cfg(test)]
793mod tests {
794    use super::ThemeColors;
795
796    mod theme_color {
797        use super::super::ThemeColor;
798
799        #[test]
800        fn should_parse_hex() {
801            assert_eq!(
802                ThemeColor::new_hex(1, 2, 3),
803                ThemeColor::try_from("#010203").unwrap()
804            );
805            assert_eq!(
806                ThemeColor::new_hex(1, 2, 3),
807                ThemeColor::try_from("0x010203").unwrap()
808            );
809        }
810
811        #[test]
812        fn should_parse_native() {
813            assert_eq!(
814                ThemeColor::new_native(),
815                ThemeColor::try_from("native").unwrap()
816            );
817        }
818
819        #[test]
820        fn should_serialize() {
821            assert_eq!(ThemeColor::new_hex(1, 2, 3).to_string(), "#010203");
822            assert_eq!(ThemeColor::new_native().to_string(), "native");
823        }
824    }
825
826    mod theme_color_hex {
827        use super::super::ThemeColorHex;
828
829        #[test]
830        fn should_parse_hashtag() {
831            assert_eq!(
832                ThemeColorHex::new(1, 2, 3),
833                ThemeColorHex::from_hex("#010203").unwrap()
834            );
835            assert_eq!(
836                ThemeColorHex::new(255, 255, 255),
837                ThemeColorHex::from_hex("#ffffff").unwrap()
838            );
839            assert_eq!(
840                ThemeColorHex::new(0, 0, 0),
841                ThemeColorHex::from_hex("#000000").unwrap()
842            );
843        }
844
845        #[test]
846        fn should_parse_0x() {
847            assert_eq!(
848                ThemeColorHex::new(1, 2, 3),
849                ThemeColorHex::from_hex("0x010203").unwrap()
850            );
851            assert_eq!(
852                ThemeColorHex::new(255, 255, 255),
853                ThemeColorHex::from_hex("0xffffff").unwrap()
854            );
855            assert_eq!(
856                ThemeColorHex::new(0, 0, 0),
857                ThemeColorHex::from_hex("0x000000").unwrap()
858            );
859        }
860    }
861
862    #[test]
863    fn should_default() {
864        // Test that there are no panics in the defaults, this should be able to be omitted once it is const
865        let _ = ThemeColors::default();
866    }
867}