waybar-dynamic 0.1.0

Dynamic widget CFFI module for Waybar — add, update, and remove widgets at runtime via Unix socket.
Documentation
use std::io::{BufRead, BufReader};
use std::os::unix::net::UnixListener;
use std::path::PathBuf;
use std::sync::Arc;

use waybar_dynamic_core::protocol::IpcMessage;

use crate::state::SharedState;

/// Spawn the IPC listener on a background OS thread.
///
/// Each newline-delimited JSON message is applied to `state`.
/// After each update, an idle callback is posted to the GTK main loop
/// via the async channel so the UI rebuild runs on the correct thread.
///
/// The listener runs for the lifetime of the process. Waybar does not
/// call `init()` again after output changes, so the listener must keep
/// accepting connections even if the GTK widget tree has been rebuilt.
pub fn spawn_listener(
    socket_path: PathBuf,
    state: SharedState,
    on_update: Arc<dyn Fn() + Send + Sync + 'static>,
) {
    // Remove stale socket file left over from a previous run.
    let _ = std::fs::remove_file(&socket_path);

    let listener = match UnixListener::bind(&socket_path) {
        Ok(l) => l,
        Err(e) => {
            eprintln!(
                "waybar-dynamic: failed to bind IPC socket at {}: {}",
                socket_path.display(),
                e
            );
            return;
        }
    };

    eprintln!(
        "waybar-dynamic: listening on {}",
        socket_path.display()
    );

    std::thread::spawn(move || {
        for stream in listener.incoming() {
            let stream = match stream {
                Ok(s) => s,
                Err(e) => {
                    eprintln!("waybar-dynamic: accept error: {}", e);
                    // Continue accepting new connections — one bad accept
                    // does not kill the listener.
                    continue;
                }
            };

            let state = state.clone();
            let on_update = Arc::clone(&on_update);

            // Each connection is handled on its own thread so that a slow
            // or broken sender does not block other connections.
            std::thread::spawn(move || {
                let reader = BufReader::new(stream);
                for line in reader.lines() {
                    let line = match line {
                        Ok(l) => l,
                        Err(e) => {
                            // Broken pipe or similar — just close this connection.
                            eprintln!("waybar-dynamic: read error: {}", e);
                            return;
                        }
                    };

                    let line = line.trim().to_string();
                    if line.is_empty() {
                        continue;
                    }

                    let msg: IpcMessage = match serde_json::from_str(&line) {
                        Ok(m) => m,
                        Err(e) => {
                            eprintln!("waybar-dynamic: invalid message: {}", e);
                            continue;
                        }
                    };

                    let widget_count = match &msg {
                        IpcMessage::SetAll { widgets } => widgets.len(),
                        IpcMessage::Patch { upsert, .. } => upsert.len(),
                        IpcMessage::Clear => 0,
                    };

                    match msg {
                        IpcMessage::SetAll { widgets } => state.set_all(widgets),
                        IpcMessage::Patch { upsert, remove } => state.patch(upsert, remove),
                        IpcMessage::Clear => state.clear(),
                    }
                    eprintln!(
                        "waybar-dynamic: received message, state now has {} widget(s)",
                        widget_count
                    );

                    // Post the UI rebuild to the GTK main thread.
                    let cb = Arc::clone(&on_update);
                    cb();
                }
            });
        }
    });
}