lazybar_core/
common.rs

1use std::{
2    collections::HashMap,
3    hash::BuildHasher,
4    rc::Rc,
5    sync::{Arc, Mutex},
6};
7
8use anyhow::Result;
9use config::Value;
10use derive_builder::Builder;
11use derive_debug::Dbg;
12use futures::task::AtomicWaker;
13use pangocairo::functions::show_layout;
14
15#[cfg(feature = "cursor")]
16use crate::bar::CursorInfo;
17use crate::{
18    actions::Actions,
19    attrs::Attrs,
20    bar::{Dependence, PanelDrawInfo},
21    image::Image,
22    remove_array_from_config, remove_bool_from_config,
23    remove_string_from_config, Highlight, PanelHideFn, PanelShowFn, Ramp,
24};
25
26/// A [`PanelShowFn`] and a [`PanelHideFn`] bundled together. Only for use with
27/// [`PanelCommon::draw`].
28#[derive(Dbg)]
29pub enum ShowHide {
30    /// This is designed for use with a [`ManagedIntervalStream`], but other
31    /// streams can also work.
32    ///
33    /// The show function sets the bool to false and wakes the Waker. The hide
34    /// function sets the bool to true (indicating paused).
35    Default(Arc<Mutex<bool>>, Arc<AtomicWaker>),
36    /// The functions are provided.
37    #[dbg(skip)]
38    Custom(Option<PanelShowFn>, Option<PanelHideFn>),
39    /// The functions are no-ops.
40    None,
41}
42
43/// The common part of most [`PanelConfigs`][crate::PanelConfig]. Stores format
44/// strings, [`Attrs`], and [`Dependence`]
45#[derive(Debug, Clone, Builder)]
46#[builder_struct_attr(allow(missing_docs))]
47#[builder_impl_attr(allow(missing_docs))]
48pub struct PanelCommon {
49    /// Whether the panel depends on its neighbors
50    pub dependence: Dependence,
51    /// The events that should be run on mouse events
52    pub actions: Actions,
53    /// The images that will be displayed on the bar
54    pub images: Vec<Image>,
55    /// Whether the panel should be visible on startup
56    pub visible: bool,
57}
58
59impl PanelCommon {
60    /// The end of a typical draw function.
61    ///
62    /// Takes a cairo context, a string to
63    /// display, and attributes to use, and returns a closure that will do the
64    /// drawing and a tuple representing the final width and height.
65    ///
66    /// The text will be interpreted as markup. If this is not your intended
67    /// behavior, use [`markup_escape_text`][crate::markup_escape_text] to
68    /// display what you want or implement this functionality manually.
69    pub fn draw(
70        &self,
71        cr: &Rc<cairo::Context>,
72        text: &str,
73        attrs: &Attrs,
74        dependence: Dependence,
75        highlight: Option<Highlight>,
76        images: Vec<Image>,
77        height: i32,
78        show_hide: ShowHide,
79        dump: String,
80    ) -> Result<PanelDrawInfo> {
81        let layout = pangocairo::functions::create_layout(cr);
82        layout.set_markup(text);
83        attrs.apply_font(&layout);
84        let dims = layout.pixel_size();
85
86        let attrs = attrs.clone();
87        let bg = attrs.bg.clone().unwrap_or_default();
88
89        let (show, hide): (Option<PanelShowFn>, Option<PanelHideFn>) =
90            match show_hide {
91                ShowHide::Default(paused, waker) => {
92                    let paused_ = paused.clone();
93                    (
94                        Some(Box::new(move || {
95                            *paused.lock().unwrap() = false;
96                            waker.wake();
97                            Ok(())
98                        })),
99                        Some(Box::new(move || {
100                            *paused_.lock().unwrap() = true;
101                            Ok(())
102                        })),
103                    )
104                }
105                ShowHide::Custom(show, hide) => (show, hide),
106                ShowHide::None => (None, None),
107            };
108
109        Ok(PanelDrawInfo::new(
110            bg.adjust_dims(dims, height),
111            dependence,
112            Box::new(move |cr, _| {
113                let offset =
114                    bg.draw(cr, dims.0 as f64, dims.1 as f64, height as f64)?;
115
116                for image in &images {
117                    image.draw(cr)?;
118                }
119
120                cr.save()?;
121
122                cr.translate(offset, 0.0);
123                if let Some(ref highlight) = highlight {
124                    highlight.draw(cr, height as f64, dims.0 as f64)?;
125                }
126
127                cr.translate(0.0, (height - dims.1) as f64 / 2.0);
128
129                attrs.apply_fg(cr);
130                show_layout(cr, &layout);
131                cr.restore()?;
132                Ok(())
133            }),
134            show,
135            hide,
136            None,
137            #[cfg(feature = "cursor")]
138            CursorInfo::Static(self.actions.get_cursor()),
139            dump,
140        ))
141    }
142
143    /// Parses a single format from a subset of the global config.
144    pub fn parse_format<S: BuildHasher>(
145        table: &mut HashMap<String, Value, S>,
146        suffix: &'static str,
147        default: &'static str,
148    ) -> String {
149        let format = remove_string_from_config(
150            format!("format{suffix}").as_str(),
151            table,
152        )
153        .unwrap_or_else(|| (*default).to_string());
154        log::debug!("got format: {:?}", format);
155        format
156    }
157
158    /// Parses a fixed-size group of formats from a subset of the global config.
159    pub fn parse_formats<S: BuildHasher, const N: usize>(
160        table: &mut HashMap<String, Value, S>,
161        suffixes: &[&'static str; N],
162        defaults: &[&'static str; N],
163    ) -> [String; N] {
164        let mut formats = [const { String::new() }; N];
165        let mut config = suffixes.iter().zip(defaults);
166        for format in &mut formats {
167            let (suffix, default) = config.next().unwrap();
168            *format = remove_string_from_config(
169                format!("format{suffix}").as_str(),
170                table,
171            )
172            .unwrap_or_else(|| (*default).to_string());
173        }
174        log::debug!("got formats: {:?}", formats);
175        formats
176    }
177
178    /// Parses a variable-size group of formats from a subset of the global
179    /// config.
180    ///
181    /// The formats should be specified with `formats = ["format1", "format2",
182    /// ...]`
183    pub fn parse_formats_variadic<S: BuildHasher>(
184        table: &mut HashMap<String, Value, S>,
185        default: &[&'static str],
186    ) -> Vec<String> {
187        let formats = remove_array_from_config("formats", table).map_or_else(
188            || default.iter().map(ToString::to_string).collect::<Vec<_>>(),
189            |arr| {
190                arr.into_iter()
191                    .filter_map(|v| v.into_string().ok())
192                    .collect::<Vec<_>>()
193            },
194        );
195        log::debug!("got formats: {:?}", formats);
196        formats
197    }
198
199    /// Parses a single [`Attrs`] from a subset of the global config.
200    pub fn parse_attr<S: BuildHasher>(
201        table: &mut HashMap<String, Value, S>,
202        suffix: &'static str,
203    ) -> Attrs {
204        let attr =
205            remove_string_from_config(format!("attrs{suffix}").as_str(), table)
206                .map_or_else(Attrs::default, |name| {
207                    Attrs::parse(name).unwrap_or_default()
208                });
209        log::debug!("got attrs: {:?}", attr);
210        attr
211    }
212
213    /// Parses a fixed-size group of [`Attrs`] from a subset of the global
214    /// config.
215    pub fn parse_attrs<S: BuildHasher, const N: usize>(
216        table: &mut HashMap<String, Value, S>,
217        suffixes: &[&'static str; N],
218    ) -> [Attrs; N] {
219        let mut attrs = [const { Attrs::empty() }; N];
220        let mut config = suffixes.iter();
221        for attr in &mut attrs {
222            let suffix = config.next().unwrap();
223            if let Some(name) = remove_string_from_config(
224                format!("attrs{suffix}").as_str(),
225                table,
226            ) {
227                if let Ok(res) = Attrs::parse(name) {
228                    *attr = res;
229                }
230            }
231        }
232        log::debug!("got attrs: {:?}", attrs);
233        attrs
234    }
235
236    /// Parses a single [`Ramp`] from a subset of the global config.
237    pub fn parse_ramp<S: BuildHasher>(
238        table: &mut HashMap<String, Value, S>,
239        suffix: &'static str,
240    ) -> Ramp {
241        let ramp =
242            remove_string_from_config(format!("ramp{suffix}").as_str(), table)
243                .and_then(Ramp::parse)
244                .unwrap_or_default();
245        log::debug!("got ramps: {:?}", ramp);
246        ramp
247    }
248
249    /// Parses a fixed-size group of [`Ramp`]s from a subset of the global
250    /// config.
251    pub fn parse_ramps<S: BuildHasher, const N: usize>(
252        table: &mut HashMap<String, Value, S>,
253        suffixes: &[&'static str; N],
254    ) -> [Ramp; N] {
255        let mut ramps = [const { Ramp::empty() }; N];
256        let mut config = suffixes.iter();
257        for ramp in &mut ramps {
258            let suffix = config.next().unwrap();
259            if let Some(name) = remove_string_from_config(
260                format!("ramp{suffix}").as_str(),
261                table,
262            ) {
263                if let Some(res) = Ramp::parse(name) {
264                    *ramp = res;
265                }
266            }
267        }
268        log::debug!("got ramps: {:?}", ramps);
269        ramps
270    }
271
272    /// Parses a single [`Highlight`] from a subset of the global config.
273    pub fn parse_highlight<S: BuildHasher>(
274        table: &mut HashMap<String, Value, S>,
275        suffix: &'static str,
276    ) -> Highlight {
277        let highlight = remove_string_from_config(
278            format!("highlight{suffix}").as_str(),
279            table,
280        )
281        .and_then(Highlight::parse)
282        .unwrap_or_default();
283        log::debug!("got highlights: {:?}", highlight);
284        highlight
285    }
286
287    /// Parses a fixed-size group of [`Highlight`]s from the global config.
288    pub fn parse_highlights<S: BuildHasher, const N: usize>(
289        table: &mut HashMap<String, Value, S>,
290        suffixes: &[&'static str; N],
291    ) -> [Highlight; N] {
292        let mut highlights = [const { Highlight::empty() }; N];
293        let mut config = suffixes.iter();
294        for highlight in &mut highlights {
295            let suffix = config.next().unwrap();
296            if let Some(name) = remove_string_from_config(
297                format!("highlight{suffix}").as_str(),
298                table,
299            ) {
300                if let Some(res) = Highlight::parse(name) {
301                    *highlight = res;
302                }
303            }
304        }
305        log::debug!("got highlights: {:?}", highlights);
306        highlights
307    }
308
309    /// Attempts to parse common panel configuration options from a subset of
310    /// the global [`Config`][config::Config].
311    ///
312    /// Format strings should be specified as `format{suffix} = "value"`. Where
313    /// not noted, panels accept one format string with no suffix.
314    ///
315    /// Dependence should be specified as `dependence = "value"`, where value is
316    /// a valid variant of [`Dependence`].
317    ///
318    /// See [`Actions::parse`] and [`Image::parse`] for more parsing details.
319    pub fn parse_common<S: BuildHasher>(
320        table: &mut HashMap<String, Value, S>,
321    ) -> Result<Self> {
322        let mut builder = PanelCommonBuilder::default();
323
324        builder.dependence(
325            match remove_string_from_config("dependence", table)
326                .map(|s| s.to_lowercase())
327                .as_deref()
328            {
329                Some("left") => Dependence::Left,
330                Some("right") => Dependence::Right,
331                Some("both") => Dependence::Both,
332                _ => Dependence::None,
333            },
334        );
335        log::debug!("got dependence: {:?}", builder.dependence);
336
337        builder.actions(Actions::parse(table)?);
338        log::debug!("got actions: {:?}", builder.actions);
339
340        builder.images(remove_array_from_config("images", table).map_or_else(
341            Vec::new,
342            |a| {
343                a.into_iter()
344                    .filter_map(|i| {
345                        Image::parse(
346                            i.into_string()
347                                .map_err(|e| {
348                                    log::warn!("Failed to parse string: {e}");
349                                })
350                                .ok()?
351                                .as_str(),
352                        )
353                        .map_err(|e| log::warn!("Failed to parse image: {e}"))
354                        .ok()
355                    })
356                    .collect()
357            },
358        ));
359        log::debug!("got images: {:?}", builder.images);
360
361        builder
362            .visible(remove_bool_from_config("visible", table).unwrap_or(true));
363
364        Ok(builder.build()?)
365    }
366}