#![doc = include_str!("../examples/config.toml")]
#![deny(missing_docs)]
#![deny(clippy::print_stdout)]
#![deny(clippy::print_stderr)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::missing_panics_doc)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::cast_precision_loss)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_lossless)]
#![allow(clippy::similar_names)]
#![allow(clippy::too_many_lines)]
#![allow(clippy::too_many_arguments)]
pub mod actions;
pub mod attrs;
pub mod background;
pub mod bar;
pub mod cleanup;
pub mod common;
mod highlight;
pub mod image;
pub mod ipc;
pub mod macros;
pub mod panels;
pub mod parser;
mod ramp;
mod utils;
mod x;
use std::{
collections::HashMap,
fmt::{Debug, Display},
pin::Pin,
rc::Rc,
sync::{Arc, Mutex},
};
use anyhow::{Error, Result};
use async_trait::async_trait;
use attrs::Attrs;
use bar::{Bar, Event, Panel, PanelDrawInfo};
#[cfg(feature = "cursor")]
use bar::{Cursor, MouseEvent};
pub use builders::BarConfig;
use config::{Config, Value};
pub use csscolorparser::Color;
pub use glib::markup_escape_text;
pub use highlight::Highlight;
use ipc::ChannelEndpoint;
use lazybar_types::EventResponse;
pub use ramp::Ramp;
use tokio_stream::Stream;
pub use utils::*;
use x::{create_surface, create_window, set_wm_properties};
use x11rb::errors::{ConnectionError, ParseError, ReplyError, ReplyOrIdError};
pub type PanelDrawFn = Box<dyn Fn(&cairo::Context, f64) -> Result<()>>;
pub type PanelShowFn = Box<dyn Fn() -> Result<()>>;
pub type PanelHideFn = Box<dyn Fn() -> Result<()>>;
pub type PanelShutdownFn = Box<dyn FnOnce()>;
#[cfg(feature = "cursor")]
pub type CursorFn = Box<dyn Fn(MouseEvent) -> Result<Cursor>>;
pub type PanelStream = Pin<Box<dyn Stream<Item = Result<PanelDrawInfo>>>>;
pub type PanelEndpoint = Arc<Mutex<ChannelEndpoint<Event, EventResponse>>>;
pub type PanelRunResult = Result<(
PanelStream,
Option<ipc::ChannelEndpoint<Event, EventResponse>>,
)>;
pub type IndexCache = Vec<ButtonIndex>;
pub(crate) type IpcStream = Pin<
Box<
dyn tokio_stream::Stream<
Item = std::result::Result<
tokio::net::UnixStream,
std::io::Error,
>,
>,
>,
>;
#[async_trait(?Send)]
pub trait PanelConfig: Debug {
fn parse(
name: &'static str,
table: &mut HashMap<String, Value>,
global: &Config,
) -> Result<Self>
where
Self: Sized;
fn props(&self) -> (&'static str, bool);
async fn run(
self: Box<Self>,
cr: Rc<cairo::Context>,
global_attrs: Attrs,
height: i32,
) -> PanelRunResult;
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
pub enum Position {
#[default]
Top,
Bottom,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub enum Alignment {
Left,
Center,
Right,
}
impl Display for Alignment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
Self::Left => f.write_str("left"),
Self::Center => f.write_str("center"),
Self::Right => f.write_str("right"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct ButtonIndex {
pub name: String,
pub start: usize,
pub length: usize,
}
#[derive(Clone, Debug, PartialEq, PartialOrd, Default)]
pub struct Margins {
pub left: f64,
pub internal: f64,
pub right: f64,
}
impl Margins {
#[must_use]
pub const fn new(left: f64, internal: f64, right: f64) -> Self {
Self {
left,
internal,
right,
}
}
}
async fn handle_error(e: Error, bar: &Bar, ipc: bool) {
if let Some(e) = e.downcast_ref::<ConnectionError>() {
log::warn!(
"X connection error (this probably points to an issue external to \
lazybar): {e}"
);
cleanup::exit(Some((bar.name.as_str(), ipc)), true, 0).await;
} else if let Some(e) = e.downcast_ref::<ParseError>() {
log::warn!("Error parsing data from X server: {e}");
} else if let Some(e) = e.downcast_ref::<ReplyError>() {
log::warn!("Error produced by X server: {e}");
} else if let Some(e) = e.downcast_ref::<ReplyOrIdError>() {
log::warn!("Error produced by X server: {e}");
} else {
log::warn!(
"Error produced as a side effect of an X event (expect cryptic \
error messages): {e}"
);
}
}
pub mod builders {
use std::thread;
use anyhow::Result;
use derive_builder::Builder;
use futures::executor;
use signal_hook::{consts::TERM_SIGNALS, iterator::Signals};
use tokio::{
runtime::Runtime,
sync::mpsc::unbounded_channel,
task::{self, JoinSet},
};
use tokio_stream::{StreamExt, StreamMap};
#[cfg(feature = "cursor")]
use crate::bar::Cursors;
use crate::{
Alignment, Attrs, Bar, Color, Margins, Panel, PanelConfig, Position,
UnixStreamWrapper, cleanup, handle_error, ipc::ChannelEndpoint,
x::XStream,
};
#[derive(Builder, Debug)]
#[builder_struct_attr(allow(missing_docs))]
#[builder_impl_attr(allow(missing_docs))]
#[builder(pattern = "owned")]
pub struct BarConfig {
pub name: String,
left: Vec<Box<dyn PanelConfig>>,
center: Vec<Box<dyn PanelConfig>>,
right: Vec<Box<dyn PanelConfig>>,
pub position: Position,
pub height: u16,
pub transparent: bool,
pub bg: Color,
pub margins: Margins,
pub attrs: Attrs,
pub reverse_scroll: bool,
pub ipc: bool,
pub monitor: Option<String>,
#[cfg(feature = "cursor")]
pub cursors: Cursors,
}
impl BarConfig {
pub fn builder() -> BarConfigBuilder {
BarConfigBuilder::default()
}
pub fn add_panel(
&mut self,
panel: Box<dyn PanelConfig>,
alignment: Alignment,
) {
match alignment {
Alignment::Left => self.left.push(panel),
Alignment::Center => self.center.push(panel),
Alignment::Right => self.right.push(panel),
};
}
pub fn run(self) -> Result<()> {
log::info!("Starting bar {}", self.name);
let rt = Runtime::new()?;
let local = task::LocalSet::new();
local.block_on(&rt, self.run_inner())?;
Ok(())
}
#[allow(clippy::future_not_send)]
async fn run_inner(self) -> Result<()> {
let (mut bar, mut ipc_stream) = Bar::new(
self.name.as_str(),
self.position,
self.height,
self.transparent,
self.bg,
self.margins,
self.reverse_scroll,
self.ipc,
self.monitor,
#[cfg(feature = "cursor")]
self.cursors,
)?;
log::debug!("bar created");
let mut joinset = JoinSet::new();
let mut left_stream = StreamMap::with_capacity(self.left.len());
let mut left_panels = Vec::new();
for (idx, panel) in self.left.into_iter().enumerate() {
left_panels.push(None);
let cr = bar.cr.clone();
let attrs = self.attrs.clone();
joinset.spawn_local(async move {
(
Alignment::Left,
idx,
panel.props(),
panel
.run(
cr.clone(),
attrs.clone(),
i32::from(self.height),
)
.await,
)
});
}
let mut center_stream = StreamMap::with_capacity(self.center.len());
let mut center_panels = Vec::new();
for (idx, panel) in self.center.into_iter().enumerate() {
center_panels.push(None);
let cr = bar.cr.clone();
let attrs = self.attrs.clone();
joinset.spawn_local(async move {
(
Alignment::Center,
idx,
panel.props(),
panel
.run(
cr.clone(),
attrs.clone(),
i32::from(self.height),
)
.await,
)
});
}
let mut right_stream = StreamMap::with_capacity(self.right.len());
let mut right_panels = Vec::new();
for (idx, panel) in self.right.into_iter().enumerate() {
right_panels.push(None);
let cr = bar.cr.clone();
let attrs = self.attrs.clone();
joinset.spawn_local(async move {
(
Alignment::Right,
idx,
panel.props(),
panel
.run(
cr.clone(),
attrs.clone(),
i32::from(self.height),
)
.await,
)
});
}
while !joinset.is_empty() {
match joinset.join_next().await {
Some(Ok((
alignment,
idx,
(name, visible),
Ok((stream, sender)),
))) => match alignment {
Alignment::Left => {
left_panels[idx] =
Some(Panel::new(None, name, sender, visible));
left_stream.insert(idx, stream);
}
Alignment::Center => {
center_panels[idx] =
Some(Panel::new(None, name, sender, visible));
center_stream.insert(idx, stream);
}
Alignment::Right => {
right_panels[idx] =
Some(Panel::new(None, name, sender, visible));
right_stream.insert(idx, stream);
}
},
Some(Ok((alignment, idx, (name, _), Err(e)))) => {
log::error!(
"Error encountered while starting {name} \
({alignment} panel at index {idx}): {e}"
);
}
Some(Err(e)) => {
log::warn!(
"Join error encountered while starting panels: {e}"
);
}
None => unreachable!(),
}
}
bar.left_panels = left_panels.into_iter().flatten().collect();
bar.center_panels = center_panels.into_iter().flatten().collect();
bar.right_panels = right_panels.into_iter().flatten().collect();
bar.streams.insert(Alignment::Left, left_stream);
log::debug!("left panels running");
bar.streams.insert(Alignment::Center, center_stream);
log::debug!("center panels running");
bar.streams.insert(Alignment::Right, right_stream);
log::debug!("right panels running");
let mut x_stream = XStream::new(bar.conn.clone());
let mut signals = Signals::new(TERM_SIGNALS)?;
let name = bar.name.clone();
let (send1, recv2) = unbounded_channel();
let (send2, recv1) = unbounded_channel();
let mut endpoint1 = ChannelEndpoint::new(send1, recv1);
let endpoint2 = ChannelEndpoint::new(send2, recv2);
*cleanup::ENDPOINT.lock().await = Some(endpoint2);
thread::spawn(move || {
loop {
if let Some(signal) = signals.wait().next() {
log::info!("Received signal {signal} - shutting down");
if let Ok(rt) = Runtime::new() {
rt.block_on(async {
cleanup::exit(
Some((name.as_str(), self.ipc)),
true,
0,
)
.await;
});
} else {
executor::block_on(cleanup::exit(
Some((name.as_str(), self.ipc)),
false,
0,
));
}
}
}
});
log::debug!("Set up signal listener");
let mut ipc_set = JoinSet::<Result<()>>::new();
let mut cleanup = task::spawn_local(cleanup::cleanup());
let mut cleanup_done = false;
task::spawn_local(async move { loop {
tokio::select! {
Some(Ok(event)) = x_stream.next() => {
log::trace!("X event: {event:?}");
if let Err(e) = bar.process_event(&event) {
handle_error(e, &bar, self.ipc).await;
}
},
Some((alignment, result)) = bar.streams.next() => {
log::debug!("Received event from {alignment} panel at index {}", result.0);
match result {
(idx, Ok(draw_info)) => if let Err(e) = bar.update_panel(alignment, idx, draw_info) {
log::warn!("Error updating {alignment} panel at index {idx}");
handle_error(e, &bar, self.ipc).await;
}
(idx, Err(e)) => {
log::warn!("Error produced by {alignment} panel at index {idx:?}");
handle_error(e, &bar, self.ipc).await;
}
}
},
Some(Ok(stream)) = ipc_stream.next(), if bar.ipc => {
log::debug!("Received new ipc connection");
let (local_send, mut local_recv) = unbounded_channel();
let (ipc_send, ipc_recv) = unbounded_channel();
let wrapper = UnixStreamWrapper::new(stream, ChannelEndpoint::new(local_send, ipc_recv));
let _handle = task::spawn(wrapper.run());
log::trace!("wrapper running");
let message = local_recv.recv().await;
log::trace!("message received: {message:?}");
if let Some(message) = message {
match bar.send_message(message.as_str(), &mut ipc_set, ipc_send) {
Ok(true) => {
task::spawn_local(cleanup::exit(Some((bar.name.clone().leak(), self.ipc)), true, 0));
}
Err(e) => log::warn!("Sending message {message} generated an error: {e}"),
_ => {}
}
}
}
Some(_) = ipc_set.join_next() => {
log::debug!("ipc future completed");
}
Some(()) = endpoint1.recv.recv() => {
bar.shutdown();
let _ = endpoint1.send.send(());
let _ = endpoint1.recv.recv().await;
break;
}
res = &mut cleanup, if !cleanup_done => {
match res {
Ok(Ok(())) => {
log::info!("IPC dir cleanup finished");
}
Ok(Err(e)) => {
log::warn!("IPC dir cleanup failed: {e}");
}
Err(e) => {
log::warn!("Failed to join cleanup task: {e}");
}
}
cleanup_done = true;
}
}
} }).await?;
Ok(())
}
}
}