lazybar_core/
utils.rs

1use std::{
2    borrow::Cow,
3    collections::HashMap,
4    env,
5    pin::Pin,
6    sync::{Arc, Mutex},
7    task::{Context, Poll},
8    time::Duration,
9};
10
11use anyhow::Result;
12use config::{Map, Value, ValueKind};
13use csscolorparser::Color;
14use derive_builder::Builder;
15use futures::{task::AtomicWaker, Stream};
16use lazy_static::lazy_static;
17use lazybar_types::EventResponse;
18use regex::{Captures, Regex};
19use tokio::{
20    io::AsyncWriteExt,
21    net::UnixStream,
22    time::{interval, Instant, Interval},
23};
24
25use crate::{ipc::ChannelEndpoint, parser};
26
27lazy_static! {
28    static ref REGEX: Regex = Regex::new(r"%\{(?<const>[^}]+)}").unwrap();
29}
30
31/// A wrapper struct to read indefinitely from a [`UnixStream`] and send the
32/// results through a channel.
33#[derive(Debug)]
34pub struct UnixStreamWrapper {
35    inner: UnixStream,
36    endpoint: ChannelEndpoint<String, EventResponse>,
37}
38
39impl UnixStreamWrapper {
40    /// Creates a new wrapper from a stream and a sender
41    pub const fn new(
42        inner: UnixStream,
43        endpoint: ChannelEndpoint<String, EventResponse>,
44    ) -> Self {
45        Self { inner, endpoint }
46    }
47
48    /// Reads a message from the inner [`UnixStream`] and returns a response
49    pub async fn run(mut self) -> Result<()> {
50        let mut data = [0; 1024];
51        self.inner.readable().await?;
52        let len = self.inner.try_read(&mut data)?;
53        let message = String::from_utf8_lossy(&data[0..len]);
54        if message.len() == 0 {
55            return Ok(());
56        }
57        self.endpoint.send.send(message.to_string())?;
58        let response = self
59            .endpoint
60            .recv
61            .recv()
62            .await
63            .unwrap_or(EventResponse::Ok(None));
64
65        self.inner.writable().await?;
66        self.inner
67            .try_write(serde_json::to_string(&response)?.as_bytes())?;
68
69        self.inner.shutdown().await?;
70
71        Ok(())
72    }
73}
74
75///Custom [`IntervalStream`]
76///
77/// Similar to [`tokio_stream::wrappers::IntervalStream`], but its interval is
78/// wrapped by a [`Mutex`] and an [`Arc`], so it can be modified externally
79/// (e.g. in a [`PanelShowFn`][crate::PanelShowFn] or
80/// [`PanelHideFn`][crate::PanelHideFn]).
81///
82/// If unset, the interval has a length of 10 seconds.
83///
84/// Make sure to set the interval's
85/// [`MissedTickBehavior`][tokio::time::MissedTickBehavior] appropriately.
86#[derive(Builder, Clone, Debug)]
87#[builder_struct_attr(allow(missing_docs))]
88#[builder_impl_attr(allow(missing_docs))]
89pub struct ManagedIntervalStream {
90    #[builder(
91        default = "Arc::new(Mutex::new(interval(Duration::from_secs(10))))"
92    )]
93    interval: Arc<Mutex<Interval>>,
94    #[builder(default)]
95    paused: Arc<Mutex<bool>>,
96    #[builder(default)]
97    waker: Arc<AtomicWaker>,
98}
99
100impl ManagedIntervalStream {
101    /// Creates a new instance using the provided parts.
102    pub const fn new(
103        interval: Arc<Mutex<Interval>>,
104        paused: Arc<Mutex<bool>>,
105        waker: Arc<AtomicWaker>,
106    ) -> Self {
107        Self {
108            interval,
109            paused,
110            waker,
111        }
112    }
113
114    /// Provides access to the [`ManagedIntervalStreamBuilder`] without an
115    /// additional import.
116    #[must_use]
117    pub fn builder() -> ManagedIntervalStreamBuilder {
118        ManagedIntervalStreamBuilder::default()
119    }
120}
121
122impl Stream for ManagedIntervalStream {
123    type Item = Instant;
124
125    fn poll_next(
126        self: Pin<&mut Self>,
127        cx: &mut Context<'_>,
128    ) -> Poll<Option<Self::Item>> {
129        self.waker.register(cx.waker());
130        if *self.paused.lock().unwrap() {
131            Poll::Pending
132        } else {
133            let val = self.interval.lock().unwrap().poll_tick(cx).map(Some);
134            val
135        }
136    }
137}
138
139impl AsRef<Arc<Mutex<Interval>>> for ManagedIntervalStream {
140    fn as_ref(&self) -> &Arc<Mutex<Interval>> {
141        &self.interval
142    }
143}
144
145impl AsMut<Arc<Mutex<Interval>>> for ManagedIntervalStream {
146    fn as_mut(&mut self) -> &mut Arc<Mutex<Interval>> {
147        &mut self.interval
148    }
149}
150
151impl ManagedIntervalStreamBuilder {
152    /// Set the interval using a [`Duration`] instead of an [`Interval`]
153    pub fn duration(&mut self, duration: Duration) -> &mut Self {
154        self.interval(Arc::new(Mutex::new(interval(duration))))
155    }
156}
157
158/// Removes a value from a given config table and returns an attempt at parsing
159/// it into a table.
160pub fn get_table_from_config<S: std::hash::BuildHasher>(
161    id: &str,
162    table: &HashMap<String, Value, S>,
163) -> Option<Map<String, Value>> {
164    table.get(id).and_then(|val| {
165        val.clone().into_table().map_or_else(
166            |_| {
167                log::warn!("Ignoring non-table value {val:?}");
168                None
169            },
170            Some,
171        )
172    })
173}
174
175/// Removes a value from a given config table and returns an attempt at parsing
176/// it into a string.
177pub fn remove_string_from_config<S: std::hash::BuildHasher>(
178    id: &str,
179    table: &mut HashMap<String, Value, S>,
180) -> Option<String> {
181    table.remove(id).and_then(|val| {
182        val.clone().into_string().map_or_else(
183            |_| {
184                log::warn!("Ignoring non-string value {val:?}");
185                None
186            },
187            |s| {
188                Some(
189                    replace_consts(s.as_str(), parser::CONSTS.get().unwrap())
190                        .to_string(),
191                )
192            },
193        )
194    })
195}
196
197/// Removes a value from a given config table and returns an attempt at parsing
198/// it into an array.
199pub fn remove_array_from_config<S: std::hash::BuildHasher>(
200    id: &str,
201    table: &mut HashMap<String, Value, S>,
202) -> Option<Vec<Value>> {
203    table.remove(id).and_then(|val| {
204        val.clone().into_array().map_or_else(
205            |_| {
206                log::warn!("Ignoring non-array value {val:?}");
207                None
208            },
209            |v| {
210                Some(
211                    v.into_iter()
212                        .map(|val| {
213                            let origin = val.origin().map(ToString::to_string);
214                            val.clone().into_string().map_or(val, |val| {
215                                Value::new(
216                                    origin.as_ref(),
217                                    ValueKind::String(
218                                        replace_consts(
219                                            val.as_str(),
220                                            parser::CONSTS.get().unwrap(),
221                                        )
222                                        .to_string(),
223                                    ),
224                                )
225                            })
226                        })
227                        .collect(),
228                )
229            },
230        )
231    })
232}
233
234/// Removes a value from a given config table and returns an attempt at parsing
235/// it into a uint.
236pub fn remove_uint_from_config<S: std::hash::BuildHasher>(
237    id: &str,
238    table: &mut HashMap<String, Value, S>,
239) -> Option<u64> {
240    table.remove(id).and_then(|val| {
241        val.clone().into_uint().map_or_else(
242            |_| {
243                log::warn!("Ignoring non-uint value {val:?}");
244                None
245            },
246            Some,
247        )
248    })
249}
250
251/// Removes a value from a given config table and returns an attempt at parsing
252/// it into a bool.
253pub fn remove_bool_from_config<S: std::hash::BuildHasher>(
254    id: &str,
255    table: &mut HashMap<String, Value, S>,
256) -> Option<bool> {
257    table.remove(id).and_then(|val| {
258        val.clone().into_bool().map_or_else(
259            |_| {
260                log::warn!("Ignoring non-boolean value {val:?}");
261                None
262            },
263            Some,
264        )
265    })
266}
267
268/// Removes a value from a given config table and returns an attempt at parsing
269/// it into a float.
270pub fn remove_float_from_config<S: std::hash::BuildHasher>(
271    id: &str,
272    table: &mut HashMap<String, Value, S>,
273) -> Option<f64> {
274    table.remove(id).and_then(|val| {
275        val.clone().into_float().map_or_else(
276            |_| {
277                log::warn!("Ignoring non-float value {val:?}");
278                None
279            },
280            Some,
281        )
282    })
283}
284
285/// Removes a value from a given config table and returns an attempt at parsing
286/// it into a color.
287pub fn remove_color_from_config<S: std::hash::BuildHasher>(
288    id: &str,
289    table: &mut HashMap<String, Value, S>,
290) -> Option<Color> {
291    table.remove(id).and_then(|val| {
292        val.clone().into_string().map_or_else(
293            |_| {
294                log::warn!("Ignoring non-string value {val:?}");
295                None
296            },
297            |val| {
298                replace_consts(val.as_str(), parser::CONSTS.get().unwrap())
299                    .parse()
300                    .map_or_else(
301                        |_| {
302                            log::warn!("Invalid color {val}");
303                            None
304                        },
305                        Some,
306                    )
307            },
308        )
309    })
310}
311
312/// Replaces references to constants (of the form `%{const_name}`) with their
313/// respective constants.
314pub fn replace_consts<'a, S: std::hash::BuildHasher>(
315    format: &'a str,
316    consts: &HashMap<String, Value, S>,
317) -> Cow<'a, str> {
318    REGEX.replace_all(format, |caps: &Captures| {
319        let con = &caps["const"];
320        if let Some(c) = con.strip_prefix("env:") {
321            if let Ok(c) = env::var(c) {
322                return c;
323            }
324        }
325        consts
326            .get(con)
327            .and_then(|c| c.clone().into_string().ok())
328            .map_or_else(
329                || {
330                    log::warn!("Invalid constant: {con}");
331                    String::new()
332                },
333                |con| con,
334            )
335    })
336}