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(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 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 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 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 let (None, Some(extension)) = (file.extension(), extension) {
67 let file = file.with_extension(extension);
68 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#[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 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 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
229pub 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 b1 += 0xa5;
239 b2 += 0xa5;
240 b1 = 0xa0 | (b1 & 0x1f);
243 b2 = 0xa0 | (b2 & 0x1f);
244 String::from_utf8(vec![0xf0, 0x9f, 0x87, b1, 0xf0, 0x9f, 0x87, b2]).unwrap()
246}
247
248#[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 assert!(has_command("sh").await.unwrap());
280 }
281
282 #[tokio::test]
283 async fn test_has_command_err() {
284 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}