lazybar_core/
lib.rs

1//! This is a lightweight, event-driven status bar for EWMH-compliant window
2//! managers on X11.
3//!
4//! It uses [`tokio`] in combination with existing event APIs to poll as rarely
5//! as possible. For example, the [`Inotify`][panels::Inotify] panel uses
6//! Linux's inotify to monitor the contents of a file.
7//!
8//! You're welcome to use this crate as a library if you want to expand on the
9//! functionality included herein, but its intentended use case is to hold most
10//! of the logic for [`lazybar`](https://docs.rs/lazybar).
11//!
12//! At runtime, it reads a configuration file located at
13//! `$XDG_CONFIG_HOME/lazybar/config.toml`, and this documentation will focus on
14//! accepted syntax for that file. See [`panels`] for panel-specific
15//! information.
16//!
17//! The general structure of the file is as follows:
18//!
19//! Top-level tables:
20//! - `bars`: each subtable defines a bar, and the name is used as a command
21//!   line argument to run that bar.
22//! - `ramps`: each subtable defines a ramp with the same name, and those names
23//!   are referenced by panel tables (see below).
24//! - `panels`: each subtable defines a panel with the same name, and those
25//!   names are referenced by bar tables.
26//! - `attrs`: each subtable defines a set of attributes that can be referenced
27//!   by panels.
28//! - `bgs`: each subtable defines a background configuration (shape, color)
29//!   that can be referenced by attrs.
30//! - `highlights`: each subtable defines a highlight, or partial background,
31//!   that can be reference by panels.
32//! - `images`: each value is a path to an image that can be rendered on a panel
33//!   by referencing its key.
34//! - `consts`: each value is a string that can be substituted into any other
35//!   string by using `%{key}`. This format can also be used to reference
36//!   environment variables using `%{env:KEY}`.
37//!
38//! Other than `images` and `consts`, none of these tables need to be declared
39//! explicitly, as they hold no values of their own. `[bars.example]` is
40//! sufficient to define a bar named `example`. Any values in these top level
41//! tables will be ignored, along with any top level table with a different
42//! name. See <https://toml.io/> for more information.
43//!
44//! Note: types are pretty flexible, and [`config`] will try its best to
45//! figure out what you mean, but if you have issues, make sure that your types
46//! are correct.
47//!
48//! # Example Config
49//! ```toml
50#![doc = include_str!("../examples/config.toml")]
51//! ```
52#![deny(missing_docs)]
53// use log macros
54#![deny(clippy::print_stdout)]
55#![deny(clippy::print_stderr)]
56#![allow(clippy::missing_errors_doc)]
57#![allow(clippy::missing_panics_doc)]
58#![allow(clippy::cast_possible_truncation)]
59#![allow(clippy::cast_possible_wrap)]
60#![allow(clippy::cast_precision_loss)]
61#![allow(clippy::cast_sign_loss)]
62#![allow(clippy::cast_lossless)]
63#![allow(clippy::similar_names)]
64#![allow(clippy::too_many_lines)]
65#![allow(clippy::too_many_arguments)]
66
67/// Configuration options for click/scroll events on panels.
68pub mod actions;
69/// Configuration options for colors and fonts.
70pub mod attrs;
71/// Background configuration options.
72pub mod background;
73/// The bar itself and bar-related utility structs and functions.
74pub mod bar;
75/// Functions to ease a clean shutdown.
76pub mod cleanup;
77/// Common configuration for panels.
78pub mod common;
79mod highlight;
80/// Support for embedding images onto the bar
81pub mod image;
82/// Support for inter-process communication, like that provided by the
83/// `lazybar-msg` crate.
84pub mod ipc;
85/// Macros used internally which may be of use to other developers.
86pub mod macros;
87/// Panels that can be added to the bar. A new panel must implement
88/// [`PanelConfig`].
89pub mod panels;
90/// The parser for the `config.toml` file.
91pub mod parser;
92mod ramp;
93mod utils;
94mod x;
95
96use std::{
97    collections::HashMap,
98    fmt::{Debug, Display},
99    pin::Pin,
100    rc::Rc,
101    sync::{Arc, Mutex},
102};
103
104use anyhow::{Error, Result};
105use async_trait::async_trait;
106use attrs::Attrs;
107use bar::{Bar, Event, Panel, PanelDrawInfo};
108#[cfg(feature = "cursor")]
109use bar::{Cursor, MouseEvent};
110pub use builders::BarConfig;
111use config::{Config, Value};
112pub use csscolorparser::Color;
113pub use glib::markup_escape_text;
114pub use highlight::Highlight;
115use ipc::ChannelEndpoint;
116use lazybar_types::EventResponse;
117pub use ramp::Ramp;
118use tokio_stream::Stream;
119pub use utils::*;
120use x::{create_surface, create_window, set_wm_properties};
121use x11rb::errors::{ConnectionError, ParseError, ReplyError, ReplyOrIdError};
122
123/// A function that can be called repeatedly to draw the panel. The
124/// [`cairo::Context`] will have its current point set to the top left corner of
125/// the panel. The second parameter is the x coordinate of that point relative
126/// to the top left corner of the bar.
127pub type PanelDrawFn = Box<dyn Fn(&cairo::Context, f64) -> Result<()>>;
128/// A function that will be called whenever the panel is shown. Use this to
129/// resume polling, remap a child window, or make any other state changes that
130/// can be cheaply reversed.
131pub type PanelShowFn = Box<dyn Fn() -> Result<()>>;
132/// A function that will be called whenever the panel is hidden. Use this to
133/// pause polling, unmap a child window, or make any other state changes that
134/// can be cheaply reversed.
135pub type PanelHideFn = Box<dyn Fn() -> Result<()>>;
136/// A function that is called for each panel before the bar shuts down.
137pub type PanelShutdownFn = Box<dyn FnOnce()>;
138/// This function receives a [`MouseEvent`] and determines what the cursor name
139/// should be. See [`CursorInfo::Dynamic`][bar::CursorInfo::Dynamic] for more
140/// details.
141#[cfg(feature = "cursor")]
142pub type CursorFn = Box<dyn Fn(MouseEvent) -> Result<Cursor>>;
143/// A stream that produces panel changes when the underlying data source
144/// changes.
145pub type PanelStream = Pin<Box<dyn Stream<Item = Result<PanelDrawInfo>>>>;
146
147/// The channel endpoint associated with a panel.
148pub type PanelEndpoint = Arc<Mutex<ChannelEndpoint<Event, EventResponse>>>;
149
150/// The return type of the [`PanelConfig::run`] function.
151///
152/// The [`PanelStream`] will be used to display the panel on the bar, and the
153/// endpoint, if present, will be used to send IPC events to the panel.
154pub type PanelRunResult = Result<(
155    PanelStream,
156    Option<ipc::ChannelEndpoint<Event, EventResponse>>,
157)>;
158
159/// A cache for the position of clickable text areas.
160pub type IndexCache = Vec<ButtonIndex>;
161
162pub(crate) type IpcStream = Pin<
163    Box<
164        dyn tokio_stream::Stream<
165                Item = std::result::Result<
166                    tokio::net::UnixStream,
167                    std::io::Error,
168                >,
169            >,
170    >,
171>;
172
173/// The trait implemented by all panels. Provides support for parsing a panel
174/// and turning it into a [`PanelStream`].
175#[async_trait(?Send)]
176pub trait PanelConfig: Debug {
177    /// Parses an instance of this type from a subset of the global [`Config`].
178    fn parse(
179        name: &'static str,
180        table: &mut HashMap<String, Value>,
181        global: &Config,
182    ) -> Result<Self>
183    where
184        Self: Sized;
185
186    /// Returns the name of the panel. If the panel supports events, each
187    /// instance must return a unique name.
188    fn props(&self) -> (&'static str, bool);
189
190    /// Performs any necessary setup, then returns a [`PanelStream`]
191    /// representing the provided [`PanelConfig`].
192    ///
193    /// # Errors
194    ///
195    /// If the process of creating a [`PanelStream`] fails.
196    async fn run(
197        self: Box<Self>,
198        cr: Rc<cairo::Context>,
199        global_attrs: Attrs,
200        height: i32,
201    ) -> PanelRunResult;
202}
203
204/// Describes where on the screen the bar should appear.
205#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
206pub enum Position {
207    /// The top of the screen
208    #[default]
209    Top,
210    /// The bottom of the screen
211    Bottom,
212}
213
214/// Describes where on the bar a panel should appear.
215#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
216pub enum Alignment {
217    /// The left of the bar
218    Left,
219    /// The center of the bar
220    Center,
221    /// The right of the bar
222    Right,
223}
224
225impl Display for Alignment {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        match *self {
228            Self::Left => f.write_str("left"),
229            Self::Center => f.write_str("center"),
230            Self::Right => f.write_str("right"),
231        }
232    }
233}
234
235/// Describes the position and size of a clickable button.
236#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
237pub struct ButtonIndex {
238    /// The name of the button.
239    pub name: String,
240    /// The start of the button in bytes. Should be converted to pixel
241    /// coordinates with [`pango::Layout::xy_to_index()`]
242    pub start: usize,
243    /// The length of the button in bytes. Should be converted to pixel
244    /// coordinates with [`pango::Layout::xy_to_index()`]
245    pub length: usize,
246}
247
248/// Describes the minimum width of gaps around panel groups.
249#[derive(Clone, Debug, PartialEq, PartialOrd, Default)]
250pub struct Margins {
251    /// The distance in pixels from the left side of the screen to the start
252    /// of the leftmost panel.
253    pub left: f64,
254    /// The minimum distance in pixels between the last panel with
255    /// [`Alignment::Left`] and the first with [`Alignment::Center`] and
256    /// between the last with [`Alignment::Center`] and the first with
257    /// [`Alignment::Right`].
258    pub internal: f64,
259    /// The distance in pixels between the rightmost panel and the right side
260    /// of the screen. Can be overriden if the panels overflow.
261    pub right: f64,
262}
263
264impl Margins {
265    /// Create a new set of margins.
266    #[must_use]
267    pub const fn new(left: f64, internal: f64, right: f64) -> Self {
268        Self {
269            left,
270            internal,
271            right,
272        }
273    }
274}
275
276async fn handle_error(e: Error, bar: &Bar, ipc: bool) {
277    if let Some(e) = e.downcast_ref::<ConnectionError>() {
278        log::warn!(
279            "X connection error (this probably points to an issue external to \
280             lazybar): {e}"
281        );
282        // close when X server does
283        // this could cause problems, maybe only exit under certain
284        // circumstances?
285        cleanup::exit(Some((bar.name.as_str(), ipc)), true, 0).await;
286    } else if let Some(e) = e.downcast_ref::<ParseError>() {
287        log::warn!("Error parsing data from X server: {e}");
288    } else if let Some(e) = e.downcast_ref::<ReplyError>() {
289        log::warn!("Error produced by X server: {e}");
290    } else if let Some(e) = e.downcast_ref::<ReplyOrIdError>() {
291        log::warn!("Error produced by X server: {e}");
292    } else {
293        log::warn!(
294            "Error produced as a side effect of an X event (expect cryptic \
295             error messages): {e}"
296        );
297    }
298}
299
300/// Builder structs for non-panel items, courtesy of [`derive_builder`]. See
301/// [`panels::builders`] for panel builders.
302pub mod builders {
303    use std::thread;
304
305    use anyhow::Result;
306    use derive_builder::Builder;
307    use futures::executor;
308    use signal_hook::{consts::TERM_SIGNALS, iterator::Signals};
309    use tokio::{
310        runtime::Runtime,
311        sync::mpsc::unbounded_channel,
312        task::{self, JoinSet},
313    };
314    use tokio_stream::{StreamExt, StreamMap};
315
316    #[cfg(feature = "cursor")]
317    use crate::bar::Cursors;
318    use crate::{
319        Alignment, Attrs, Bar, Color, Margins, Panel, PanelConfig, Position,
320        UnixStreamWrapper, cleanup, handle_error, ipc::ChannelEndpoint,
321        x::XStream,
322    };
323
324    /// A set of options for a bar.
325    ///
326    /// See [`parser::parse`][crate::parser::parse] for configuration details.
327    #[derive(Builder, Debug)]
328    #[builder_struct_attr(allow(missing_docs))]
329    #[builder_impl_attr(allow(missing_docs))]
330    #[builder(pattern = "owned")]
331    pub struct BarConfig {
332        /// The bar name to look for in the config file
333        pub name: String,
334        left: Vec<Box<dyn PanelConfig>>,
335        center: Vec<Box<dyn PanelConfig>>,
336        right: Vec<Box<dyn PanelConfig>>,
337        /// Whether the bar should be rendered at the top or bottom of the
338        /// screen
339        pub position: Position,
340        /// In pixels
341        pub height: u16,
342        /// Whether the bar can be transparent. The background color still
343        /// applies!
344        pub transparent: bool,
345        /// The background color. Supports transparency if `transparent` is
346        /// true.
347        pub bg: Color,
348        /// The minimum gaps between the edges of the screen and panel
349        /// sections. See [`Margins`] for details.
350        pub margins: Margins,
351        /// The default attributes of panels on the bar. See [`Attrs`] for
352        /// details.
353        pub attrs: Attrs,
354        /// Whether to reverse the scrolling direction for panel events.
355        pub reverse_scroll: bool,
356        /// Whether inter-process communication (via Unix socket) is enabled.
357        /// See [`crate::ipc`] for details.
358        pub ipc: bool,
359        /// Which monitor to display the bar on. Defaults to the primary
360        /// monitor.
361        pub monitor: Option<String>,
362        /// The X11 cursor names associated with the bar.
363        #[cfg(feature = "cursor")]
364        pub cursors: Cursors,
365    }
366
367    impl BarConfig {
368        /// Provides access to the [`BarConfigBuilder`] without an
369        /// additional import.
370        pub fn builder() -> BarConfigBuilder {
371            BarConfigBuilder::default()
372        }
373
374        /// Add a panel to the bar with a given [`Alignment`]. It will appear to
375        /// the right of all existing panels with the same alignment.
376        pub fn add_panel(
377            &mut self,
378            panel: Box<dyn PanelConfig>,
379            alignment: Alignment,
380        ) {
381            match alignment {
382                Alignment::Left => self.left.push(panel),
383                Alignment::Center => self.center.push(panel),
384                Alignment::Right => self.right.push(panel),
385            };
386        }
387
388        /// Turn the provided [`BarConfig`] into a [`Bar`] and start the main
389        /// event loop.
390        ///
391        /// # Errors
392        ///
393        /// In the case of unrecoverable runtime errors.
394        pub fn run(self) -> Result<()> {
395            log::info!("Starting bar {}", self.name);
396            let rt = Runtime::new()?;
397            let local = task::LocalSet::new();
398            local.block_on(&rt, self.run_inner())?;
399            Ok(())
400        }
401
402        #[allow(clippy::future_not_send)]
403        async fn run_inner(self) -> Result<()> {
404            let (mut bar, mut ipc_stream) = Bar::new(
405                self.name.as_str(),
406                self.position,
407                self.height,
408                self.transparent,
409                self.bg,
410                self.margins,
411                self.reverse_scroll,
412                self.ipc,
413                self.monitor,
414                #[cfg(feature = "cursor")]
415                self.cursors,
416            )?;
417            log::debug!("bar created");
418
419            let mut joinset = JoinSet::new();
420
421            let mut left_stream = StreamMap::with_capacity(self.left.len());
422            let mut left_panels = Vec::new();
423            for (idx, panel) in self.left.into_iter().enumerate() {
424                left_panels.push(None);
425                let cr = bar.cr.clone();
426                let attrs = self.attrs.clone();
427                joinset.spawn_local(async move {
428                    (
429                        Alignment::Left,
430                        idx,
431                        panel.props(),
432                        panel
433                            .run(
434                                cr.clone(),
435                                attrs.clone(),
436                                i32::from(self.height),
437                            )
438                            .await,
439                    )
440                });
441            }
442
443            let mut center_stream = StreamMap::with_capacity(self.center.len());
444            let mut center_panels = Vec::new();
445            for (idx, panel) in self.center.into_iter().enumerate() {
446                center_panels.push(None);
447                let cr = bar.cr.clone();
448                let attrs = self.attrs.clone();
449                joinset.spawn_local(async move {
450                    (
451                        Alignment::Center,
452                        idx,
453                        panel.props(),
454                        panel
455                            .run(
456                                cr.clone(),
457                                attrs.clone(),
458                                i32::from(self.height),
459                            )
460                            .await,
461                    )
462                });
463            }
464
465            let mut right_stream = StreamMap::with_capacity(self.right.len());
466            let mut right_panels = Vec::new();
467            for (idx, panel) in self.right.into_iter().enumerate() {
468                right_panels.push(None);
469                let cr = bar.cr.clone();
470                let attrs = self.attrs.clone();
471                joinset.spawn_local(async move {
472                    (
473                        Alignment::Right,
474                        idx,
475                        panel.props(),
476                        panel
477                            .run(
478                                cr.clone(),
479                                attrs.clone(),
480                                i32::from(self.height),
481                            )
482                            .await,
483                    )
484                });
485            }
486
487            while !joinset.is_empty() {
488                match joinset.join_next().await {
489                    Some(Ok((
490                        alignment,
491                        idx,
492                        (name, visible),
493                        Ok((stream, sender)),
494                    ))) => match alignment {
495                        Alignment::Left => {
496                            left_panels[idx] =
497                                Some(Panel::new(None, name, sender, visible));
498                            left_stream.insert(idx, stream);
499                        }
500                        Alignment::Center => {
501                            center_panels[idx] =
502                                Some(Panel::new(None, name, sender, visible));
503                            center_stream.insert(idx, stream);
504                        }
505                        Alignment::Right => {
506                            right_panels[idx] =
507                                Some(Panel::new(None, name, sender, visible));
508                            right_stream.insert(idx, stream);
509                        }
510                    },
511                    Some(Ok((alignment, idx, (name, _), Err(e)))) => {
512                        log::error!(
513                            "Error encountered while starting {name} \
514                             ({alignment} panel at index {idx}): {e}"
515                        );
516                    }
517                    Some(Err(e)) => {
518                        log::warn!(
519                            "Join error encountered while starting panels: {e}"
520                        );
521                    }
522                    None => unreachable!(),
523                }
524            }
525
526            bar.left_panels = left_panels.into_iter().flatten().collect();
527            bar.center_panels = center_panels.into_iter().flatten().collect();
528            bar.right_panels = right_panels.into_iter().flatten().collect();
529
530            bar.streams.insert(Alignment::Left, left_stream);
531            log::debug!("left panels running");
532
533            bar.streams.insert(Alignment::Center, center_stream);
534            log::debug!("center panels running");
535
536            bar.streams.insert(Alignment::Right, right_stream);
537            log::debug!("right panels running");
538
539            let mut x_stream = XStream::new(bar.conn.clone());
540
541            let mut signals = Signals::new(TERM_SIGNALS)?;
542            let name = bar.name.clone();
543
544            let (send1, recv2) = unbounded_channel();
545            let (send2, recv1) = unbounded_channel();
546            let mut endpoint1 = ChannelEndpoint::new(send1, recv1);
547            let endpoint2 = ChannelEndpoint::new(send2, recv2);
548            *cleanup::ENDPOINT.lock().await = Some(endpoint2);
549            thread::spawn(move || {
550                loop {
551                    if let Some(signal) = signals.wait().next() {
552                        log::info!("Received signal {signal} - shutting down");
553                        if let Ok(rt) = Runtime::new() {
554                            rt.block_on(async {
555                                cleanup::exit(
556                                    Some((name.as_str(), self.ipc)),
557                                    true,
558                                    0,
559                                )
560                                .await;
561                            });
562                        } else {
563                            executor::block_on(cleanup::exit(
564                                Some((name.as_str(), self.ipc)),
565                                false,
566                                0,
567                            ));
568                        }
569                    }
570                }
571            });
572            log::debug!("Set up signal listener");
573
574            let mut ipc_set = JoinSet::<Result<()>>::new();
575
576            let mut cleanup = task::spawn_local(cleanup::cleanup());
577            let mut cleanup_done = false;
578
579            task::spawn_local(async move { loop {
580                tokio::select! {
581                    Some(Ok(event)) = x_stream.next() => {
582                        log::trace!("X event: {event:?}");
583                        if let Err(e) = bar.process_event(&event) {
584                            handle_error(e, &bar, self.ipc).await;
585                        }
586                    },
587                    Some((alignment, result)) = bar.streams.next() => {
588                        log::debug!("Received event from {alignment} panel at index {}", result.0);
589                        match result {
590                            (idx, Ok(draw_info)) => if let Err(e) = bar.update_panel(alignment, idx, draw_info) {
591                                log::warn!("Error updating {alignment} panel at index {idx}");
592                                handle_error(e, &bar, self.ipc).await;
593                            }
594                            (idx, Err(e)) => {
595                                log::warn!("Error produced by {alignment} panel at index {idx:?}");
596                                handle_error(e, &bar, self.ipc).await;
597                            }
598                        }
599                    },
600                    Some(Ok(stream)) = ipc_stream.next(), if bar.ipc => {
601                        log::debug!("Received new ipc connection");
602
603                        let (local_send, mut local_recv) = unbounded_channel();
604                        let (ipc_send, ipc_recv) = unbounded_channel();
605
606                        let wrapper = UnixStreamWrapper::new(stream, ChannelEndpoint::new(local_send, ipc_recv));
607
608                        let _handle = task::spawn(wrapper.run());
609                        log::trace!("wrapper running");
610
611                        let message = local_recv.recv().await;
612                        log::trace!("message received: {message:?}");
613
614                        if let Some(message) = message {
615                            match bar.send_message(message.as_str(), &mut ipc_set, ipc_send) {
616                                Ok(true) => {
617                                    task::spawn_local(cleanup::exit(Some((bar.name.clone().leak(), self.ipc)), true, 0));
618                                }
619                                Err(e) => log::warn!("Sending message {message} generated an error: {e}"),
620                                _ => {}
621                            }
622                        }
623                    }
624                    // maybe not strictly necessary, but ensures that the ipc futures get polled
625                    Some(_) = ipc_set.join_next() => {
626                        log::debug!("ipc future completed");
627                    }
628                    Some(()) = endpoint1.recv.recv() => {
629                        bar.shutdown();
630                        let _ = endpoint1.send.send(());
631                        // this message will never arrive, but it avoids a race condition with the
632                        // break statement.
633                        let _ = endpoint1.recv.recv().await;
634                        // this is necessary to satisfy the borrow checker, even though it will
635                        // never run.
636                        break;
637                    }
638                    res = &mut cleanup, if !cleanup_done => {
639                        match res {
640                            Ok(Ok(())) => {
641                                log::info!("IPC dir cleanup finished");
642                            }
643                            Ok(Err(e)) => {
644                                log::warn!("IPC dir cleanup failed: {e}");
645                            }
646                            Err(e) => {
647                                log::warn!("Failed to join cleanup task: {e}");
648                            }
649                        }
650                        cleanup_done = true;
651                    }
652                }
653            } }).await?;
654
655            Ok(())
656        }
657    }
658}