i3status_rs/
util.rs

1use std::path::{Path, PathBuf};
2
3use dirs::{config_dir, data_dir};
4use serde::de::DeserializeOwned;
5use tokio::io::AsyncReadExt as _;
6use tokio::process::Command;
7
8use crate::errors::*;
9
10/// Tries to find a file in standard locations:
11/// - Fist try to find a file by full path (only if path is absolute)
12/// - Then try XDG_CONFIG_HOME (e.g. `~/.config`)
13/// - Then try XDG_DATA_HOME (e.g. `~/.local/share/`)
14/// - Then try `/usr/share/`
15///
16/// Automatically append an extension if not presented.
17pub fn find_file(file: &str, subdir: Option<&str>, extension: Option<&str>) -> Option<PathBuf> {
18    let file = Path::new(file);
19
20    if file.is_absolute() && file.exists() {
21        return Some(file.to_path_buf());
22    }
23
24    // Try XDG_CONFIG_HOME (e.g. `~/.config`)
25    if let Some(mut xdg_config) = config_dir() {
26        xdg_config.push("i3status-rust");
27        if let Some(subdir) = subdir {
28            xdg_config.push(subdir);
29        }
30        xdg_config.push(file);
31        if let Some(file) = exists_with_opt_extension(&xdg_config, extension) {
32            return Some(file);
33        }
34    }
35
36    // Try XDG_DATA_HOME (e.g. `~/.local/share/`)
37    if let Some(mut xdg_data) = data_dir() {
38        xdg_data.push("i3status-rust");
39        if let Some(subdir) = subdir {
40            xdg_data.push(subdir);
41        }
42        xdg_data.push(file);
43        if let Some(file) = exists_with_opt_extension(&xdg_data, extension) {
44            return Some(file);
45        }
46    }
47
48    // Try `/usr/share/`
49    let mut usr_share_path = PathBuf::from("/usr/share/i3status-rust");
50    if let Some(subdir) = subdir {
51        usr_share_path.push(subdir);
52    }
53    usr_share_path.push(file);
54    if let Some(file) = exists_with_opt_extension(&usr_share_path, extension) {
55        return Some(file);
56    }
57
58    None
59}
60
61fn exists_with_opt_extension(file: &Path, extension: Option<&str>) -> Option<PathBuf> {
62    if file.exists() {
63        return Some(file.into());
64    }
65    // If file has no extension, test with given extension
66    if let (None, Some(extension)) = (file.extension(), extension) {
67        let file = file.with_extension(extension);
68        // Check again with extension added
69        if file.exists() {
70            return Some(file);
71        }
72    }
73    None
74}
75
76pub async fn new_dbus_connection() -> Result<zbus::Connection> {
77    zbus::Connection::session()
78        .await
79        .error("Failed to open DBus session connection")
80}
81
82pub async fn new_system_dbus_connection() -> Result<zbus::Connection> {
83    zbus::Connection::system()
84        .await
85        .error("Failed to open DBus system connection")
86}
87
88pub fn deserialize_toml_file<T, P>(path: P) -> Result<T>
89where
90    T: DeserializeOwned,
91    P: AsRef<Path>,
92{
93    let path = path.as_ref();
94
95    let contents = std::fs::read_to_string(path)
96        .or_error(|| format!("Failed to read file: {}", path.display()))?;
97
98    toml::from_str(&contents).map_err(|err| {
99        let location_msg = err
100            .span()
101            .map(|span| {
102                let line = 1 + contents.as_bytes()[..(span.start)]
103                    .iter()
104                    .filter(|b| **b == b'\n')
105                    .count();
106                format!(" at line {line}")
107            })
108            .unwrap_or_default();
109        Error::new(format!(
110            "Failed to deserialize TOML file {}{}: {}",
111            path.display(),
112            location_msg,
113            err.message()
114        ))
115    })
116}
117
118pub async fn read_file(path: impl AsRef<Path>) -> std::io::Result<String> {
119    let mut file = tokio::fs::File::open(path).await?;
120    let mut content = String::new();
121    file.read_to_string(&mut content).await?;
122    Ok(content.trim_end().to_string())
123}
124
125pub async fn has_command(command: &str) -> Result<bool> {
126    Command::new("sh")
127        .args([
128            "-c",
129            format!("command -v {command} >/dev/null 2>&1").as_ref(),
130        ])
131        .status()
132        .await
133        .or_error(|| format!("Failed to check {command} presence"))
134        .map(|status| status.success())
135}
136
137/// # Example
138///
139/// ```ignore
140/// let opt = Some(1);
141/// let m: HashMap<&'static str, String> = map! {
142///     "key" => "value",
143///     [if true] "hello" => "world",
144///     [if let Some(x) = opt] "opt" => x.to_string(),
145/// };
146/// map! { @extend m
147///     "new key" => "new value",
148///     "one" => "more",
149/// }
150/// ```
151#[macro_export]
152macro_rules! map {
153    (@extend $map:ident $( $([$($cond_tokens:tt)*])? $key:literal => $value:expr ),* $(,)?) => {{
154        $(
155        map!(@insert $map, $key, $value $(,$($cond_tokens)*)?);
156        )*
157    }};
158    (@extend $map:ident $( $key:expr => $value:expr ),* $(,)?) => {{
159        $(
160        map!(@insert $map, $key, $value);
161        )*
162    }};
163    (@insert $map:ident, $key:expr, $value:expr) => {{
164        $map.insert($key.into(), $value.into());
165    }};
166    (@insert $map:ident, $key:expr, $value:expr, if $cond:expr) => {{
167        if $cond {
168        $map.insert($key.into(), $value.into());
169        }
170    }};
171    (@insert $map:ident, $key:expr, $value:expr, if let $pat:pat = $match_on:expr) => {{
172        if let $pat = $match_on {
173        $map.insert($key.into(), $value.into());
174        }
175    }};
176    ($($tt:tt)*) => {{
177        #[allow(unused_mut)]
178        let mut m = ::std::collections::HashMap::new();
179        map!(@extend m $($tt)*);
180        m
181    }};
182}
183
184pub use map;
185
186macro_rules! regex {
187    ($re:literal $(,)?) => {{
188        static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
189        RE.get_or_init(|| regex::Regex::new($re).unwrap())
190    }};
191}
192
193macro_rules! make_log_macro {
194    (@wdoll $macro_name:ident, $block_name:literal, ($dol:tt)) => {
195        #[allow(dead_code)]
196        macro_rules! $macro_name {
197            ($dol($args:tt)+) => {
198                ::log::$macro_name!(target: $block_name, $dol($args)+);
199            };
200        }
201    };
202    ($macro_name:ident, $block_name:literal) => {
203        make_log_macro!(@wdoll $macro_name, $block_name, ($));
204    };
205}
206
207pub fn format_bar_graph(content: &[f64]) -> String {
208    // (x * one eighth block) https://en.wikipedia.org/wiki/Block_Elements
209    static BARS: [char; 8] = [
210        '\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}',
211        '\u{2588}',
212    ];
213
214    // Find min and max
215    let mut min = f64::INFINITY;
216    let mut max = f64::NEG_INFINITY;
217    for &v in content {
218        min = min.min(v);
219        max = max.max(v);
220    }
221
222    let range = max - min;
223    content
224        .iter()
225        .map(|x| BARS[((x - min) / range * 7.).clamp(0., 7.) as usize])
226        .collect()
227}
228
229/// Convert 2 letter country code to Unicode
230pub fn country_flag_from_iso_code(country_code: &str) -> String {
231    let [mut b1, mut b2]: [u8; 2] = country_code.as_bytes().try_into().unwrap_or([0, 0]);
232
233    if !b1.is_ascii_uppercase() || !b2.is_ascii_uppercase() {
234        return country_code.into();
235    }
236
237    // Each char is encoded as 1F1E6 to 1F1FF for A-Z
238    b1 += 0xa5;
239    b2 += 0xa5;
240    // The last byte will always start with 101 (0xa0) and then the 5 least
241    // significant bits from the previous result
242    b1 = 0xa0 | (b1 & 0x1f);
243    b2 = 0xa0 | (b2 & 0x1f);
244    // Get the flag string from the UTF-8 representation of our Unicode characters.
245    String::from_utf8(vec![0xf0, 0x9f, 0x87, b1, 0xf0, 0x9f, 0x87, b2]).unwrap()
246}
247
248/// A shortcut for `Default::default()`
249/// See <https://github.com/rust-lang/rust/issues/73014>
250#[inline]
251pub fn default<T: Default>() -> T {
252    Default::default()
253}
254
255pub trait StreamExtDebounced: futures::StreamExt {
256    fn next_debounced(&mut self) -> impl Future<Output = Option<Self::Item>>;
257}
258
259impl<T: futures::StreamExt + Unpin> StreamExtDebounced for T {
260    async fn next_debounced(&mut self) -> Option<Self::Item> {
261        let mut result = self.next().await?;
262        let mut noop_ctx = std::task::Context::from_waker(std::task::Waker::noop());
263        loop {
264            match self.poll_next_unpin(&mut noop_ctx) {
265                std::task::Poll::Ready(Some(x)) => result = x,
266                std::task::Poll::Ready(None) | std::task::Poll::Pending => return Some(result),
267            }
268        }
269    }
270}
271
272#[cfg(test)]
273mod tests {
274    use super::*;
275
276    #[tokio::test]
277    async fn test_has_command_ok() {
278        // we assume sh is always available
279        assert!(has_command("sh").await.unwrap());
280    }
281
282    #[tokio::test]
283    async fn test_has_command_err() {
284        // we assume thequickbrownfoxjumpsoverthelazydog command does not exist
285        assert!(
286            !has_command("thequickbrownfoxjumpsoverthelazydog")
287                .await
288                .unwrap()
289        );
290    }
291
292    #[test]
293    fn test_flags() {
294        assert!(country_flag_from_iso_code("ES") == "πŸ‡ͺπŸ‡Έ");
295        assert!(country_flag_from_iso_code("US") == "πŸ‡ΊπŸ‡Έ");
296        assert!(country_flag_from_iso_code("USA") == "USA");
297    }
298}