niri_taskbar/
lib.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use button::Button;
4use config::Config;
5use error::Error;
6use state::State;
7use waybar_cffi::{
8    gtk::{
9        self,
10        glib::MainContext,
11        traits::{BoxExt, ContainerExt, StyleContextExt, WidgetExt},
12        Orientation,
13    },
14    waybar_module, Module,
15};
16
17mod button;
18mod config;
19mod error;
20mod icon;
21mod niri;
22mod state;
23
24struct TaskbarModule {}
25
26impl Module for TaskbarModule {
27    type Config = Config;
28
29    fn init(info: &waybar_cffi::InitInfo, config: Config) -> Self {
30        let module = Self {};
31        let state = State::new(config);
32
33        if let Err(e) = init(info, state) {
34            eprintln!("niri taskbar module init error: {e:?}");
35        }
36
37        module
38    }
39}
40
41waybar_module!(TaskbarModule);
42
43fn init(info: &waybar_cffi::InitInfo, state: State) -> Result<(), Error> {
44    // Set up the box that we'll use to contain the actual window buttons.
45    let root = info.get_root_widget();
46    let container = gtk::Box::new(Orientation::Horizontal, 0);
47    container.style_context().add_class("niri-taskbar");
48    root.add(&container);
49
50    // We need to spawn a task to receive the window snapshots and update the container.
51    let context = MainContext::default();
52    let stream = state.niri().window_stream()?;
53
54    context.spawn_local(async move {
55        // It's inefficient to recreate every button every time, so we keep them in a cache and
56        // just reorder the container as we update based on each snapshot. This also avoids
57        // flickering while rendering, since the images don't have to be reloaded from disk.
58        let mut buttons = BTreeMap::new();
59
60        while let Some(windows) = stream.next().await {
61            // We need to track which, if any, windows are no longer present.
62            let mut omitted = buttons.keys().copied().collect::<BTreeSet<_>>();
63
64            for window in windows.into_iter() {
65                let button = buttons.entry(window.id).or_insert_with(|| {
66                    let button = Button::new(&state, &window);
67
68                    // Implicitly adding the button widget to the box as we create it simplifies
69                    // reordering, since it means we can just do it as we go.
70                    container.add(button.widget());
71                    button
72                });
73
74                // Update the window properties.
75                button.set_focus(window.is_focused);
76                button.set_title(window.title.as_deref());
77
78                // Ensure we don't remove this button from the container.
79                omitted.remove(&window.id);
80
81                // Since we get the windows in order in the snapshot, we can just push this to the
82                // back and then let other widgets push in front as we iterate.
83                container.reorder_child(button.widget(), -1);
84            }
85
86            // Remove any windows that no longer exist.
87            for id in omitted.into_iter() {
88                if let Some(button) = buttons.remove(&id) {
89                    container.remove(button.widget());
90                }
91            }
92
93            // Ensure everything is rendered.
94            container.show_all();
95        }
96    });
97
98    Ok(())
99}