1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::sync::{Arc, Mutex};
4
5use serde::Deserialize;
6use waybar_cffi::{
7 gtk::{
8 glib,
9 prelude::{ContainerExt, StyleContextExt, WidgetExt},
10 Box as GtkBox, EventBox, Label, Orientation,
11 },
12 waybar_module, InitInfo, Module,
13};
14use waybar_dynamic_core::socket::socket_path;
15
16mod ipc;
17mod state;
18
19use state::SharedState;
20
21static LISTENER_REGISTRY: std::sync::LazyLock<Mutex<HashMap<String, SharedState>>> =
23 std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
24
25thread_local! {
28 static CONTAINERS: RefCell<HashMap<String, Vec<GtkBox>>> =
29 RefCell::new(HashMap::new());
30}
31
32#[derive(Deserialize)]
33struct Config {
34 #[serde(default = "default_name")]
35 name: String,
36 #[serde(default = "default_spacing")]
37 spacing: i32,
38}
39
40fn default_name() -> String {
41 "waybar-dynamic".to_string()
42}
43
44fn default_spacing() -> i32 {
45 4
46}
47
48struct DynamicModule {
49 _state: SharedState,
50}
51
52impl Module for DynamicModule {
53 type Config = Config;
54
55 fn init(info: &InitInfo, config: Config) -> Self {
56 let root = info.get_root_widget();
57 let inner = GtkBox::new(Orientation::Horizontal, config.spacing);
58
59 inner.set_widget_name("waybar-dynamic");
60 inner.style_context().add_class(&config.name);
61
62 root.add(&inner);
63 root.show_all();
64
65 CONTAINERS.with(|c| {
67 c.borrow_mut()
68 .entry(config.name.clone())
69 .or_default()
70 .push(inner);
71 });
72
73 let mut registry = LISTENER_REGISTRY.lock().expect("waybar-dynamic: poisoned mutex");
74
75 if let Some(state) = registry.get(&config.name).cloned() {
76 eprintln!(
78 "waybar-dynamic: reusing listener for '{}'",
79 config.name
80 );
81 rebuild_all(&config.name, &state);
82 drop(registry);
83 return DynamicModule { _state: state };
84 }
85
86 let state = SharedState::new();
88
89 let (tx, rx) = async_channel::unbounded::<()>();
90
91 let rebuild_name = config.name.clone();
92 let rebuild_state = state.clone();
93 glib::MainContext::default().spawn_local(async move {
94 while rx.recv().await.is_ok() {
95 rebuild_all(&rebuild_name, &rebuild_state);
96 }
97 });
98
99 let on_update: Arc<dyn Fn() + Send + Sync + 'static> = Arc::new(move || {
100 let _ = tx.send_blocking(());
101 });
102
103 ipc::spawn_listener(socket_path(&config.name), state.clone(), on_update);
104
105 registry.insert(config.name, state.clone());
106 drop(registry);
107
108 DynamicModule { _state: state }
109 }
110}
111
112fn rebuild_all(name: &str, state: &SharedState) {
115 CONTAINERS.with(|c| {
116 let mut map = c.borrow_mut();
117 if let Some(containers) = map.get_mut(name) {
118 containers.retain(|b| b.is_realized());
120 for inner in containers.iter() {
121 rebuild_ui(inner, state);
122 }
123 }
124 });
125}
126
127fn rebuild_ui(inner: &GtkBox, state: &SharedState) {
128 let snapshot = state.snapshot();
129 for child in inner.children() {
130 inner.remove(&child);
131 }
132
133 for spec in snapshot {
134 let label = Label::new(Some(&spec.label));
135
136 if let Some(id) = &spec.id {
137 label.set_widget_name(id.as_str());
138 }
139
140 let ctx = label.style_context();
141 for class in &spec.classes {
142 ctx.add_class(class.as_str());
143 }
144
145 if let Some(tip) = &spec.tooltip {
146 label.set_tooltip_text(Some(tip.as_str()));
147 }
148
149 let event_box = EventBox::new();
150 event_box.add(&label);
151
152 let hover_label = label.clone();
153 event_box.connect_enter_notify_event(move |_, _| {
154 hover_label.style_context().add_class("hover");
155 glib::Propagation::Proceed
156 });
157
158 let hover_label = label.clone();
159 event_box.connect_leave_notify_event(move |_, _| {
160 hover_label.style_context().remove_class("hover");
161 glib::Propagation::Proceed
162 });
163
164 if spec.on_click.is_some() || spec.on_right_click.is_some() || spec.on_middle_click.is_some() {
165 let left_cmd = spec.on_click.clone();
166 let right_cmd = spec.on_right_click.clone();
167 let middle_cmd = spec.on_middle_click.clone();
168
169 event_box.connect_button_press_event(move |_, event| {
170 match event.button() {
171 1 => { if let Some(cmd) = &left_cmd { run_command(cmd); } }
172 2 => { if let Some(cmd) = &middle_cmd { run_command(cmd); } }
173 3 => { if let Some(cmd) = &right_cmd { run_command(cmd); } }
174 _ => {}
175 }
176 glib::Propagation::Proceed
177 });
178 }
179
180 inner.add(&event_box);
181 event_box.show_all();
182 }
183
184 inner.show();
185}
186
187fn run_command(cmd: &str) {
188 if let Err(e) = std::process::Command::new("sh")
189 .arg("-c")
190 .arg(cmd)
191 .spawn()
192 {
193 eprintln!("waybar-dynamic: failed to run command {:?}: {}", cmd, e);
194 }
195}
196
197waybar_module!(DynamicModule);