Skip to main content

i3status_rs/
themes.rs

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