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

1use serde::{Deserialize, Serialize};
2use tuirealm::props::Color;
3
4/// All values correspond to the Theme's selected color for that
5#[derive(Copy, Clone, Deserialize, Serialize, PartialEq, Eq, Debug)]
6pub enum ColorTermusic {
7    /// Reset to Terminal default (resulting color will depend on what context it is set)
8    Reset = 0,
9    Foreground = 1,
10    Background = 2,
11    Black = 3,
12    Red = 4,
13    Green = 5,
14    Yellow = 6,
15    Blue = 7,
16    Magenta = 8,
17    Cyan = 9,
18    White = 10,
19    LightBlack = 11,
20    LightRed = 12,
21    LightGreen = 13,
22    LightYellow = 14,
23    LightBlue = 15,
24    LightMagenta = 16,
25    LightCyan = 17,
26    LightWhite = 18,
27}
28
29impl AsRef<str> for ColorTermusic {
30    fn as_ref(&self) -> &str {
31        match self {
32            ColorTermusic::Reset => "reset",
33            ColorTermusic::Foreground => "foreground",
34            ColorTermusic::Background => "background",
35            ColorTermusic::Black => "black",
36            ColorTermusic::Red => "red",
37            ColorTermusic::Green => "green",
38            ColorTermusic::Yellow => "yellow",
39            ColorTermusic::Blue => "blue",
40            ColorTermusic::Magenta => "magenta",
41            ColorTermusic::Cyan => "cyan",
42            ColorTermusic::White => "white",
43            ColorTermusic::LightBlack => "bright_black",
44            ColorTermusic::LightRed => "bright_red",
45            ColorTermusic::LightGreen => "bright_green",
46            ColorTermusic::LightYellow => "bright_yellow",
47            ColorTermusic::LightBlue => "bright_blue",
48            ColorTermusic::LightMagenta => "bright_magenta",
49            ColorTermusic::LightCyan => "bright_cyan",
50            ColorTermusic::LightWhite => "bright_white",
51        }
52    }
53}
54
55impl ColorTermusic {
56    #[must_use]
57    pub const fn as_usize(self) -> usize {
58        self as usize
59    }
60}
61
62impl From<ColorTermusic> for Color {
63    fn from(value: ColorTermusic) -> Self {
64        match value {
65            ColorTermusic::Reset => Color::Reset,
66            ColorTermusic::Background | ColorTermusic::Black => Color::Black,
67            ColorTermusic::Red => Color::Red,
68            ColorTermusic::Green => Color::Green,
69            ColorTermusic::Yellow => Color::Yellow,
70            ColorTermusic::Blue => Color::Blue,
71            ColorTermusic::Magenta => Color::Magenta,
72            ColorTermusic::Cyan => Color::Cyan,
73            ColorTermusic::White => Color::Gray,
74            ColorTermusic::LightBlack => Color::DarkGray,
75            ColorTermusic::LightRed => Color::LightRed,
76            ColorTermusic::LightGreen => Color::LightGreen,
77            ColorTermusic::LightYellow => Color::LightYellow,
78            ColorTermusic::LightBlue => Color::LightBlue,
79            ColorTermusic::LightMagenta => Color::LightMagenta,
80            ColorTermusic::LightCyan => Color::LightCyan,
81            ColorTermusic::Foreground | ColorTermusic::LightWhite => Color::White,
82        }
83    }
84}
85
86/// Style for the Library view
87#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Default)]
88#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
89pub struct Styles {
90    pub library: StyleLibrary,
91    pub playlist: StylePlaylist,
92    pub lyric: StyleLyric,
93    pub progress: StyleProgress,
94    pub important_popup: StyleImportantPopup,
95    pub fallback: StyleFallback,
96}
97
98/// Style for the Library view
99#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
100#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
101pub struct StyleLibrary {
102    /// Music Library foreground color (text)
103    pub foreground_color: ColorTermusic,
104    /// Music Library background color (background)
105    pub background_color: ColorTermusic,
106    /// Music Library border color (when focused)
107    pub border_color: ColorTermusic,
108    /// Music Library selected node highlight color
109    pub highlight_color: ColorTermusic,
110
111    /// Music Library selected node highlight symbol
112    pub highlight_symbol: String,
113}
114
115impl Default for StyleLibrary {
116    fn default() -> Self {
117        Self {
118            foreground_color: ColorTermusic::Foreground,
119            background_color: ColorTermusic::Reset,
120            border_color: ColorTermusic::Blue,
121            highlight_color: ColorTermusic::LightYellow,
122
123            highlight_symbol: "🦄".into(),
124        }
125    }
126}
127
128/// Style for the Playlist Widget
129#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
130#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
131pub struct StylePlaylist {
132    /// Playlist foreground color (text)
133    pub foreground_color: ColorTermusic,
134    /// Playlist background color (text)
135    pub background_color: ColorTermusic,
136    /// Playlist border color (when focused)
137    pub border_color: ColorTermusic,
138    /// Playlist selected node highlight color
139    pub highlight_color: ColorTermusic,
140
141    /// Playlist selected track highlight symbol
142    pub highlight_symbol: String,
143    /// Playlist current playing track symbol
144    pub current_track_symbol: String,
145
146    /// If enabled use a symbol for the Loop-Mode, otherwise use text
147    ///
148    /// Example: true -> "Mode: 🔁"; false -> "Mode: playlist"
149    pub use_loop_mode_symbol: bool,
150}
151
152impl Default for StylePlaylist {
153    fn default() -> Self {
154        Self {
155            foreground_color: ColorTermusic::Foreground,
156            background_color: ColorTermusic::Reset,
157            border_color: ColorTermusic::Blue,
158            highlight_color: ColorTermusic::LightYellow,
159
160            highlight_symbol: "🚀".into(),
161            current_track_symbol: "►".into(),
162
163            use_loop_mode_symbol: true,
164        }
165    }
166}
167
168/// Style for the Lyric text view widget (also the radio text)
169#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
170#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
171pub struct StyleLyric {
172    /// Lyrics foreground color (text)
173    pub foreground_color: ColorTermusic,
174    /// Lyrics background color (background)
175    pub background_color: ColorTermusic,
176    /// Lyrics border color (when focused)
177    pub border_color: ColorTermusic,
178}
179
180impl Default for StyleLyric {
181    fn default() -> Self {
182        Self {
183            foreground_color: ColorTermusic::Foreground,
184            background_color: ColorTermusic::Reset,
185            border_color: ColorTermusic::Blue,
186        }
187    }
188}
189
190/// Style for the Player Progress widget
191#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
192#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
193pub struct StyleProgress {
194    /// Track Progressbar foreground color (text)
195    pub foreground_color: ColorTermusic,
196    /// Track Progressbar background color (background)
197    pub background_color: ColorTermusic,
198    /// Track Progressbar border (always)
199    pub border_color: ColorTermusic,
200}
201
202impl Default for StyleProgress {
203    fn default() -> Self {
204        Self {
205            foreground_color: ColorTermusic::LightBlack,
206            background_color: ColorTermusic::Reset,
207            border_color: ColorTermusic::Blue,
208        }
209    }
210}
211
212/// Style for Important Popups (quit, save config, delete, NOT Error)
213#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
214#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
215pub struct StyleImportantPopup {
216    /// Important Popup (like Error or Delete) foreground color (text)
217    pub foreground_color: ColorTermusic,
218    /// Important Popup (like Error or Delete) background color (background)
219    pub background_color: ColorTermusic,
220    /// Important Popup (like Error or Delete) border color (always)
221    pub border_color: ColorTermusic,
222}
223
224impl Default for StyleImportantPopup {
225    fn default() -> Self {
226        Self {
227            foreground_color: ColorTermusic::Yellow,
228            background_color: ColorTermusic::Reset,
229            border_color: ColorTermusic::Yellow,
230        }
231    }
232}
233
234/// Generic is when there is no specific config entry for it, like the `AskQuit` popup
235#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
236#[serde(default)] // allow missing fields and fill them with the `..Self::default()` in this struct
237pub struct StyleFallback {
238    /// Generic foreground color (text)
239    pub foreground_color: ColorTermusic,
240    /// Generic background color (background)
241    pub background_color: ColorTermusic,
242    /// Generic border color (always)
243    pub border_color: ColorTermusic,
244    /// Generic Highlight color
245    pub highlight_color: ColorTermusic,
246}
247
248impl Default for StyleFallback {
249    fn default() -> Self {
250        Self {
251            foreground_color: ColorTermusic::Foreground,
252            background_color: ColorTermusic::Reset,
253            border_color: ColorTermusic::Blue,
254            highlight_color: ColorTermusic::LightYellow,
255        }
256    }
257}
258
259mod v1_interop {
260    use super::{
261        ColorTermusic, StyleFallback, StyleImportantPopup, StyleLibrary, StyleLyric, StylePlaylist,
262        StyleProgress, Styles,
263    };
264    use crate::config::v1;
265
266    impl From<v1::ColorTermusic> for ColorTermusic {
267        fn from(value: v1::ColorTermusic) -> Self {
268            match value {
269                v1::ColorTermusic::Reset => Self::Reset,
270                v1::ColorTermusic::Foreground => Self::Foreground,
271                v1::ColorTermusic::Background => Self::Background,
272                v1::ColorTermusic::Black => Self::Black,
273                v1::ColorTermusic::Red => Self::Red,
274                v1::ColorTermusic::Green => Self::Green,
275                v1::ColorTermusic::Yellow => Self::Yellow,
276                v1::ColorTermusic::Blue => Self::Blue,
277                v1::ColorTermusic::Magenta => Self::Magenta,
278                v1::ColorTermusic::Cyan => Self::Cyan,
279                v1::ColorTermusic::White => Self::White,
280                v1::ColorTermusic::LightBlack => Self::LightBlack,
281                v1::ColorTermusic::LightRed => Self::LightRed,
282                v1::ColorTermusic::LightGreen => Self::LightGreen,
283                v1::ColorTermusic::LightYellow => Self::LightYellow,
284                v1::ColorTermusic::LightBlue => Self::LightBlue,
285                v1::ColorTermusic::LightMagenta => Self::LightMagenta,
286                v1::ColorTermusic::LightCyan => Self::LightCyan,
287                v1::ColorTermusic::LightWhite => Self::LightWhite,
288            }
289        }
290    }
291
292    impl From<&v1::StyleColorSymbol> for StyleLibrary {
293        fn from(value: &v1::StyleColorSymbol) -> Self {
294            Self {
295                foreground_color: value.library_foreground.into(),
296                background_color: value.library_background.into(),
297                border_color: value.library_border.into(),
298                highlight_color: value.library_highlight.into(),
299
300                highlight_symbol: value.library_highlight_symbol.clone(),
301            }
302        }
303    }
304
305    impl From<&v1::Settings> for StylePlaylist {
306        fn from(value: &v1::Settings) -> Self {
307            let use_loop_mode_symbol = value.playlist_display_symbol;
308            let value = &value.style_color_symbol;
309            Self {
310                foreground_color: value.playlist_foreground.into(),
311                background_color: value.playlist_background.into(),
312                border_color: value.playlist_border.into(),
313                highlight_color: value.playlist_highlight.into(),
314                highlight_symbol: value.playlist_highlight_symbol.clone(),
315                current_track_symbol: value.currently_playing_track_symbol.clone(),
316                use_loop_mode_symbol,
317            }
318        }
319    }
320
321    impl From<&v1::StyleColorSymbol> for StyleLyric {
322        fn from(value: &v1::StyleColorSymbol) -> Self {
323            Self {
324                foreground_color: value.lyric_foreground.into(),
325                background_color: value.lyric_background.into(),
326                border_color: value.lyric_border.into(),
327            }
328        }
329    }
330
331    impl From<&v1::StyleColorSymbol> for StyleProgress {
332        fn from(value: &v1::StyleColorSymbol) -> Self {
333            Self {
334                foreground_color: value.progress_foreground.into(),
335                background_color: value.progress_background.into(),
336                border_color: value.progress_border.into(),
337            }
338        }
339    }
340
341    impl From<&v1::StyleColorSymbol> for StyleImportantPopup {
342        fn from(value: &v1::StyleColorSymbol) -> Self {
343            Self {
344                foreground_color: value.important_popup_foreground.into(),
345                background_color: value.important_popup_background.into(),
346                border_color: value.important_popup_border.into(),
347            }
348        }
349    }
350
351    impl From<&v1::StyleColorSymbol> for StyleFallback {
352        fn from(value: &v1::StyleColorSymbol) -> Self {
353            Self {
354                foreground_color: value.fallback_foreground.into(),
355                background_color: value.fallback_background.into(),
356                border_color: value.fallback_border.into(),
357                highlight_color: value.fallback_highlight.into(),
358            }
359        }
360    }
361
362    impl From<&v1::Settings> for Styles {
363        fn from(value: &v1::Settings) -> Self {
364            let playlist = value.into();
365            let value = &value.style_color_symbol;
366            Self {
367                library: value.into(),
368                playlist,
369                lyric: value.into(),
370                progress: value.into(),
371                important_popup: value.into(),
372                fallback: value.into(),
373            }
374        }
375    }
376
377    #[cfg(test)]
378    mod tests {
379        use super::*;
380
381        #[test]
382        fn should_convert_default_without_error() {
383            let converted: Styles = (&v1::Settings::default()).into();
384
385            let expected_library = StyleLibrary {
386                foreground_color: ColorTermusic::Foreground,
387                background_color: ColorTermusic::Reset,
388                border_color: ColorTermusic::Blue,
389                highlight_color: ColorTermusic::LightYellow,
390
391                highlight_symbol: "🦄".into(),
392            };
393            assert_eq!(converted.library, expected_library);
394
395            let expected_playlist = StylePlaylist {
396                foreground_color: ColorTermusic::Foreground,
397                background_color: ColorTermusic::Reset,
398                border_color: ColorTermusic::Blue,
399                highlight_color: ColorTermusic::LightYellow,
400
401                highlight_symbol: "🚀".into(),
402                current_track_symbol: "►".into(),
403                use_loop_mode_symbol: true,
404            };
405            assert_eq!(converted.playlist, expected_playlist);
406
407            let expected_lyric = StyleLyric {
408                foreground_color: ColorTermusic::Foreground,
409                background_color: ColorTermusic::Reset,
410                border_color: ColorTermusic::Blue,
411            };
412            assert_eq!(converted.lyric, expected_lyric);
413
414            let expected_progress = StyleProgress {
415                foreground_color: ColorTermusic::LightBlack,
416                background_color: ColorTermusic::Reset,
417                border_color: ColorTermusic::Blue,
418            };
419            assert_eq!(converted.progress, expected_progress);
420
421            let expected_important_popup = StyleImportantPopup {
422                foreground_color: ColorTermusic::Yellow,
423                background_color: ColorTermusic::Reset,
424                border_color: ColorTermusic::Yellow,
425            };
426            assert_eq!(converted.important_popup, expected_important_popup);
427
428            let expected_fallback = StyleFallback {
429                foreground_color: ColorTermusic::Foreground,
430                background_color: ColorTermusic::Reset,
431                border_color: ColorTermusic::Blue,
432                highlight_color: ColorTermusic::LightYellow,
433            };
434            assert_eq!(converted.fallback, expected_fallback);
435
436            assert_eq!(
437                converted,
438                Styles {
439                    library: expected_library,
440                    playlist: expected_playlist,
441                    lyric: expected_lyric,
442                    progress: expected_progress,
443                    important_popup: expected_important_popup,
444                    fallback: expected_fallback
445                }
446            );
447        }
448    }
449}