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}