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::{
9 v1::AlacrittyColor,
10 yaml_theme::{YAMLTheme, YAMLThemeBright, YAMLThemeCursor, YAMLThemeNormal, YAMLThemePrimary},
11};
12
13use styles::ColorTermusic;
14
15pub mod styles;
16
17#[derive(Debug, Clone, Deserialize, Serialize, Default, PartialEq)]
20#[serde(default)] pub struct ThemeWrap {
22 pub style: styles::Styles,
23 #[serde(default = "ThemeColors::full_default")]
26 pub theme: ThemeColors,
27}
28
29impl ThemeWrap {
30 #[must_use]
31 pub fn get_color_from_theme(&self, color: ColorTermusic) -> Color {
32 match color {
33 ColorTermusic::Reset => Color::Reset,
34 ColorTermusic::Foreground => self.theme.primary.foreground.into(),
35 ColorTermusic::Background => self.theme.primary.background.into(),
36 ColorTermusic::Black => self.theme.normal.black.into(),
37 ColorTermusic::Red => self.theme.normal.red.into(),
38 ColorTermusic::Green => self.theme.normal.green.into(),
39 ColorTermusic::Yellow => self.theme.normal.yellow.into(),
40 ColorTermusic::Blue => self.theme.normal.blue.into(),
41 ColorTermusic::Magenta => self.theme.normal.magenta.into(),
42 ColorTermusic::Cyan => self.theme.normal.cyan.into(),
43 ColorTermusic::White => self.theme.normal.white.into(),
44 ColorTermusic::LightBlack => self.theme.bright.black.into(),
45 ColorTermusic::LightRed => self.theme.bright.red.into(),
46 ColorTermusic::LightGreen => self.theme.bright.green.into(),
47 ColorTermusic::LightYellow => self.theme.bright.yellow.into(),
48 ColorTermusic::LightBlue => self.theme.bright.blue.into(),
49 ColorTermusic::LightMagenta => self.theme.bright.magenta.into(),
50 ColorTermusic::LightCyan => self.theme.bright.cyan.into(),
51 ColorTermusic::LightWhite => self.theme.bright.white.into(),
52 }
53 }
54
55 #[inline]
56 #[must_use]
57 pub fn library_foreground(&self) -> Color {
58 self.get_color_from_theme(self.style.library.foreground_color)
59 }
60
61 #[inline]
62 #[must_use]
63 pub fn library_background(&self) -> Color {
64 self.get_color_from_theme(self.style.library.background_color)
65 }
66
67 #[inline]
68 #[must_use]
69 pub fn library_highlight(&self) -> Color {
70 self.get_color_from_theme(self.style.library.highlight_color)
71 }
72
73 #[inline]
74 #[must_use]
75 pub fn library_border(&self) -> Color {
76 self.get_color_from_theme(self.style.library.border_color)
77 }
78
79 #[inline]
80 #[must_use]
81 pub fn playlist_foreground(&self) -> Color {
82 self.get_color_from_theme(self.style.playlist.foreground_color)
83 }
84
85 #[inline]
86 #[must_use]
87 pub fn playlist_background(&self) -> Color {
88 self.get_color_from_theme(self.style.playlist.background_color)
89 }
90
91 #[inline]
92 #[must_use]
93 pub fn playlist_highlight(&self) -> Color {
94 self.get_color_from_theme(self.style.playlist.highlight_color)
95 }
96
97 #[inline]
98 #[must_use]
99 pub fn playlist_border(&self) -> Color {
100 self.get_color_from_theme(self.style.playlist.border_color)
101 }
102
103 #[inline]
104 #[must_use]
105 pub fn progress_foreground(&self) -> Color {
106 self.get_color_from_theme(self.style.progress.foreground_color)
107 }
108
109 #[inline]
110 #[must_use]
111 pub fn progress_background(&self) -> Color {
112 self.get_color_from_theme(self.style.progress.background_color)
113 }
114
115 #[inline]
116 #[must_use]
117 pub fn progress_border(&self) -> Color {
118 self.get_color_from_theme(self.style.progress.border_color)
119 }
120
121 #[inline]
122 #[must_use]
123 pub fn lyric_foreground(&self) -> Color {
124 self.get_color_from_theme(self.style.lyric.foreground_color)
125 }
126
127 #[inline]
128 #[must_use]
129 pub fn lyric_background(&self) -> Color {
130 self.get_color_from_theme(self.style.lyric.background_color)
131 }
132
133 #[inline]
134 #[must_use]
135 pub fn lyric_border(&self) -> Color {
136 self.get_color_from_theme(self.style.lyric.border_color)
137 }
138
139 #[inline]
140 #[must_use]
141 pub fn important_popup_foreground(&self) -> Color {
142 self.get_color_from_theme(self.style.important_popup.foreground_color)
143 }
144
145 #[inline]
146 #[must_use]
147 pub fn important_popup_background(&self) -> Color {
148 self.get_color_from_theme(self.style.important_popup.background_color)
149 }
150
151 #[inline]
152 #[must_use]
153 pub fn important_popup_border(&self) -> Color {
154 self.get_color_from_theme(self.style.important_popup.border_color)
155 }
156
157 #[inline]
158 #[must_use]
159 pub fn fallback_foreground(&self) -> Color {
160 self.get_color_from_theme(self.style.fallback.foreground_color)
161 }
162
163 #[inline]
164 #[must_use]
165 pub fn fallback_background(&self) -> Color {
166 self.get_color_from_theme(self.style.fallback.background_color)
167 }
168
169 #[inline]
170 #[must_use]
171 pub fn fallback_highlight(&self) -> Color {
172 self.get_color_from_theme(self.style.fallback.highlight_color)
173 }
174
175 #[inline]
176 #[must_use]
177 pub fn fallback_border(&self) -> Color {
178 self.get_color_from_theme(self.style.fallback.border_color)
179 }
180}
181
182#[derive(Debug, Clone, PartialEq, thiserror::Error)]
184pub enum ThemeColorParseError {
185 #[error("Failed to parse color because of {0}")]
186 ParseIntError(#[from] ParseIntError),
187 #[error("Failed to parse color because of incorrect length {0}, expected prefix \"#\" or \"0x\" and length 6")]
188 IncorrectLength(usize),
189 #[error("Failed to parse color becazse of unknown prefix \"{0}\", expected \"#\" or \"0x\"")]
190 UnknownPrefix(String),
191}
192
193#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq, Eq)]
195#[serde(try_from = "String")]
196#[serde(into = "String")]
197pub struct ThemeColor {
198 pub r: u8,
199 pub g: u8,
200 pub b: u8,
201}
202
203impl ThemeColor {
204 #[must_use]
206 pub const fn new(r: u8, g: u8, b: u8) -> Self {
207 Self { r, g, b }
208 }
209
210 pub fn from_hex(val: &str) -> Result<Self, ThemeColorParseError> {
212 let Some(without_prefix) = val.strip_prefix('#').or(val.strip_prefix("0x")) else {
213 return Err(ThemeColorParseError::UnknownPrefix(val.to_string()));
214 };
215
216 if without_prefix.len() != 6 {
218 return Err(ThemeColorParseError::IncorrectLength(without_prefix.len()));
219 }
220
221 let r = u8::from_str_radix(&without_prefix[0..=1], 16)
222 .map_err(ThemeColorParseError::ParseIntError)?;
223 let g = u8::from_str_radix(&without_prefix[2..=3], 16)
224 .map_err(ThemeColorParseError::ParseIntError)?;
225 let b = u8::from_str_radix(&without_prefix[4..=5], 16)
226 .map_err(ThemeColorParseError::ParseIntError)?;
227
228 Ok(Self { r, g, b })
229 }
230
231 #[inline]
233 #[must_use]
234 pub fn to_hex(&self) -> String {
235 format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b)
236 }
237}
238
239impl TryFrom<String> for ThemeColor {
240 type Error = ThemeColorParseError;
241
242 fn try_from(value: String) -> Result<Self, Self::Error> {
243 Self::from_hex(&value)
244 }
245}
246
247impl TryFrom<&str> for ThemeColor {
248 type Error = ThemeColorParseError;
249
250 fn try_from(value: &str) -> Result<Self, Self::Error> {
251 Self::from_hex(value)
252 }
253}
254
255impl From<AlacrittyColor> for ThemeColor {
256 fn from(value: AlacrittyColor) -> Self {
257 Self {
258 r: value.r,
259 g: value.g,
260 b: value.b,
261 }
262 }
263}
264
265impl From<ThemeColor> for String {
266 fn from(val: ThemeColor) -> Self {
267 ThemeColor::to_hex(&val)
268 }
269}
270
271impl From<ThemeColor> for Color {
272 fn from(val: ThemeColor) -> Self {
273 Color::Rgb(val.r, val.g, val.b)
274 }
275}
276
277#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
278#[serde(default)] pub struct ThemeColors {
280 #[serde(skip_serializing_if = "Option::is_none")]
285 pub file_name: Option<String>,
286 pub name: String,
287 pub author: String,
288 pub primary: ThemePrimary,
289 pub cursor: ThemeCursor,
290 pub normal: ThemeNormal,
291 pub bright: ThemeBright,
292}
293
294impl Default for ThemeColors {
295 fn default() -> Self {
296 Self {
297 file_name: None,
298 name: default_name(),
299 author: default_author(),
300 primary: ThemePrimary::default(),
301 cursor: ThemeCursor::default(),
302 normal: ThemeNormal::default(),
303 bright: ThemeBright::default(),
304 }
305 }
306}
307
308impl ThemeColors {
309 #[must_use]
313 pub fn full_default() -> Self {
314 Self {
315 name: "Termusic Default".to_string(),
316 author: "Termusic Developers".to_string(),
317 ..Default::default()
318 }
319 }
320}
321
322#[derive(Debug, Clone, PartialEq, thiserror::Error)]
324pub enum ThemeColorsParseError {
325 #[error("Failed to parse Theme: {0}")]
326 ThemeColor(#[from] ThemeColorParseError),
327}
328
329impl TryFrom<YAMLTheme> for ThemeColors {
330 type Error = ThemeColorsParseError;
331
332 fn try_from(value: YAMLTheme) -> Result<Self, Self::Error> {
333 let colors = value.colors;
334 Ok(Self {
335 file_name: None,
336 name: colors.name.unwrap_or_else(default_name),
337 author: colors.author.unwrap_or_else(default_author),
338 primary: colors.primary.try_into()?,
339 cursor: colors.cursor.try_into()?,
340 normal: colors.normal.try_into()?,
341 bright: colors.bright.try_into()?,
342 })
343 }
344}
345
346impl ThemeColors {
347 pub fn from_yaml_file(path: &Path) -> anyhow::Result<Self> {
349 let parsed: YAMLTheme = serde_yaml::from_reader(BufReader::new(File::open(path)?))?;
350
351 let mut theme = Self::try_from(parsed)?;
352
353 let file_name = path.file_stem();
354 theme.file_name = file_name.map(|v| v.to_string_lossy().to_string());
355
356 Ok(theme)
357 }
358}
359
360#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
361pub struct ThemePrimary {
362 pub background: ThemeColor,
363 pub foreground: ThemeColor,
364}
365
366impl Default for ThemePrimary {
367 fn default() -> Self {
368 Self {
369 background: ThemeColor::new(0x10, 0x14, 0x21),
370 foreground: ThemeColor::new(0xff, 0xfb, 0xf6),
371 }
372 }
373}
374
375impl TryFrom<YAMLThemePrimary> for ThemePrimary {
376 type Error = ThemeColorsParseError;
377
378 fn try_from(value: YAMLThemePrimary) -> Result<Self, Self::Error> {
379 Ok(Self {
380 background: value.background.try_into()?,
381 foreground: value.foreground.try_into()?,
382 })
383 }
384}
385
386#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
387#[serde(default)] pub struct ThemeCursor {
389 pub text: ThemeColor,
390 pub cursor: ThemeColor,
391}
392
393impl Default for ThemeCursor {
394 fn default() -> Self {
395 Self {
396 text: ThemeColor::new(0x1e, 0x1e, 0x1e),
397 cursor: default_fff(),
398 }
399 }
400}
401
402impl TryFrom<YAMLThemeCursor> for ThemeCursor {
403 type Error = ThemeColorsParseError;
404
405 fn try_from(value: YAMLThemeCursor) -> Result<Self, Self::Error> {
406 Ok(Self {
407 text: value.text.try_into()?,
408 cursor: value.cursor.try_into()?,
409 })
410 }
411}
412
413#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
414#[serde(default)] pub struct ThemeNormal {
416 pub black: ThemeColor,
417 pub red: ThemeColor,
418 pub green: ThemeColor,
419 pub yellow: ThemeColor,
420 pub blue: ThemeColor,
421 pub magenta: ThemeColor,
422 pub cyan: ThemeColor,
423 pub white: ThemeColor,
424}
425
426impl Default for ThemeNormal {
427 fn default() -> Self {
428 Self {
429 black: ThemeColor::new(0x2e, 0x2e, 0x2e),
430 red: ThemeColor::new(0xeb, 0x41, 0x29),
431 green: ThemeColor::new(0xab, 0xe0, 0x47),
432 yellow: ThemeColor::new(0xf6, 0xc7, 0x44),
433 blue: ThemeColor::new(0x47, 0xa0, 0xf3),
434 magenta: ThemeColor::new(0x7b, 0x5c, 0xb0),
435 cyan: ThemeColor::new(0x64, 0xdb, 0xed),
436 white: ThemeColor::new(0xe5, 0xe9, 0xf0),
437 }
438 }
439}
440
441impl TryFrom<YAMLThemeNormal> for ThemeNormal {
442 type Error = ThemeColorsParseError;
443
444 fn try_from(value: YAMLThemeNormal) -> Result<Self, Self::Error> {
445 Ok(Self {
446 black: value.black.try_into()?,
447 red: value.red.try_into()?,
448 green: value.green.try_into()?,
449 yellow: value.yellow.try_into()?,
450 blue: value.blue.try_into()?,
451 magenta: value.magenta.try_into()?,
452 cyan: value.cyan.try_into()?,
453 white: value.white.try_into()?,
454 })
455 }
456}
457
458#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
459#[serde(default)] pub struct ThemeBright {
461 pub black: ThemeColor,
462 pub red: ThemeColor,
463 pub green: ThemeColor,
464 pub yellow: ThemeColor,
465 pub blue: ThemeColor,
466 pub magenta: ThemeColor,
467 pub cyan: ThemeColor,
468 pub white: ThemeColor,
469}
470
471impl Default for ThemeBright {
472 fn default() -> Self {
473 Self {
474 black: ThemeColor::new(0x56, 0x56, 0x56),
475 red: ThemeColor::new(0xec, 0x53, 0x57),
476 green: ThemeColor::new(0xc0, 0xe1, 0x7d),
477 yellow: ThemeColor::new(0xf9, 0xda, 0x6a),
478 blue: ThemeColor::new(0x49, 0xa4, 0xf8),
479 magenta: ThemeColor::new(0xa4, 0x7d, 0xe9),
480 cyan: ThemeColor::new(0x99, 0xfa, 0xf2),
481 white: default_fff(),
482 }
483 }
484}
485
486impl TryFrom<YAMLThemeBright> for ThemeBright {
487 type Error = ThemeColorsParseError;
488
489 fn try_from(value: YAMLThemeBright) -> Result<Self, Self::Error> {
490 Ok(Self {
491 black: value.black.try_into()?,
492 red: value.red.try_into()?,
493 green: value.green.try_into()?,
494 yellow: value.yellow.try_into()?,
495 blue: value.blue.try_into()?,
496 magenta: value.magenta.try_into()?,
497 cyan: value.cyan.try_into()?,
498 white: value.white.try_into()?,
499 })
500 }
501}
502
503#[inline]
504fn default_name() -> String {
505 "empty name".to_string()
506}
507
508#[inline]
509fn default_author() -> String {
510 "empty author".to_string()
511}
512
513#[inline]
514fn default_fff() -> ThemeColor {
515 ThemeColor::new(0xFF, 0xFF, 0xFF)
516}
517
518mod v1_interop {
519 use super::{ThemeBright, ThemeColors, ThemeCursor, ThemeNormal, ThemePrimary, ThemeWrap};
520 use crate::config::v1;
521
522 impl From<&v1::Alacritty> for ThemeColors {
523 fn from(value: &v1::Alacritty) -> Self {
524 Self {
525 file_name: None,
526 name: value.name.clone(),
527 author: value.author.clone(),
528 primary: ThemePrimary {
529 background: value.background.into(),
530 foreground: value.foreground.into(),
531 },
532 cursor: ThemeCursor {
533 text: value.text.into(),
534 cursor: value.cursor.into(),
535 },
536 normal: ThemeNormal {
537 black: value.black.into(),
538 red: value.red.into(),
539 green: value.green.into(),
540 yellow: value.yellow.into(),
541 blue: value.blue.into(),
542 magenta: value.magenta.into(),
543 cyan: value.cyan.into(),
544 white: value.white.into(),
545 },
546 bright: ThemeBright {
547 black: value.light_black.into(),
548 red: value.light_red.into(),
549 green: value.light_green.into(),
550 yellow: value.light_yellow.into(),
551 blue: value.light_blue.into(),
552 magenta: value.light_magenta.into(),
553 cyan: value.light_cyan.into(),
554 white: value.light_white.into(),
555 },
556 }
557 }
558 }
559
560 impl From<&v1::Settings> for ThemeWrap {
561 fn from(value: &v1::Settings) -> Self {
562 Self {
563 theme: (&value.style_color_symbol.alacritty_theme).into(),
564 style: value.into(),
565 }
566 }
567 }
568
569 #[cfg(test)]
570 mod tests {
571 use super::*;
572
573 #[test]
574 fn should_convert_default_without_error() {
575 let converted: ThemeColors = (&v1::StyleColorSymbol::default().alacritty_theme).into();
576
577 assert_eq!(
578 converted,
579 ThemeColors {
580 file_name: None,
581 name: "default".into(),
582 author: "Larry Hao".into(),
583 primary: ThemePrimary {
584 background: "#101421".try_into().unwrap(),
585 foreground: "#fffbf6".try_into().unwrap()
586 },
587 cursor: ThemeCursor {
588 text: "#1E1E1E".try_into().unwrap(),
589 cursor: "#FFFFFF".try_into().unwrap()
590 },
591 normal: ThemeNormal {
592 black: "#2e2e2e".try_into().unwrap(),
593 red: "#eb4129".try_into().unwrap(),
594 green: "#abe047".try_into().unwrap(),
595 yellow: "#f6c744".try_into().unwrap(),
596 blue: "#47a0f3".try_into().unwrap(),
597 magenta: "#7b5cb0".try_into().unwrap(),
598 cyan: "#64dbed".try_into().unwrap(),
599 white: "#e5e9f0".try_into().unwrap()
600 },
601 bright: ThemeBright {
602 black: "#565656".try_into().unwrap(),
603 red: "#ec5357".try_into().unwrap(),
604 green: "#c0e17d".try_into().unwrap(),
605 yellow: "#f9da6a".try_into().unwrap(),
606 blue: "#49a4f8".try_into().unwrap(),
607 magenta: "#a47de9".try_into().unwrap(),
608 cyan: "#99faf2".try_into().unwrap(),
609 white: "#ffffff".try_into().unwrap()
610 }
611 }
612 );
613 }
614 }
615}
616
617#[cfg(test)]
618mod tests {
619 use super::ThemeColors;
620
621 mod theme_color {
622 use super::super::ThemeColor;
623
624 #[test]
625 fn should_parse_hashtag() {
626 assert_eq!(
627 ThemeColor::new(1, 2, 3),
628 ThemeColor::from_hex("#010203").unwrap()
629 );
630 assert_eq!(
631 ThemeColor::new(255, 255, 255),
632 ThemeColor::from_hex("#ffffff").unwrap()
633 );
634 assert_eq!(
635 ThemeColor::new(0, 0, 0),
636 ThemeColor::from_hex("#000000").unwrap()
637 );
638 }
639
640 #[test]
641 fn should_parse_0x() {
642 assert_eq!(
643 ThemeColor::new(1, 2, 3),
644 ThemeColor::from_hex("0x010203").unwrap()
645 );
646 assert_eq!(
647 ThemeColor::new(255, 255, 255),
648 ThemeColor::from_hex("0xffffff").unwrap()
649 );
650 assert_eq!(
651 ThemeColor::new(0, 0, 0),
652 ThemeColor::from_hex("0x000000").unwrap()
653 );
654 }
655 }
656
657 #[test]
658 fn should_default() {
659 let _ = ThemeColors::default();
661 }
662}