1use serde::de::{Deserializer, Error};
32use serde::Deserialize;
33
34use std::path::{Path, PathBuf};
35
36use crate::components::Component;
37
38#[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
60fn 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#[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#[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#[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
125pub struct Font {
126 pub description: String,
127 pub size: u8,
128}
129
130#[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#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
145pub struct Border {
146 pub height: u8,
147 pub color: Color,
148}
149
150#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Deserialize)]
152pub enum Position {
153 Top,
154 Bottom,
155}
156
157#[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 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
194impl 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}