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/// Use XDG spec for directories
68pub static PROJ_DIRS: LazyLock<ProjectDirs> =
69    LazyLock::new(|| ProjectDirs::from("com", "qelxiros", "lazybar").unwrap());
70
71/// Configuration options for click/scroll events on panels.
72pub mod actions;
73/// Configuration options for colors and fonts.
74pub mod attrs;
75/// Background configuration options.
76pub mod background;
77/// The bar itself and bar-related utility structs and functions.
78pub mod bar;
79/// Functions to ease a clean shutdown.
80pub mod cleanup;
81/// Common configuration for panels.
82pub mod common;
83mod highlight;
84/// Support for embedding images onto the bar
85pub mod image;
86/// Support for inter-process communication, like that provided by the
87/// `lazybar-msg` crate.
88pub mod ipc;
89/// Macros used internally which may be of use to other developers.
90pub mod macros;
91/// Panels that can be added to the bar. A new panel must implement
92/// [`PanelConfig`].
93pub mod panels;
94/// The parser for the `config.toml` file.
95pub mod parser;
96mod ramp;
97mod utils;
98mod x;
99
100use std::{
101    collections::HashMap,
102    fmt::{Debug, Display},
103    pin::Pin,
104    rc::Rc,
105    sync::{Arc, LazyLock, Mutex},
106};
107
108use anyhow::{Error, Result};
109use async_trait::async_trait;
110use attrs::Attrs;
111use bar::{Bar, Event, Panel, PanelDrawInfo};
112#[cfg(feature = "cursor")]
113use bar::{Cursor, MouseEvent};
114pub use builders::BarConfig;
115use config::{Config, Value};
116pub use csscolorparser::Color;
117use directories::ProjectDirs;
118pub use glib::markup_escape_text;
119pub use highlight::Highlight;
120use ipc::ChannelEndpoint;
121use lazybar_types::EventResponse;
122pub use ramp::Ramp;
123use tokio_stream::Stream;
124pub use utils::*;
125use x::{create_surface, create_window, set_wm_properties};
126use x11rb::errors::{ConnectionError, ParseError, ReplyError, ReplyOrIdError};
127
128/// A function that can be called repeatedly to draw the panel. The
129/// [`cairo::Context`] will have its current point set to the top left corner of
130/// the panel. The second parameter is the x coordinate of that point relative
131/// to the top left corner of the bar.
132pub type PanelDrawFn = Box<dyn Fn(&cairo::Context, f64) -> Result<()>>;
133/// A function that will be called whenever the panel is shown. Use this to
134/// resume polling, remap a child window, or make any other state changes that
135/// can be cheaply reversed.
136pub type PanelShowFn = Box<dyn Fn() -> Result<()>>;
137/// A function that will be called whenever the panel is hidden. Use this to
138/// pause polling, unmap a child window, or make any other state changes that
139/// can be cheaply reversed.
140pub type PanelHideFn = Box<dyn Fn() -> Result<()>>;
141/// A function that is called for each panel before the bar shuts down.
142pub type PanelShutdownFn = Box<dyn FnOnce()>;
143/// This function receives a [`MouseEvent`] and determines what the cursor name
144/// should be. See [`CursorInfo::Dynamic`][bar::CursorInfo::Dynamic] for more
145/// details.
146#[cfg(feature = "cursor")]
147pub type CursorFn = Box<dyn Fn(MouseEvent) -> Result<Cursor>>;
148/// A stream that produces panel changes when the underlying data source
149/// changes.
150pub type PanelStream = Pin<Box<dyn Stream<Item = Result<PanelDrawInfo>>>>;
151
152/// The channel endpoint associated with a panel.
153pub type PanelEndpoint = Arc<Mutex<ChannelEndpoint<Event, EventResponse>>>;
154
155/// The return type of the [`PanelConfig::run`] function.
156///
157/// The [`PanelStream`] will be used to display the panel on the bar, and the
158/// endpoint, if present, will be used to send IPC events to the panel.
159pub type PanelRunResult = Result<(
160    PanelStream,
161    Option<ipc::ChannelEndpoint<Event, EventResponse>>,
162)>;
163
164/// A cache for the position of clickable text areas.
165pub type IndexCache = Vec<ButtonIndex>;
166
167pub(crate) type IpcStream = Pin<
168    Box<
169        dyn tokio_stream::Stream<
170            Item = std::result::Result<tokio::net::UnixStream, std::io::Error>,
171        >,
172    >,
173>;
174
175/// The trait implemented by all panels. Provides support for parsing a panel
176/// and turning it into a [`PanelStream`].
177#[async_trait(?Send)]
178pub trait PanelConfig: Debug {
179    /// Parses an instance of this type from a subset of the global [`Config`].
180    fn parse(
181        name: &'static str,
182        table: &mut HashMap<String, Value>,
183        global: &Config,
184    ) -> Result<Self>
185    where
186        Self: Sized;
187
188    /// Returns the name of the panel. If the panel supports events, each
189    /// instance must return a unique name.
190    fn props(&self) -> (&'static str, bool);
191
192    /// Performs any necessary setup, then returns a [`PanelStream`]
193    /// representing the provided [`PanelConfig`].
194    ///
195    /// # Errors
196    ///
197    /// If the process of creating a [`PanelStream`] fails.
198    async fn run(
199        self: Box<Self>,
200        cr: Rc<cairo::Context>,
201        global_attrs: Attrs,
202        height: i32,
203    ) -> PanelRunResult;
204}
205
206/// Describes where on the screen the bar should appear.
207#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
208pub enum Position {
209    /// The top of the screen
210    #[default]
211    Top,
212    /// The bottom of the screen
213    Bottom,
214}
215
216/// Describes where on the bar a panel should appear.
217#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
218pub enum Alignment {
219    /// The left of the bar
220    Left,
221    /// The center of the bar
222    Center,
223    /// The right of the bar
224    Right,
225}
226
227impl Display for Alignment {
228    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229        match *self {
230            Self::Left => f.write_str("left"),
231            Self::Center => f.write_str("center"),
232            Self::Right => f.write_str("right"),
233        }
234    }
235}
236
237/// Describes the position and size of a clickable button.
238#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
239pub struct ButtonIndex {
240    /// The name of the button.
241    pub name: String,
242    /// The start of the button in bytes. Should be converted to pixel
243    /// coordinates with [`pango::Layout::xy_to_index()`]
244    pub start: usize,
245    /// The length of the button in bytes. Should be converted to pixel
246    /// coordinates with [`pango::Layout::xy_to_index()`]
247    pub length: usize,
248}
249
250/// Describes the minimum width of gaps around panel groups.
251#[derive(Clone, Debug, PartialEq, PartialOrd, Default)]
252pub struct Margins {
253    /// The distance in pixels from the left side of the screen to the start
254    /// of the leftmost panel.
255    pub left: f64,
256    /// The minimum distance in pixels between the last panel with
257    /// [`Alignment::Left`] and the first with [`Alignment::Center`] and
258    /// between the last with [`Alignment::Center`] and the first with
259    /// [`Alignment::Right`].
260    pub internal: f64,
261    /// The distance in pixels between the rightmost panel and the right side
262    /// of the screen. Can be overriden if the panels overflow.
263    pub right: f64,
264}
265
266impl Margins {
267    /// Create a new set of margins.
268    #[must_use]
269    pub const fn new(left: f64, internal: f64, right: f64) -> Self {
270        Self {
271            left,
272            internal,
273            right,
274        }
275    }
276}
277
278async fn handle_error(e: Error, bar: &Bar, ipc: bool) {
279    if let Some(e) = e.downcast_ref::<ConnectionError>() {
280        log::warn!(
281            "X connection error (this probably points to an issue external to \
282             lazybar): {e}"
283        );
284        // close when X server does
285        // this could cause problems, maybe only exit under certain
286        // circumstances?
287        cleanup::exit(Some((bar.name.as_str(), ipc)), true, 0).await;
288    } else if let Some(e) = e.downcast_ref::<ParseError>() {
289        log::warn!("Error parsing data from X server: {e}");
290    } else if let Some(e) = e.downcast_ref::<ReplyError>() {
291        log::warn!("Error produced by X server: {e}");
292    } else if let Some(e) = e.downcast_ref::<ReplyOrIdError>() {
293        log::warn!("Error produced by X server: {e}");
294    } else {
295        log::warn!(
296            "Error produced as a side effect of an X event (expect cryptic \
297             error messages): {e}"
298        );
299    }
300}
301
302/// Builder structs for non-panel items, courtesy of [`derive_builder`]. See
303/// [`panels::builders`] for panel builders.
304pub mod builders {
305    use std::thread;
306
307    use anyhow::Result;
308    use derive_builder::Builder;
309    use futures::executor;
310    use signal_hook::{consts::TERM_SIGNALS, iterator::Signals};
311    use tokio::{
312        runtime::Runtime,
313        sync::mpsc::unbounded_channel,
314        task::{self, JoinSet},
315    };
316    use tokio_stream::{StreamExt, StreamMap};
317
318    #[cfg(feature = "cursor")]
319    use crate::bar::Cursors;
320    use crate::{
321        cleanup, handle_error, ipc::ChannelEndpoint, x::XStream, Alignment,
322        Attrs, Bar, Color, Margins, Panel, PanelConfig, Position,
323        UnixStreamWrapper,
324    };
325
326    /// A set of options for a bar.
327    ///
328    /// See [`parser::parse`][crate::parser::parse] for configuration details.
329    #[derive(Builder, Debug)]
330    #[builder_struct_attr(allow(missing_docs))]
331    #[builder_impl_attr(allow(missing_docs))]
332    #[builder(pattern = "owned")]
333    pub struct BarConfig {
334        /// The bar name to look for in the config file
335        pub name: String,
336        left: Vec<Box<dyn PanelConfig>>,
337        center: Vec<Box<dyn PanelConfig>>,
338        right: Vec<Box<dyn PanelConfig>>,
339        /// Whether the bar should be rendered at the top or bottom of the
340        /// screen
341        pub position: Position,
342        /// In pixels
343        pub height: u16,
344        /// Whether the bar can be transparent. The background color still
345        /// applies!
346        pub transparent: bool,
347        /// The background color. Supports transparency if `transparent` is
348        /// true.
349        pub bg: Color,
350        /// The minimum gaps between the edges of the screen and panel
351        /// sections. See [`Margins`] for details.
352        pub margins: Margins,
353        /// The default attributes of panels on the bar. See [`Attrs`] for
354        /// details.
355        pub attrs: Attrs,
356        /// Whether to reverse the scrolling direction for panel events.
357        pub reverse_scroll: bool,
358        /// Whether inter-process communication (via Unix socket) is enabled.
359        /// See [`crate::ipc`] for details.
360        pub ipc: bool,
361        /// Which monitor to display the bar on. Defaults to the primary
362        /// monitor.
363        pub monitor: Option<String>,
364        /// The X11 cursor names associated with the bar.
365        #[cfg(feature = "cursor")]
366        pub cursors: Cursors,
367    }
368
369    impl BarConfig {
370        /// Provides access to the [`BarConfigBuilder`] without an
371        /// additional import.
372        pub fn builder() -> BarConfigBuilder {
373            BarConfigBuilder::default()
374        }
375
376        /// Add a panel to the bar with a given [`Alignment`]. It will appear to
377        /// the right of all existing panels with the same alignment.
378        pub fn add_panel(
379            &mut self,
380            panel: Box<dyn PanelConfig>,
381            alignment: Alignment,
382        ) {
383            match alignment {
384                Alignment::Left => self.left.push(panel),
385                Alignment::Center => self.center.push(panel),
386                Alignment::Right => self.right.push(panel),
387            };
388        }
389
390        /// Turn the provided [`BarConfig`] into a [`Bar`] and start the main
391        /// event loop.
392        ///
393        /// # Errors
394        ///
395        /// In the case of unrecoverable runtime errors.
396        pub fn run(self) -> Result<()> {
397            log::info!("Starting bar {}", self.name);
398            let rt = Runtime::new()?;
399            let local = task::LocalSet::new();
400            local.block_on(&rt, self.run_inner())?;
401            Ok(())
402        }
403
404        #[allow(clippy::future_not_send)]
405        async fn run_inner(self) -> Result<()> {
406            let (mut bar, mut ipc_stream) = Bar::new(
407                self.name.as_str(),
408                self.position,
409                self.height,
410                self.transparent,
411                self.bg,
412                self.margins,
413                self.reverse_scroll,
414                self.ipc,
415                self.monitor,
416                #[cfg(feature = "cursor")]
417                self.cursors,
418            )?;
419            log::debug!("bar created");
420
421            let mut joinset = JoinSet::new();
422
423            let mut left_stream = StreamMap::with_capacity(self.left.len());
424            let mut left_panels = Vec::new();
425            for (idx, panel) in self.left.into_iter().enumerate() {
426                left_panels.push(None);
427                let cr = bar.cr.clone();
428                let attrs = self.attrs.clone();
429                joinset.spawn_local(async move {
430                    (
431                        Alignment::Left,
432                        idx,
433                        panel.props(),
434                        panel
435                            .run(
436                                cr.clone(),
437                                attrs.clone(),
438                                i32::from(self.height),
439                            )
440                            .await,
441                    )
442                });
443            }
444
445            let mut center_stream = StreamMap::with_capacity(self.center.len());
446            let mut center_panels = Vec::new();
447            for (idx, panel) in self.center.into_iter().enumerate() {
448                center_panels.push(None);
449                let cr = bar.cr.clone();
450                let attrs = self.attrs.clone();
451                joinset.spawn_local(async move {
452                    (
453                        Alignment::Center,
454                        idx,
455                        panel.props(),
456                        panel
457                            .run(
458                                cr.clone(),
459                                attrs.clone(),
460                                i32::from(self.height),
461                            )
462                            .await,
463                    )
464                });
465            }
466
467            let mut right_stream = StreamMap::with_capacity(self.right.len());
468            let mut right_panels = Vec::new();
469            for (idx, panel) in self.right.into_iter().enumerate() {
470                right_panels.push(None);
471                let cr = bar.cr.clone();
472                let attrs = self.attrs.clone();
473                joinset.spawn_local(async move {
474                    (
475                        Alignment::Right,
476                        idx,
477                        panel.props(),
478                        panel
479                            .run(
480                                cr.clone(),
481                                attrs.clone(),
482                                i32::from(self.height),
483                            )
484                            .await,
485                    )
486                });
487            }
488
489            while !joinset.is_empty() {
490                match joinset.join_next().await {
491                    Some(Ok((
492                        alignment,
493                        idx,
494                        (name, visible),
495                        Ok((stream, sender)),
496                    ))) => match alignment {
497                        Alignment::Left => {
498                            left_panels[idx] =
499                                Some(Panel::new(None, name, sender, visible));
500                            left_stream.insert(idx, stream);
501                        }
502                        Alignment::Center => {
503                            center_panels[idx] =
504                                Some(Panel::new(None, name, sender, visible));
505                            center_stream.insert(idx, stream);
506                        }
507                        Alignment::Right => {
508                            right_panels[idx] =
509                                Some(Panel::new(None, name, sender, visible));
510                            right_stream.insert(idx, stream);
511                        }
512                    },
513                    Some(Ok((alignment, idx, (name, _), Err(e)))) => {
514                        log::error!(
515                            "Error encountered while starting {name} \
516                             ({alignment} panel at index {idx}): {e}"
517                        );
518                    }
519                    Some(Err(e)) => {
520                        log::warn!(
521                            "Join error encountered while starting panels: {e}"
522                        );
523                    }
524                    None => unreachable!(),
525                }
526            }
527
528            bar.left_panels = left_panels.into_iter().flatten().collect();
529            bar.center_panels = center_panels.into_iter().flatten().collect();
530            bar.right_panels = right_panels.into_iter().flatten().collect();
531
532            bar.streams.insert(Alignment::Left, left_stream);
533            log::debug!("left panels running");
534
535            bar.streams.insert(Alignment::Center, center_stream);
536            log::debug!("center panels running");
537
538            bar.streams.insert(Alignment::Right, right_stream);
539            log::debug!("right panels running");
540
541            let mut x_stream = XStream::new(bar.conn.clone());
542
543            let mut signals = Signals::new(TERM_SIGNALS)?;
544            let name = bar.name.clone();
545
546            let (send1, recv2) = unbounded_channel();
547            let (send2, recv1) = unbounded_channel();
548            let mut endpoint1 = ChannelEndpoint::new(send1, recv1);
549            let endpoint2 = ChannelEndpoint::new(send2, recv2);
550            *cleanup::ENDPOINT.lock().await = Some(endpoint2);
551            thread::spawn(move || loop {
552                if let Some(signal) = signals.wait().next() {
553                    log::info!("Received signal {signal} - shutting down");
554                    if let Ok(rt) = Runtime::new() {
555                        rt.block_on(async {
556                            cleanup::exit(
557                                Some((name.as_str(), self.ipc)),
558                                true,
559                                0,
560                            )
561                            .await;
562                        });
563                    } else {
564                        executor::block_on(cleanup::exit(
565                            Some((name.as_str(), self.ipc)),
566                            false,
567                            0,
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}