use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use serde::Deserialize;
use waybar_cffi::{
gtk::{
glib,
prelude::{ContainerExt, StyleContextExt, WidgetExt},
Box as GtkBox, EventBox, Label, Orientation,
},
waybar_module, InitInfo, Module,
};
use waybar_dynamic_core::socket::socket_path;
mod ipc;
mod state;
use state::SharedState;
static LISTENER_REGISTRY: std::sync::LazyLock<Mutex<HashMap<String, SharedState>>> =
std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
thread_local! {
static CONTAINERS: RefCell<HashMap<String, Vec<GtkBox>>> =
RefCell::new(HashMap::new());
}
#[derive(Deserialize)]
struct Config {
#[serde(default = "default_name")]
name: String,
#[serde(default = "default_spacing")]
spacing: i32,
}
fn default_name() -> String {
"waybar-dynamic".to_string()
}
fn default_spacing() -> i32 {
4
}
struct DynamicModule {
_state: SharedState,
}
impl Module for DynamicModule {
type Config = Config;
fn init(info: &InitInfo, config: Config) -> Self {
let root = info.get_root_widget();
let inner = GtkBox::new(Orientation::Horizontal, config.spacing);
inner.set_widget_name("waybar-dynamic");
inner.style_context().add_class(&config.name);
root.add(&inner);
root.show_all();
CONTAINERS.with(|c| {
c.borrow_mut()
.entry(config.name.clone())
.or_default()
.push(inner);
});
let mut registry = LISTENER_REGISTRY.lock().expect("waybar-dynamic: poisoned mutex");
if let Some(state) = registry.get(&config.name).cloned() {
eprintln!(
"waybar-dynamic: reusing listener for '{}'",
config.name
);
rebuild_all(&config.name, &state);
drop(registry);
return DynamicModule { _state: state };
}
let state = SharedState::new();
let (tx, rx) = async_channel::unbounded::<()>();
let rebuild_name = config.name.clone();
let rebuild_state = state.clone();
glib::MainContext::default().spawn_local(async move {
while rx.recv().await.is_ok() {
rebuild_all(&rebuild_name, &rebuild_state);
}
});
let on_update: Arc<dyn Fn() + Send + Sync + 'static> = Arc::new(move || {
let _ = tx.send_blocking(());
});
ipc::spawn_listener(socket_path(&config.name), state.clone(), on_update);
registry.insert(config.name, state.clone());
drop(registry);
DynamicModule { _state: state }
}
}
fn rebuild_all(name: &str, state: &SharedState) {
CONTAINERS.with(|c| {
let mut map = c.borrow_mut();
if let Some(containers) = map.get_mut(name) {
containers.retain(|b| b.is_realized());
for inner in containers.iter() {
rebuild_ui(inner, state);
}
}
});
}
fn rebuild_ui(inner: &GtkBox, state: &SharedState) {
let snapshot = state.snapshot();
for child in inner.children() {
inner.remove(&child);
}
for spec in snapshot {
let label = Label::new(Some(&spec.label));
if let Some(id) = &spec.id {
label.set_widget_name(id.as_str());
}
let ctx = label.style_context();
for class in &spec.classes {
ctx.add_class(class.as_str());
}
if let Some(tip) = &spec.tooltip {
label.set_tooltip_text(Some(tip.as_str()));
}
let event_box = EventBox::new();
event_box.add(&label);
let hover_label = label.clone();
event_box.connect_enter_notify_event(move |_, _| {
hover_label.style_context().add_class("hover");
glib::Propagation::Proceed
});
let hover_label = label.clone();
event_box.connect_leave_notify_event(move |_, _| {
hover_label.style_context().remove_class("hover");
glib::Propagation::Proceed
});
if spec.on_click.is_some() || spec.on_right_click.is_some() || spec.on_middle_click.is_some() {
let left_cmd = spec.on_click.clone();
let right_cmd = spec.on_right_click.clone();
let middle_cmd = spec.on_middle_click.clone();
event_box.connect_button_press_event(move |_, event| {
match event.button() {
1 => { if let Some(cmd) = &left_cmd { run_command(cmd); } }
2 => { if let Some(cmd) = &middle_cmd { run_command(cmd); } }
3 => { if let Some(cmd) = &right_cmd { run_command(cmd); } }
_ => {}
}
glib::Propagation::Proceed
});
}
inner.add(&event_box);
event_box.show_all();
}
inner.show();
}
fn run_command(cmd: &str) {
if let Err(e) = std::process::Command::new("sh")
.arg("-c")
.arg(cmd)
.spawn()
{
eprintln!("waybar-dynamic: failed to run command {:?}: {}", cmd, e);
}
}
waybar_module!(DynamicModule);