penrose_ui/bar/
mod.rs

1//! A lightweight and configurable status bar for penrose
2use crate::{Result, core::Draw};
3use penrose::{
4    Color, WinId,
5    core::{State, WindowManager},
6    pure::geometry::Rect,
7    x::{Atom, ClientConfig, Prop, WinType, XConn, event::XEvent},
8};
9use std::fmt;
10use tracing::{debug, error, info};
11
12pub mod schedule;
13pub mod widgets;
14
15use schedule::{UpdateSchedule, run_update_schedules};
16use widgets::Widget;
17
18/// The position of a status bar
19#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
20pub enum Position {
21    /// Top of the screen
22    Top,
23    /// Bottom of the screen
24    Bottom,
25}
26
27/// A group of [Widget]s and associated point size to use for rendering a [StatusBar] on a single
28/// screen.
29pub struct PerScreen<X: XConn> {
30    point_size: u8,
31    h: u32,
32    ws: Vec<Box<dyn Widget<X>>>,
33}
34
35impl<X: XConn> fmt::Debug for PerScreen<X> {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        f.debug_struct("PerScreen")
38            .field("point_size", &self.point_size)
39            .field("h", &self.h)
40            .finish()
41    }
42}
43
44impl<X: XConn> PerScreen<X> {
45    /// Construct a new per-screen set of widgets with an associated point size for the font.
46    pub fn new(point_size: u8, h: u32, ws: Vec<Box<dyn Widget<X>>>) -> Self {
47        Self { point_size, ws, h }
48    }
49}
50
51#[derive(Debug)]
52enum Widgets<X: XConn> {
53    Shared(PerScreen<X>),
54    PerScreen(Vec<PerScreen<X>>),
55}
56
57impl<X: XConn> Widgets<X> {
58    fn for_screen_mut(&mut self, ix: usize) -> &mut PerScreen<X> {
59        match self {
60            Self::Shared(ps) => ps,
61            Self::PerScreen(pss) => {
62                let ix = if ix >= pss.len() { pss.len() - 1 } else { ix };
63                &mut pss[ix]
64            }
65        }
66    }
67
68    fn for_each_mut<F>(&mut self, n_screens: usize, mut f: F)
69    where
70        F: FnMut(&mut Box<dyn Widget<X>>),
71    {
72        match self {
73            Self::Shared(ps) => ps.ws.iter_mut().for_each(f),
74            Self::PerScreen(pss) => pss
75                .iter_mut()
76                .take(n_screens) // avoid checking widgets that are not in use
77                .for_each(|ps| ps.ws.iter_mut().for_each(&mut f)),
78        }
79    }
80
81    fn require_draw(&self, n_screens: usize) -> bool {
82        match self {
83            Self::Shared(ps) => ps.ws.iter().any(|w| w.require_draw()),
84            Self::PerScreen(pss) => pss
85                .iter()
86                .take(n_screens) // avoid checking widgets that are not in use
87                .any(|ps| ps.ws.iter().any(|w| w.require_draw())),
88        }
89    }
90
91    fn update_schedules(&mut self) -> Vec<UpdateSchedule> {
92        match self {
93            Self::Shared(ps) => ps
94                .ws
95                .iter_mut()
96                .filter_map(|w| w.update_schedule())
97                .collect(),
98            Self::PerScreen(pss) => pss
99                .iter_mut()
100                .flat_map(|ps| ps.ws.iter_mut().filter_map(|w| w.update_schedule()))
101                .collect(),
102        }
103    }
104}
105
106/// A simple text based status bar that renders a user defined array of [`Widget`]s.
107#[derive(Debug)]
108pub struct StatusBar<X: XConn> {
109    draw: Draw,
110    position: Position,
111    widgets: Widgets<X>,
112    screens: Vec<(WinId, u32)>,
113    active_screen: usize,
114    font: String,
115    redrawn: bool,
116}
117
118impl<X: XConn> StatusBar<X> {
119    /// Try to initialise a new empty status bar. Can fail if we are unable to create a
120    /// new window for each bar.
121    pub fn try_new(
122        position: Position,
123        h: u32,
124        bg: impl Into<Color>,
125        font: &str,
126        point_size: u8,
127        widgets: Vec<Box<dyn Widget<X>>>,
128    ) -> Result<Self> {
129        let bg = bg.into();
130        let draw = Draw::new(font, point_size, bg)?;
131
132        Ok(Self {
133            draw,
134            position,
135            widgets: Widgets::Shared(PerScreen::new(point_size, h, widgets)),
136            screens: vec![],
137            active_screen: 0,
138            font: font.to_string(),
139            redrawn: false,
140        })
141    }
142
143    /// Try to create a new status bar using a different arrangement of widgets for each screen.
144    ///
145    /// If more screens are attached than available widget arrangements, the last widget
146    /// arrangement will be used as a fallback.
147    pub fn try_new_per_screen(
148        position: Position,
149        bg: impl Into<Color>,
150        font: &str,
151        widgets: Vec<PerScreen<X>>,
152    ) -> Result<Self> {
153        let bg = bg.into();
154        let point_size = widgets[0].point_size;
155        let mut draw = Draw::new(font, point_size, bg)?;
156        for &PerScreen { point_size, .. } in widgets.iter() {
157            draw.add_font(font, point_size)?;
158        }
159
160        Ok(Self {
161            draw,
162            position,
163            widgets: Widgets::PerScreen(widgets),
164            screens: vec![],
165            active_screen: 0,
166            font: font.to_string(),
167            redrawn: false,
168        })
169    }
170
171    /// Add this [`StatusBar`] into the given [`WindowManager`] along with the required
172    /// hooks for driving it from the main WindowManager event loop.
173    ///
174    /// If any [UpdateSchedule]s are requested by [Widgets] then they will be extracted and run as
175    /// part of calling this method.
176    pub fn add_to(mut self, mut wm: WindowManager<X>) -> WindowManager<X>
177    where
178        X: 'static,
179    {
180        let schedules = self.widgets.update_schedules();
181        if !schedules.is_empty() {
182            run_update_schedules(schedules);
183        }
184
185        wm.state.add_extension(self);
186        wm.state.config.compose_or_set_event_hook(event_hook);
187        wm.state.config.compose_or_set_manage_hook(manage_hook);
188        wm.state.config.compose_or_set_refresh_hook(refresh_hook);
189        wm.state.config.compose_or_set_startup_hook(startup_hook);
190
191        wm
192    }
193
194    fn init_for_screens(&mut self) -> Result<()> {
195        info!("initialising per screen status bar windows");
196        let screen_details = self.draw.conn.screen_details()?;
197
198        self.screens = screen_details
199            .iter()
200            .enumerate()
201            .map(|(i, &Rect { x, y, w, h })| {
202                let bar_h = self.widgets.for_screen_mut(i).h;
203                let y = match self.position {
204                    Position::Top => y,
205                    Position::Bottom => y + (h - bar_h) as i32,
206                };
207
208                debug!("creating new window");
209                let id = self.draw.new_window(
210                    WinType::InputOutput(Atom::NetWindowTypeDock),
211                    Rect::new(x, y, w, bar_h),
212                    false,
213                )?;
214
215                let data = &[ClientConfig::StackBottom];
216                self.draw.conn.set_client_config(id, data)?;
217
218                debug!(%id, "setting props");
219                let p = Prop::UTF8String(vec!["penrose-statusbar".to_string()]);
220                for atom in &[Atom::NetWmName, Atom::WmName, Atom::WmClass] {
221                    self.draw.conn.set_prop(id, atom.as_ref(), p.clone())?;
222                }
223
224                debug!("flushing");
225                self.draw.flush(id)?;
226
227                Ok((id, w))
228            })
229            .collect::<Result<Vec<(WinId, u32)>>>()?;
230
231        Ok(())
232    }
233
234    /// Re-render all widgets in this status bar for a single screen.
235    /// Will panic if `i` is out of bounds
236    fn redraw_screen(&mut self, i: usize) -> Result<()> {
237        let (id, w_screen) = self.screens[i];
238        let screen_has_focus = self.active_screen == i;
239        let ps = self.widgets.for_screen_mut(i);
240
241        self.draw.set_font(&self.font, ps.point_size)?;
242        let mut ctx = self.draw.context_for(id)?;
243        ctx.clear()?;
244
245        let mut extents = Vec::new();
246        let mut greedy_indices = Vec::new();
247
248        for (j, w) in ps.ws.iter_mut().enumerate() {
249            extents.push(w.current_extent(&mut ctx, ps.h)?);
250            if w.is_greedy() {
251                greedy_indices.push(j)
252            }
253        }
254
255        let total = extents.iter().map(|(w, _)| w).sum::<u32>();
256        let n_greedy = greedy_indices.len();
257
258        if total < w_screen && n_greedy > 0 {
259            let per_greedy = (w_screen - total) / n_greedy as u32;
260            for i in greedy_indices.iter() {
261                let (w, h) = extents[*i];
262                extents[*i] = (w + per_greedy, h);
263            }
264        }
265
266        let mut x = 0;
267        for (wd, (w, _)) in ps.ws.iter_mut().zip(extents) {
268            wd.draw(&mut ctx, self.active_screen, screen_has_focus, w, ps.h)?;
269            x += w;
270            ctx.set_x_offset(x as i32);
271        }
272
273        self.draw.flush(id)?;
274
275        Ok(())
276    }
277
278    /// Re-render all widgets in this status bar for each screen it is displayed on
279    pub fn redraw(&mut self) -> Result<()> {
280        for i in 0..self.screens.len() {
281            self.redraw_screen(i)?;
282        }
283        self.redrawn = true;
284
285        Ok(())
286    }
287
288    fn redraw_if_needed(&mut self) -> Result<()> {
289        if self.widgets.require_draw(self.screens.len()) {
290            self.redraw()?;
291        }
292
293        Ok(())
294    }
295}
296
297/// Run any widget startup actions and then redraw
298pub fn startup_hook<X: XConn + 'static>(state: &mut State<X>, x: &mut X) -> penrose::Result<()> {
299    let s = state.extension::<StatusBar<X>>()?;
300    let mut bar = s.borrow_mut();
301
302    if let Err(e) = bar.init_for_screens() {
303        error!(%e, "unabled to initialise for screens");
304        return Err(penrose::Error::NoScreens);
305    }
306
307    info!("running startup widget hooks");
308    let n_screens = bar.screens.len();
309    bar.widgets.for_each_mut(n_screens, |w| {
310        if let Err(e) = w.on_startup(state, x) {
311            error!(%e, "error running widget startup hook");
312        };
313    });
314
315    if let Err(e) = bar.redraw() {
316        error!(%e, "error redrawing status bar");
317    }
318
319    Ok(())
320}
321
322/// Run any widget refresh actions and then redraw if needed
323pub fn refresh_hook<X: XConn + 'static>(state: &mut State<X>, x: &mut X) -> penrose::Result<()> {
324    let s = state.extension::<StatusBar<X>>()?;
325    let mut bar = s.borrow_mut();
326
327    bar.active_screen = state.client_set.current_screen().index();
328    let n_screens = bar.screens.len();
329    bar.widgets.for_each_mut(n_screens, |w| {
330        if let Err(e) = w.on_refresh(state, x) {
331            error!(%e, "error running widget refresh hook");
332        }
333    });
334
335    // the redrawn flag is set to false at the start of our event hook. If neither of the other
336    // hooks trigger a redraw then we force one now on refresh to ensure that we're up to date.
337    let res = if !bar.redrawn {
338        bar.redraw()
339    } else {
340        bar.redraw_if_needed()
341    };
342
343    if let Err(e) = res {
344        error!(%e, "error redrawing status bar");
345    }
346
347    Ok(())
348}
349
350/// Run any widget event actions and then redraw if needed
351pub fn event_hook<X: XConn + 'static>(
352    event: &XEvent,
353    state: &mut State<X>,
354    x: &mut X,
355) -> penrose::Result<bool> {
356    use XEvent::{ConfigureNotify, RandrNotify};
357
358    let s = state.extension::<StatusBar<X>>()?;
359    let mut bar = s.borrow_mut();
360    bar.redrawn = false;
361
362    if matches!(event, RandrNotify) || matches!(event, ConfigureNotify(e) if e.is_root) {
363        info!("screens have changed: recreating status bars");
364        let screens: Vec<_> = bar.screens.drain(..).collect();
365
366        for (id, _) in screens {
367            info!(%id, "removing previous status bar");
368            if let Err(e) = bar.draw.destroy_window_and_surface(id) {
369                error!(%e, "error when removing previous status bar state");
370            }
371        }
372
373        if let Err(e) = bar.init_for_screens() {
374            error!(%e, "unabled to initialise for screens");
375            return Err(penrose::Error::NoScreens);
376        }
377    }
378
379    bar.active_screen = state.client_set.current_screen().index();
380    let n_screens = bar.screens.len();
381    bar.widgets.for_each_mut(n_screens, |w| {
382        if let Err(e) = w.on_event(event, state, x) {
383            error!(%e, "error running widget event hook");
384        };
385    });
386
387    if let Err(e) = bar.redraw_if_needed() {
388        error!(%e, "error redrawing status bar");
389    }
390
391    Ok(true)
392}
393
394/// Run any widget on_new_client actions and then redraw if needed
395pub fn manage_hook<X: XConn + 'static>(
396    id: WinId,
397    state: &mut State<X>,
398    x: &mut X,
399) -> penrose::Result<()> {
400    let s = state.extension::<StatusBar<X>>()?;
401    let mut bar = s.borrow_mut();
402
403    bar.active_screen = state.client_set.current_screen().index();
404    let n_screens = bar.screens.len();
405    bar.widgets.for_each_mut(n_screens, |w| {
406        if let Err(e) = w.on_new_client(id, state, x) {
407            error!(%e, "error running widget manage hook");
408        }
409    });
410
411    if let Err(e) = bar.redraw_if_needed() {
412        error!(%e, "error redrawing status bar");
413    }
414
415    Ok(())
416}