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
10pub fn find_file(
18 file: &str,
19 subdir: Option<&str>,
20 extension: Option<&str>,
21) -> Result<Option<PathBuf>> {
22 let file = Path::new(file);
23
24 if file.is_absolute() && file.try_exists().error("Unable to stat file")? {
25 return Ok(Some(file.to_path_buf()));
26 }
27
28 if let Some(mut xdg_config) = config_dir() {
30 xdg_config.push("i3status-rust");
31 if let Some(subdir) = subdir {
32 xdg_config.push(subdir);
33 }
34 xdg_config.push(file);
35 if let Some(file) = exists_with_opt_extension(&xdg_config, extension)? {
36 return Ok(Some(file));
37 }
38 }
39
40 if let Some(mut xdg_data) = data_dir() {
42 xdg_data.push("i3status-rust");
43 if let Some(subdir) = subdir {
44 xdg_data.push(subdir);
45 }
46 xdg_data.push(file);
47 if let Some(file) = exists_with_opt_extension(&xdg_data, extension)? {
48 return Ok(Some(file));
49 }
50 }
51
52 let mut usr_share_path = PathBuf::from("/usr/share/i3status-rust");
54 if let Some(subdir) = subdir {
55 usr_share_path.push(subdir);
56 }
57 usr_share_path.push(file);
58 if let Some(file) = exists_with_opt_extension(&usr_share_path, extension)? {
59 return Ok(Some(file));
60 }
61
62 Ok(None)
63}
64
65fn exists_with_opt_extension(file: &Path, extension: Option<&str>) -> Result<Option<PathBuf>> {
66 if file.try_exists().error("Unable to stat file")? {
67 return Ok(Some(file.into()));
68 }
69 if let (None, Some(extension)) = (file.extension(), extension) {
71 let file = file.with_extension(extension);
72 if file.try_exists().error("Unable to stat file")? {
74 return Ok(Some(file));
75 }
76 }
77 Ok(None)
78}
79
80pub async fn new_dbus_connection() -> Result<zbus::Connection> {
81 zbus::Connection::session()
82 .await
83 .error("Failed to open DBus session connection")
84}
85
86pub async fn new_system_dbus_connection() -> Result<zbus::Connection> {
87 zbus::Connection::system()
88 .await
89 .error("Failed to open DBus system connection")
90}
91
92pub fn deserialize_toml_file<T, P>(path: P) -> Result<T>
93where
94 T: DeserializeOwned,
95 P: AsRef<Path>,
96{
97 let path = path.as_ref();
98
99 let contents = std::fs::read_to_string(path)
100 .or_error(|| format!("Failed to read file: {}", path.display()))?;
101
102 deserialize_toml_file_string(contents, path)
103}
104
105pub async fn async_deserialize_toml_file<T, P>(path: P) -> Result<T>
106where
107 T: DeserializeOwned,
108 P: AsRef<Path>,
109{
110 let path = path.as_ref();
111
112 let contents = read_file(path)
113 .await
114 .or_error(|| format!("Failed to read file: {}", path.display()))?;
115
116 deserialize_toml_file_string(contents, path)
117}
118
119fn deserialize_toml_file_string<T>(contents: String, path: &Path) -> Result<T>
120where
121 T: DeserializeOwned,
122{
123 toml::from_str(&contents).map_err(|err| {
124 let location_msg = err
125 .span()
126 .map(|span| {
127 if span == (0..0) {
128 String::new()
129 } else {
130 let line = 1 + contents.as_bytes()[..(span.start)]
131 .iter()
132 .filter(|b| **b == b'\n')
133 .count();
134 format!(" at line {line}")
135 }
136 })
137 .unwrap_or_default();
138 Error::new(format!(
139 "Failed to deserialize TOML file {}{}: {}",
140 path.display(),
141 location_msg,
142 err.message()
143 ))
144 })
145}
146
147pub async fn read_file(path: impl AsRef<Path>) -> std::io::Result<String> {
148 let mut file = tokio::fs::File::open(path).await?;
149 let mut content = String::new();
150 file.read_to_string(&mut content).await?;
151 Ok(content.trim_end().to_string())
152}
153
154pub async fn has_command(command: &str) -> Result<bool> {
155 Command::new("sh")
156 .args([
157 "-c",
158 format!("command -v {command} >/dev/null 2>&1").as_ref(),
159 ])
160 .status()
161 .await
162 .or_error(|| format!("Failed to check {command} presence"))
163 .map(|status| status.success())
164}
165
166#[macro_export]
181macro_rules! map {
182 (@extend $map:ident $( $([$($cond_tokens:tt)*])? $key:literal => $value:expr ),* $(,)?) => {{
183 $(
184 map!(@insert $map, $key, $value $(,$($cond_tokens)*)?);
185 )*
186 }};
187 (@extend $map:ident $( $key:expr => $value:expr ),* $(,)?) => {{
188 $(
189 map!(@insert $map, $key, $value);
190 )*
191 }};
192 (@insert $map:ident, $key:expr, $value:expr) => {{
193 $map.insert($key.into(), $value.into());
194 }};
195 (@insert $map:ident, $key:expr, $value:expr, if $cond:expr) => {{
196 if $cond {
197 $map.insert($key.into(), $value.into());
198 }
199 }};
200 (@insert $map:ident, $key:expr, $value:expr, if let $pat:pat = $match_on:expr) => {{
201 if let $pat = $match_on {
202 $map.insert($key.into(), $value.into());
203 }
204 }};
205 ($($tt:tt)*) => {{
206 #[allow(unused_mut)]
207 let mut m = ::std::collections::HashMap::new();
208 map!(@extend m $($tt)*);
209 m
210 }};
211}
212
213pub use map;
214
215macro_rules! regex {
216 ($re:literal $(,)?) => {{
217 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
218 RE.get_or_init(|| regex::Regex::new($re).unwrap())
219 }};
220}
221
222macro_rules! make_log_macro {
223 (@wdoll $macro_name:ident, $block_name:literal, ($dol:tt)) => {
224 #[allow(dead_code)]
225 macro_rules! $macro_name {
226 ($dol($args:tt)+) => {
227 ::log::$macro_name!(target: $block_name, $dol($args)+);
228 };
229 }
230 };
231 ($macro_name:ident, $block_name:literal) => {
232 make_log_macro!(@wdoll $macro_name, $block_name, ($));
233 };
234}
235
236pub fn format_bar_graph(content: &[f64]) -> String {
237 static BARS: [char; 8] = [
239 '\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}',
240 '\u{2588}',
241 ];
242
243 let mut min = f64::INFINITY;
245 let mut max = f64::NEG_INFINITY;
246 for &v in content {
247 min = min.min(v);
248 max = max.max(v);
249 }
250
251 let range = max - min;
252 content
253 .iter()
254 .map(|x| BARS[((x - min) / range * 7.).clamp(0., 7.) as usize])
255 .collect()
256}
257
258pub fn country_flag_from_iso_code(country_code: &str) -> String {
260 let [mut b1, mut b2]: [u8; 2] = country_code.as_bytes().try_into().unwrap_or([0, 0]);
261
262 if !b1.is_ascii_uppercase() || !b2.is_ascii_uppercase() {
263 return country_code.into();
264 }
265
266 b1 += 0xa5;
268 b2 += 0xa5;
269 b1 = 0xa0 | (b1 & 0x1f);
272 b2 = 0xa0 | (b2 & 0x1f);
273 String::from_utf8(vec![0xf0, 0x9f, 0x87, b1, 0xf0, 0x9f, 0x87, b2]).unwrap()
275}
276
277#[inline]
280pub fn default<T: Default>() -> T {
281 Default::default()
282}
283
284pub trait StreamExtDebounced: futures::StreamExt {
285 fn next_debounced(&mut self) -> impl Future<Output = Option<Self::Item>>;
286}
287
288impl<T: futures::StreamExt + Unpin> StreamExtDebounced for T {
289 async fn next_debounced(&mut self) -> Option<Self::Item> {
290 let mut result = self.next().await?;
291 let mut noop_ctx = std::task::Context::from_waker(std::task::Waker::noop());
292 loop {
293 match self.poll_next_unpin(&mut noop_ctx) {
294 std::task::Poll::Ready(Some(x)) => result = x,
295 std::task::Poll::Ready(None) | std::task::Poll::Pending => return Some(result),
296 }
297 }
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[tokio::test]
306 async fn test_has_command_ok() {
307 assert!(has_command("sh").await.unwrap());
309 }
310
311 #[tokio::test]
312 async fn test_has_command_err() {
313 assert!(
315 !has_command("thequickbrownfoxjumpsoverthelazydog")
316 .await
317 .unwrap()
318 );
319 }
320
321 #[test]
322 fn test_flags() {
323 assert!(country_flag_from_iso_code("ES") == "πͺπΈ");
324 assert!(country_flag_from_iso_code("US") == "πΊπΈ");
325 assert!(country_flag_from_iso_code("USA") == "USA");
326 }
327}