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}