bar_config/
config.rs

1//! Bar configuration state.
2//!
3//! This module contains everything required to express the any state of the bar. The root
4//! element ([`Config`]) can be accessed through the [`Bar`] using the [`load`] and [`lock`] methods.
5//!
6//! # Examples
7//!
8//! ```
9//! use bar_config::Bar;
10//! use std::io::Cursor;
11//!
12//! let config_file = Cursor::new(String::from(
13//!     "height: 30\n\
14//!      monitors:\n\
15//!       - { name: \"DVI-1\" }"
16//! ));
17//!
18//! let bar = Bar::load(config_file).unwrap();
19//! let config = bar.lock();
20//!
21//! assert_eq!(config.height, 30);
22//! assert_eq!(config.monitors.len(), 1);
23//! assert_eq!(config.monitors[0].name, "DVI-1");
24//! ```
25//!
26//! [`Config`]: struct.Config.html
27//! [`Bar`]: ../struct.Bar.html
28//! [`load`]: ../struct.Bar.html#method.load
29//! [`lock`]: ../struct.Bar.html#method.lock
30
31use serde::de::{Deserializer, Error};
32use serde::Deserialize;
33
34use std::path::{Path, PathBuf};
35
36use crate::components::Component;
37
38/// Root element of the bar configuration.
39///
40/// This element contains the complete state of the bar necessary to render it.
41#[derive(Debug, Deserialize)]
42pub struct Config {
43    pub height: u8,
44    pub position: Option<Position>,
45    pub background: Option<Background>,
46    #[serde(
47        deserialize_with = "deserialize_monitors",
48        skip_serializing_if = "Vec::is_empty"
49    )]
50    pub monitors: Vec<Monitor>,
51    pub defaults: Option<ComponentSettings>,
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub left: Vec<Box<Component>>,
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    pub center: Vec<Box<Component>>,
56    #[serde(default, skip_serializing_if = "Vec::is_empty")]
57    pub right: Vec<Box<Component>>,
58}
59
60// Require at least one monitor
61fn deserialize_monitors<'a, D>(deserializer: D) -> Result<Vec<Monitor>, D::Error>
62where
63    D: Deserializer<'a>,
64{
65    match Vec::<Monitor>::deserialize(deserializer) {
66        Ok(monitors) => {
67            if monitors.is_empty() {
68                Err(D::Error::custom(String::from(
69                    "at least one monitor is required",
70                )))
71            } else {
72                Ok(monitors)
73            }
74        }
75        err => err,
76    }
77}
78
79/// Default options available for every component.
80#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
81pub struct ComponentSettings {
82    pub foreground: Option<Color>,
83    pub background: Option<Background>,
84    pub width: Option<u8>,
85    pub padding: Option<u8>,
86    pub offset_x: Option<i8>,
87    pub offset_y: Option<i8>,
88    #[serde(default, skip_serializing_if = "Vec::is_empty")]
89    pub fonts: Vec<Font>,
90    pub border: Option<Border>,
91}
92
93/// Background of a component or the bar.
94#[derive(Clone, Debug, Eq, PartialEq, Hash)]
95pub enum Background {
96    Image(PathBuf),
97    Color(Color),
98}
99
100impl<'de> Deserialize<'de> for Background {
101    fn deserialize<D>(deserializer: D) -> Result<Background, D::Error>
102    where
103        D: Deserializer<'de>,
104    {
105        match String::deserialize(deserializer) {
106            Ok(text) => {
107                if text.starts_with('#') {
108                    Color::from_str(&text)
109                        .map_err(D::Error::custom)
110                        .map(Background::Color)
111                } else {
112                    Path::new(&text)
113                        .canonicalize()
114                        .map_err(D::Error::custom)
115                        .map(Background::Image)
116                }
117            }
118            Err(err) => Err(err),
119        }
120    }
121}
122
123/// Distinct identification for a font.
124#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
125pub struct Font {
126    pub description: String,
127    pub size: u8,
128}
129
130/// Distinct identification for a monitor.
131///
132/// The [`fallback_names`] can be used to specify alternative screens which should be used when the
133/// primary monitor is not available.
134///
135/// [`fallback_names`]: #structfield.fallback_names
136#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
137pub struct Monitor {
138    pub name: String,
139    #[serde(default)]
140    pub fallback_names: Vec<String>,
141}
142
143/// Border separating the bar from the rest of the WM.
144#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
145pub struct Border {
146    pub height: u8,
147    pub color: Color,
148}
149
150/// Available positions for the bar.
151#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
152pub enum Position {
153    Top,
154    Bottom,
155}
156
157/// RGBA color specified as four values from 0 to 255.
158#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
159pub struct Color {
160    r: u8,
161    g: u8,
162    b: u8,
163    a: u8,
164}
165
166impl Color {
167    fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
168        Color { r, g, b, a }
169    }
170
171    // Deserialize the `#ff00ff` and `#ff00ff00` color formats
172    fn from_str(string: &str) -> Result<Self, String> {
173        if !string.starts_with('#') || (string.len() != 7 && string.len() != 9) {
174            return Err(String::from(
175                "colors need to follow the format `#RRGGBB` or `#RRGGBBAA`",
176            ));
177        }
178
179        let radix_error =
180            |_| String::from("hexadecimal color digits need to be within the range 0..=F");
181        let r = u8::from_str_radix(&string[1..3], 16).map_err(radix_error)?;
182        let g = u8::from_str_radix(&string[3..5], 16).map_err(radix_error)?;
183        let b = u8::from_str_radix(&string[5..7], 16).map_err(radix_error)?;
184        let a = if string.len() == 9 {
185            u8::from_str_radix(&string[7..9], 16).map_err(radix_error)?
186        } else {
187            255
188        };
189
190        Ok(Color::new(r, g, b, a))
191    }
192}
193
194// Format the color in the format `#RRGGBBAA`
195impl ToString for Color {
196    fn to_string(&self) -> String {
197        format!("#{:02x}{:02x}{:02x}{:02x}", self.r, self.g, self.b, self.a)
198    }
199}
200
201impl<'de> Deserialize<'de> for Color {
202    fn deserialize<D>(deserializer: D) -> Result<Color, D::Error>
203    where
204        D: Deserializer<'de>,
205    {
206        match String::deserialize(deserializer) {
207            Ok(color_string) => Color::from_str(&color_string).map_err(D::Error::custom),
208            Err(err) => Err(err),
209        }
210    }
211}