Skip to main content

i3status_rs/
icons.rs

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
29/// Every icon key the bar knows. Blocks name icons through these consts,
30/// and `ALL` lets the tests check the default set covers every key.
31macro_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        // "none" icon set
123        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}