1use crate::errors::*;
2use crate::util;
3use serde::Deserialize;
4use std::collections::HashMap;
5
6#[derive(Deserialize, Debug, Clone)]
7#[serde(try_from = "IconsConfigRaw")]
8pub struct Icons(pub HashMap<String, Icon>);
9
10#[derive(Deserialize, Debug, Clone)]
11#[serde(untagged)]
12pub enum Icon {
13 Single(String),
14 Progression(Vec<String>),
15}
16
17impl From<&'static str> for Icon {
18 fn from(value: &'static str) -> Self {
19 Self::Single(value.into())
20 }
21}
22
23impl<const N: usize> From<[&str; N]> for Icon {
24 fn from(value: [&str; N]) -> Self {
25 Self::Progression(value.iter().map(|s| s.to_string()).collect())
26 }
27}
28
29macro_rules! icon_keys {
32 ($($name:ident = $key:literal,)*) => {
33 $(pub(crate) const $name: &str = $key;)*
34
35 #[cfg(test)]
36 const ALL: &[&str] = &[$($name),*];
37 };
38}
39
40icon_keys! {
41 BACKLIGHT = "backlight",
42 BAT = "bat",
43 BAT_CHARGING = "bat_charging",
44 BAT_NOT_AVAILABLE = "bat_not_available",
45 BELL = "bell",
46 BELL_SLASH = "bell-slash",
47 BLUETOOTH = "bluetooth",
48 CALENDAR = "calendar",
49 COGS = "cogs",
50 CPU = "cpu",
51 CPU_BOOST_OFF = "cpu_boost_off",
52 CPU_BOOST_ON = "cpu_boost_on",
53 DISK_DRIVE = "disk_drive",
54 DOCKER = "docker",
55 GITHUB = "github",
56 GPU = "gpu",
57 HEADPHONES = "headphones",
58 HUESHIFT = "hueshift",
59 JOYSTICK = "joystick",
60 KEYBOARD = "keyboard",
61 MAIL = "mail",
62 MEMORY_MEM = "memory_mem",
63 MEMORY_SWAP = "memory_swap",
64 MICROPHONE = "microphone",
65 MICROPHONE_MUTED = "microphone_muted",
66 MOUSE = "mouse",
67 MUSIC = "music",
68 MUSIC_NEXT = "music_next",
69 MUSIC_PAUSE = "music_pause",
70 MUSIC_PLAY = "music_play",
71 MUSIC_PREV = "music_prev",
72 NET_BRIDGE = "net_bridge",
73 NET_CELLULAR = "net_cellular",
74 NET_DOWN = "net_down",
75 NET_LOOPBACK = "net_loopback",
76 NET_MODEM = "net_modem",
77 NET_UP = "net_up",
78 NET_VPN = "net_vpn",
79 NET_WIRED = "net_wired",
80 NET_WIRELESS = "net_wireless",
81 NOTIFICATION = "notification",
82 PHONE = "phone",
83 PHONE_DISCONNECTED = "phone_disconnected",
84 PING = "ping",
85 POMODORO = "pomodoro",
86 POMODORO_BREAK = "pomodoro_break",
87 POMODORO_PAUSED = "pomodoro_paused",
88 POMODORO_STARTED = "pomodoro_started",
89 POMODORO_STOPPED = "pomodoro_stopped",
90 REFRESH = "refresh",
91 RESOLUTION = "resolution",
92 SCRATCHPAD = "scratchpad",
93 TASKS = "tasks",
94 TEA = "tea",
95 THERMOMETER = "thermometer",
96 TIME = "time",
97 TOGGLE_OFF = "toggle_off",
98 TOGGLE_ON = "toggle_on",
99 UNKNOWN = "unknown",
100 UPDATE = "update",
101 UPTIME = "uptime",
102 VOLUME = "volume",
103 VOLUME_MUTED = "volume_muted",
104 WEATHER_CLOUDS = "weather_clouds",
105 WEATHER_CLOUDS_NIGHT = "weather_clouds_night",
106 WEATHER_DEFAULT = "weather_default",
107 WEATHER_FOG = "weather_fog",
108 WEATHER_FOG_NIGHT = "weather_fog_night",
109 WEATHER_MOON = "weather_moon",
110 WEATHER_RAIN = "weather_rain",
111 WEATHER_RAIN_NIGHT = "weather_rain_night",
112 WEATHER_SNOW = "weather_snow",
113 WEATHER_SUN = "weather_sun",
114 WEATHER_THUNDER = "weather_thunder",
115 WEATHER_THUNDER_NIGHT = "weather_thunder_night",
116 WEBCAM = "webcam",
117 XRANDR = "xrandr",
118}
119
120impl Default for Icons {
121 fn default() -> Self {
122 Self(map! {
124 BACKLIGHT => "BRIGHT",
125 BAT => "BAT",
126 BAT_CHARGING => "CHG",
127 BAT_NOT_AVAILABLE => "BAT N/A",
128 BELL => "ON",
129 BELL_SLASH => "OFF",
130 BLUETOOTH => "BT",
131 CALENDAR => "CAL",
132 COGS => "LOAD",
133 CPU => "CPU",
134 CPU_BOOST_ON => "BOOST ON",
135 CPU_BOOST_OFF => "BOOST OFF",
136 DISK_DRIVE => "DISK",
137 DOCKER => "DOCKER",
138 GITHUB => "GITHUB",
139 GPU => "GPU",
140 HEADPHONES => "HEAD",
141 HUESHIFT => "HUE",
142 JOYSTICK => "JOY",
143 KEYBOARD => "KBD",
144 MAIL => "MAIL",
145 MEMORY_MEM => "MEM",
146 MEMORY_SWAP => "SWAP",
147 MOUSE => "MOUSE",
148 MUSIC => "MUSIC",
149 MUSIC_NEXT => ">",
150 MUSIC_PAUSE => "||",
151 MUSIC_PLAY => ">",
152 MUSIC_PREV => "<",
153 NET_BRIDGE => "BRIDGE",
154 NET_CELLULAR => [
155 "NO SIGNAL",
156 "0 BARS",
157 "1 BAR",
158 "2 BARS",
159 "3 BARS",
160 "4 BARS",
161 ],
162 NET_DOWN => "DOWN",
163 NET_LOOPBACK => "LO",
164 NET_MODEM => "MODEM",
165 NET_UP => "UP ",
166 NET_VPN => "VPN",
167 NET_WIRED => "ETH",
168 NET_WIRELESS => "WLAN",
169 NOTIFICATION => "NOTIF",
170 PHONE => "PHONE",
171 PHONE_DISCONNECTED => "PHONE",
172 PING => "PING",
173 POMODORO => "POMODORO",
174 POMODORO_BREAK => "BREAK",
175 POMODORO_PAUSED => "PAUSED",
176 POMODORO_STARTED => "STARTED",
177 POMODORO_STOPPED => "STOPPED",
178 REFRESH => "REFRESH",
179 RESOLUTION => "RES",
180 SCRATCHPAD => "[]",
181 TASKS => "TSK",
182 TEA => "TEA",
183 THERMOMETER => "TEMP",
184 TIME => "TIME",
185 TOGGLE_OFF => "OFF",
186 TOGGLE_ON => "ON",
187 UNKNOWN => "??",
188 UPDATE => "UPD",
189 UPTIME => "UP",
190 VOLUME => "VOL",
191 VOLUME_MUTED => "VOL MUTED",
192 MICROPHONE => "MIC",
193 MICROPHONE_MUTED => "MIC MUTED",
194 WEATHER_CLOUDS_NIGHT => "CLOUDY",
195 WEATHER_CLOUDS => "CLOUDY",
196 WEATHER_DEFAULT => "WEATHER",
197 WEATHER_FOG_NIGHT => "FOG",
198 WEATHER_FOG => "FOG",
199 WEATHER_MOON => "MOONY",
200 WEATHER_RAIN_NIGHT => "RAIN",
201 WEATHER_RAIN => "RAIN",
202 WEATHER_SNOW => "SNOW",
203 WEATHER_SUN => "SUNNY",
204 WEATHER_THUNDER_NIGHT => "STORM",
205 WEATHER_THUNDER => "STORM",
206 WEBCAM => "CAM",
207 XRANDR => "SCREEN"
208 })
209 }
210}
211
212impl Icons {
213 pub fn from_file(file: &str) -> Result<Self> {
214 if file == "none" {
215 Ok(Icons::default())
216 } else {
217 let file = util::find_file(file, Some("icons"), Some("toml"))?
218 .or_error(|| format!("Icon set '{file}' not found"))?;
219 Ok(Icons(util::deserialize_toml_file(file)?))
220 }
221 }
222
223 pub fn apply_overrides(&mut self, overrides: HashMap<String, Icon>) {
224 self.0.extend(overrides);
225 }
226
227 pub fn get(&self, icon: &'_ str, value: Option<f64>) -> Option<&str> {
228 match (self.0.get(icon)?, value) {
229 (Icon::Single(icon), _) => Some(icon),
230 (Icon::Progression(prog), _) if prog.is_empty() => None,
231 (Icon::Progression(prog), None) => Some(prog.last().unwrap()),
232 (Icon::Progression(prog), Some(value)) => {
233 let index = ((value * prog.len() as f64) as usize).clamp(0, prog.len() - 1);
234 Some(prog[index].as_str())
235 }
236 }
237 }
238}
239
240#[derive(Deserialize, Default)]
241#[serde(deny_unknown_fields, default)]
242struct IconsConfigRaw {
243 icons: Option<String>,
244 overrides: Option<HashMap<String, Icon>>,
245}
246
247impl TryFrom<IconsConfigRaw> for Icons {
248 type Error = Error;
249
250 fn try_from(raw: IconsConfigRaw) -> Result<Self, Self::Error> {
251 let mut icons = Self::from_file(raw.icons.as_deref().unwrap_or("none"))?;
252 if let Some(overrides) = raw.overrides {
253 for icon in overrides {
254 icons.0.insert(icon.0, icon.1);
255 }
256 }
257 Ok(icons)
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn default_icons_cover_every_key() {
267 let icons = Icons::default();
268 let mut defaults: Vec<&str> = icons.0.keys().map(String::as_str).collect();
269 let mut keys = ALL.to_vec();
270 defaults.sort_unstable();
271 keys.sort_unstable();
272 assert_eq!(defaults, keys);
273 }
274}