penrose_ui/bar/
schedule.rs

1//! Utilities for running scheduled updates to widgets
2use crate::bar::widgets::Text;
3use penrose::util::spawn_with_args;
4use std::{
5    cmp::max,
6    fmt,
7    sync::{Arc, Mutex},
8    thread,
9    time::{Duration, Instant},
10};
11use tracing::trace;
12
13/// The minimum allowed interval for an [UpdateSchedule].
14pub const MIN_DURATION: Duration = Duration::from_secs(1);
15
16/// For widgets that want to have their content updated periodically by the status bar by calling
17/// an external function.
18///
19/// See [IntervalText] for a simple implementation of this behaviour
20pub struct UpdateSchedule {
21    pub(crate) next: Instant,
22    pub(crate) interval: Duration,
23    pub(crate) get_text: Box<dyn Fn() -> Option<String> + Send + 'static>,
24    pub(crate) txt: Arc<Mutex<Text>>,
25}
26
27impl fmt::Debug for UpdateSchedule {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        f.debug_struct("UpdateSchedule")
30            .field("next", &self.next)
31            .field("interval", &self.interval)
32            .field("txt", &self.txt)
33            .finish()
34    }
35}
36
37impl UpdateSchedule {
38    /// Construct a new [UpdateSchedule] specifying the interval that the [Widget] content should
39    /// be updated on and an update function for producing the widget content.
40    ///
41    /// The updated content will then be stored in the provided `Arc<Mutex<Text>>` for access
42    /// within your widget logic.
43    pub fn new(
44        interval: Duration,
45        get_text: Box<dyn Fn() -> Option<String> + Send + 'static>,
46        txt: Arc<Mutex<Text>>,
47    ) -> Self {
48        if interval < MIN_DURATION {
49            panic!("UpdateSchedule interval is too small: {interval:?} < {MIN_DURATION:?}");
50        }
51
52        Self {
53            next: Instant::now(),
54            interval,
55            get_text,
56            txt,
57        }
58    }
59
60    /// Call our `get_text` function to update the contents of our paired [CronText] and then bump
61    /// our `next` time to the next interval point.
62    ///
63    /// This is gives us behaviour of a consistent interval between invocation end/start but not
64    /// necessarily a consistent interval between start/start depending on how long `get_text`
65    /// takes to run.
66    fn update_text(&mut self) {
67        trace!("running UpdateSchedule get_text");
68        let s = (self.get_text)();
69        trace!(?s, "ouput from running get_text");
70
71        if let Some(s) = s {
72            let mut t = match self.txt.lock() {
73                Ok(inner) => inner,
74                Err(poisoned) => poisoned.into_inner(),
75            };
76            t.set_text(s);
77        }
78
79        let next = self.next + self.interval;
80        let now = Instant::now();
81        self.next = max(next, now);
82        trace!(next = ?self.next, "next update at");
83    }
84}
85
86/// Run the polling thread for a set of [UpdateSchedule]s and update their contents on
87/// their requested intervals.
88pub(crate) fn run_update_schedules(mut schedules: Vec<UpdateSchedule>) {
89    thread::spawn(move || {
90        loop {
91            trace!("running UpdateSchedule updates for all pending widgets");
92            while schedules[0].next < Instant::now() {
93                schedules[0].update_text();
94                schedules.sort_by(|a, b| a.next.cmp(&b.next));
95            }
96
97            // FIXME: this is a hack at the moment to ensure that an event drops into the main
98            // window manager event loop and triggers the `on_event` hook of the status bar.
99            let _ = spawn_with_args("xsetroot", &["-name", ""]);
100
101            let interval = schedules[0].next - Instant::now();
102            trace!(?interval, "sleeping until next update point");
103            thread::sleep(interval);
104        }
105    });
106}