1pub mod color;
2pub mod separator;
3pub mod xresources;
4
5use std::fmt;
6use std::ops::{Deref, DerefMut};
7
8use serde::{Deserialize, de};
9
10use crate::errors::*;
11use crate::util;
12use crate::widget::State;
13use color::Color;
14use separator::Separator;
15
16#[derive(Debug, Clone)]
17pub struct Theme(pub ThemeInner);
18
19impl Default for Theme {
20 fn default() -> Self {
21 ThemeUserConfig::default()
22 .try_into()
23 .unwrap_or_else(|_| Self(ThemeInner::default()))
24 }
25}
26
27impl Deref for Theme {
28 type Target = ThemeInner;
29 fn deref(&self) -> &Self::Target {
30 &self.0
31 }
32}
33impl DerefMut for Theme {
34 fn deref_mut(&mut self) -> &mut Self::Target {
35 &mut self.0
36 }
37}
38
39#[derive(Deserialize, Debug, Clone, Default)]
40#[serde(deny_unknown_fields, default)]
41pub struct ThemeInner {
42 pub idle_bg: Color,
43 pub idle_fg: Color,
44 pub info_bg: Color,
45 pub info_fg: Color,
46 pub good_bg: Color,
47 pub good_fg: Color,
48 pub warning_bg: Color,
49 pub warning_fg: Color,
50 pub critical_bg: Color,
51 pub critical_fg: Color,
52 pub separator: Separator,
53 pub separator_bg: Color,
54 pub separator_fg: Color,
55 pub alternating_tint_bg: Color,
56 pub alternating_tint_fg: Color,
57 pub end_separator: Separator,
58 pub start_separator: Separator,
59}
60
61impl Theme {
62 pub fn get_colors(&self, state: State) -> (Color, Color) {
63 match state {
64 State::Idle => (self.idle_bg, self.idle_fg),
65 State::Info => (self.info_bg, self.info_fg),
66 State::Good => (self.good_bg, self.good_fg),
67 State::Warning => (self.warning_bg, self.warning_fg),
68 State::Critical => (self.critical_bg, self.critical_fg),
69 }
70 }
71
72 pub fn apply_overrides(&mut self, overrides: ThemeOverrides) -> Result<()> {
73 let copy = self.clone();
74
75 if let Some(separator) = overrides.separator {
76 self.separator = separator;
77 }
78 if let Some(end_separator) = overrides.end_separator {
79 self.end_separator = end_separator;
80 }
81 if let Some(start_separator) = overrides.start_separator {
82 self.start_separator = start_separator;
83 }
84
85 macro_rules! apply {
86 ($prop:tt) => {
87 if let Some(color) = overrides.$prop {
88 self.$prop = color.eval(©)?;
89 }
90 };
91 }
92 apply!(idle_bg);
93 apply!(idle_fg);
94 apply!(info_bg);
95 apply!(info_fg);
96 apply!(good_bg);
97 apply!(good_fg);
98 apply!(warning_bg);
99 apply!(warning_fg);
100 apply!(critical_bg);
101 apply!(critical_fg);
102 apply!(separator_bg);
103 apply!(separator_fg);
104 apply!(alternating_tint_bg);
105 apply!(alternating_tint_fg);
106
107 Ok(())
108 }
109}
110
111#[derive(Deserialize, Default)]
112#[serde(deny_unknown_fields, default)]
113pub struct ThemeUserConfig {
114 pub theme: Option<String>,
115 pub overrides: Option<ThemeOverrides>,
116}
117
118#[derive(Deserialize, Debug, Clone, Default)]
119pub struct ThemeOverrides {
120 pub idle_bg: Option<ColorOrLink>,
121 pub idle_fg: Option<ColorOrLink>,
122 pub info_bg: Option<ColorOrLink>,
123 pub info_fg: Option<ColorOrLink>,
124 pub good_bg: Option<ColorOrLink>,
125 pub good_fg: Option<ColorOrLink>,
126 pub warning_bg: Option<ColorOrLink>,
127 pub warning_fg: Option<ColorOrLink>,
128 pub critical_bg: Option<ColorOrLink>,
129 pub critical_fg: Option<ColorOrLink>,
130 pub separator: Option<Separator>,
131 pub separator_bg: Option<ColorOrLink>,
132 pub separator_fg: Option<ColorOrLink>,
133 pub alternating_tint_bg: Option<ColorOrLink>,
134 pub alternating_tint_fg: Option<ColorOrLink>,
135 pub end_separator: Option<Separator>,
136 pub start_separator: Option<Separator>,
137}
138
139impl TryFrom<ThemeUserConfig> for Theme {
140 type Error = Error;
141
142 fn try_from(user_config: ThemeUserConfig) -> Result<Self, Self::Error> {
143 let name = user_config.theme.as_deref().unwrap_or("plain");
144 let file = util::find_file(name, Some("themes"), Some("toml"))
145 .or_error(|| format!("Theme '{name}' not found"))?;
146 let theme: ThemeInner = util::deserialize_toml_file(file)?;
147 let mut theme = Theme(theme);
148 if let Some(overrides) = user_config.overrides {
149 theme.apply_overrides(overrides)?;
150 }
151 Ok(theme)
152 }
153}
154
155#[derive(Debug, Clone)]
156pub enum ColorOrLink {
157 Color(Color),
158 Link { link: String },
159}
160
161impl ColorOrLink {
162 fn eval(self, theme: &Theme) -> Result<Color> {
163 Ok(match self {
164 Self::Color(c) => c,
165 Self::Link { link } => match link.as_str() {
166 "idle_bg" => theme.idle_bg,
167 "idle_fg" => theme.idle_fg,
168 "info_bg" => theme.info_bg,
169 "info_fg" => theme.info_fg,
170 "good_bg" => theme.good_bg,
171 "good_fg" => theme.good_fg,
172 "warning_bg" => theme.warning_bg,
173 "warning_fg" => theme.warning_fg,
174 "critical_bg" => theme.critical_bg,
175 "critical_fg" => theme.critical_fg,
176 "separator_bg" => theme.separator_bg,
177 "separator_fg" => theme.separator_fg,
178 "alternating_tint_bg" => theme.alternating_tint_bg,
179 "alternating_tint_fg" => theme.alternating_tint_fg,
180 _ => return Err(Error::new(format!("{link} is not a correct theme color"))),
181 },
182 })
183 }
184}
185
186impl<'de> Deserialize<'de> for ColorOrLink {
187 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
188 where
189 D: de::Deserializer<'de>,
190 {
191 struct Visitor;
192 impl<'de> de::Visitor<'de> for Visitor {
193 type Value = ColorOrLink;
194
195 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
196 formatter.write_str("color or link")
197 }
198
199 fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
200 where
201 A: de::MapAccess<'de>,
202 {
203 #[derive(Deserialize)]
204 #[serde(deny_unknown_fields)]
205 struct Link {
206 link: String,
207 }
208 Link::deserialize(de::value::MapAccessDeserializer::new(map))
209 .map(|link| ColorOrLink::Link { link: link.link })
210 }
211
212 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
213 where
214 E: de::Error,
215 {
216 v.parse::<Color>().serde_error().map(ColorOrLink::Color)
217 }
218 }
219 deserializer.deserialize_any(Visitor)
220 }
221}