Skip to main content

waybar_dynamic/
lib.rs

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
21/// Global registry (Send+Sync): tracks which names already have a listener.
22static LISTENER_REGISTRY: std::sync::LazyLock<Mutex<HashMap<String, SharedState>>> =
23    std::sync::LazyLock::new(|| Mutex::new(HashMap::new()));
24
25// GTK-thread-local: all GtkBox containers per instance name.
26// Each init() adds its container. rebuild_ui updates all live ones.
27thread_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        // Register this container.
66        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            // Listener already exists — rebuild all containers with current state.
77            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        // First init — create state, listener, rebuild task.
87        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
112/// Rebuild widgets in all containers for a given instance name.
113/// Skips containers that are no longer realized (orphaned by waybar).
114fn 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            // Prune dead containers.
119            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);