diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 0000000..7defb8c --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,36 @@ +/// Central module registry. Defines all modules with watch channels in one place. +/// +/// Invoke with a callback macro name. The callback receives repeated entries of the form: +/// { $feature:literal, $field:ident, $state:ty, [$($name:literal),+], [$($sig_name:literal),+], $module:path, $signal:ident, [$($default_arg:literal),*], $config:ident } +/// +/// Fields: +/// - feature: Cargo feature gate (e.g., "mod-network") +/// - field: AppReceivers field name (e.g., network) +/// - state: State type for the watch channel (e.g., NetworkState) +/// - names: CLI name aliases for dispatch (e.g., ["net", "network"]) +/// - signaler_names: Waybar module names to signal when the channel fires +/// (usually [first dispatch name], but audio signals ["vol", "mic"]) +/// - module: Module struct implementing WaybarModule (e.g., network::NetworkModule) +/// - signal: SignalsConfig field name (e.g., network) +/// - default_args: Default args for signaler evaluation +/// - config: Config section field name (e.g., network) +/// +/// Modules without watch channels (power, game, pool/btrfs) are handled manually. +macro_rules! for_each_watched_module { + ($m:ident) => { + $m! { + { "mod-network", network, crate::state::NetworkState, ["net", "network"], ["net"], crate::modules::network::NetworkModule, network, [], network } + { "mod-hardware", cpu, crate::state::CpuState, ["cpu"], ["cpu"], crate::modules::cpu::CpuModule, cpu, [], cpu } + { "mod-hardware", memory, crate::state::MemoryState, ["mem", "memory"], ["mem"], crate::modules::memory::MemoryModule, memory, [], memory } + { "mod-hardware", sys, crate::state::SysState, ["sys"], ["sys"], crate::modules::sys::SysModule, sys, [], sys } + { "mod-hardware", gpu, crate::state::GpuState, ["gpu"], ["gpu"], crate::modules::gpu::GpuModule, gpu, [], gpu } + { "mod-hardware", disks, Vec, ["disk"], ["disk"], crate::modules::disk::DiskModule, disk, ["/"], disk } + { "mod-bt", bluetooth, crate::state::BtState, ["bt", "bluetooth"], ["bt"], crate::modules::bt::BtModule, bt, ["show"], bt } + { "mod-audio", audio, crate::state::AudioState, ["vol", "audio"], ["vol", "mic"], crate::modules::audio::AudioModule, audio, ["sink", "show"], audio } + { "mod-dbus", mpris, crate::state::MprisState, ["mpris"], ["mpris"], crate::modules::mpris::MprisModule, mpris, [], mpris } + { "mod-dbus", backlight, crate::state::BacklightState, ["backlight"], ["backlight"], crate::modules::backlight::BacklightModule, backlight, [], backlight } + { "mod-dbus", keyboard, crate::state::KeyboardState, ["kbd", "keyboard"], ["kbd"], crate::modules::keyboard::KeyboardModule, keyboard, [], keyboard } + { "mod-dbus", dnd, crate::state::DndState, ["dnd"], ["dnd"], crate::modules::dnd::DndModule, dnd, [], dnd } + } + }; +} diff --git a/src/main.rs b/src/main.rs index 6d4bef4..07af785 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +#[macro_use] +mod macros; mod config; mod daemon; mod error; diff --git a/src/registry.rs b/src/registry.rs index d70dda0..e4bd07d 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -6,120 +6,66 @@ use crate::state::AppReceivers; #[allow(unused_imports)] use crate::modules::WaybarModule; -pub async fn dispatch( - module_name: &str, - #[allow(unused)] config: &Config, - #[allow(unused)] state: &AppReceivers, - #[allow(unused)] args: &[&str], -) -> FluxoResult { - if !config.is_module_enabled(module_name) { - return Err(FluxoError::Disabled(module_name.to_string())); - } +macro_rules! gen_dispatch { + ($( { $feature:literal, $field:ident, $state:ty, [$($name:literal),+], [$($sig_name:literal),+], $module:path, $signal:ident, [$($default_arg:literal),*], $config:ident } )*) => { + pub async fn dispatch( + module_name: &str, + #[allow(unused)] config: &Config, + #[allow(unused)] state: &AppReceivers, + #[allow(unused)] args: &[&str], + ) -> FluxoResult { + if !config.is_module_enabled(module_name) { + return Err(FluxoError::Disabled(module_name.to_string())); + } - match module_name { - #[cfg(feature = "mod-network")] - "net" | "network" => { - crate::modules::network::NetworkModule - .run(config, state, args) - .await + match module_name { + $( + #[cfg(feature = $feature)] + $($name)|+ => { + $module.run(config, state, args).await + } + )* + + // Dispatch-only modules (no watch channel) + #[cfg(feature = "mod-audio")] + "mic" => { + crate::modules::audio::AudioModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-hardware")] + "power" => { + crate::modules::power::PowerModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-hardware")] + "game" => { + crate::modules::game::GameModule + .run(config, state, args) + .await + } + #[cfg(feature = "mod-hardware")] + "pool" | "btrfs" => { + crate::modules::btrfs::BtrfsModule + .run(config, state, args) + .await + } + _ => Err(FluxoError::Ipc(format!("Unknown module: {}", module_name))), + } } - #[cfg(feature = "mod-hardware")] - "cpu" => { - crate::modules::cpu::CpuModule - .run(config, state, args) - .await + + /// Returns the default args used by the signaler when evaluating a module. + pub fn signaler_default_args(module_name: &str) -> &'static [&'static str] { + match module_name { + $( + $($name)|+ => &[$($default_arg),*], + )* + "mic" => &["source", "show"], + _ => &[], + } } - #[cfg(feature = "mod-hardware")] - "mem" | "memory" => { - crate::modules::memory::MemoryModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-hardware")] - "disk" => { - crate::modules::disk::DiskModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-hardware")] - "pool" | "btrfs" => { - crate::modules::btrfs::BtrfsModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-audio")] - "vol" | "audio" => { - crate::modules::audio::AudioModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-audio")] - "mic" => { - crate::modules::audio::AudioModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-hardware")] - "gpu" => { - crate::modules::gpu::GpuModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-hardware")] - "sys" => { - crate::modules::sys::SysModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-bt")] - "bt" | "bluetooth" => crate::modules::bt::BtModule.run(config, state, args).await, - #[cfg(feature = "mod-hardware")] - "power" => { - crate::modules::power::PowerModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-hardware")] - "game" => { - crate::modules::game::GameModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-dbus")] - "backlight" => { - crate::modules::backlight::BacklightModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-dbus")] - "kbd" | "keyboard" => { - crate::modules::keyboard::KeyboardModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-dbus")] - "dnd" => { - crate::modules::dnd::DndModule - .run(config, state, args) - .await - } - #[cfg(feature = "mod-dbus")] - "mpris" => { - crate::modules::mpris::MprisModule - .run(config, state, args) - .await - } - _ => Err(FluxoError::Ipc(format!("Unknown module: {}", module_name))), - } + }; } -/// Returns the default args used by the signaler when evaluating a module. -pub fn signaler_default_args(module_name: &str) -> &'static [&'static str] { - match module_name { - "disk" => &["/"], - "vol" | "audio" => &["sink", "show"], - "mic" => &["source", "show"], - "bt" | "bluetooth" => &["show"], - _ => &[], - } -} +for_each_watched_module!(gen_dispatch); diff --git a/src/signaler.rs b/src/signaler.rs index 010b027..78725e4 100644 --- a/src/signaler.rs +++ b/src/signaler.rs @@ -63,173 +63,78 @@ impl WaybarSignaler { debug!("Waybar process not found, skipping signal."); } } +} - pub async fn run(mut self, config_lock: Arc>, mut receivers: AppReceivers) { - let mut last_outputs: HashMap<&'static str, String> = HashMap::new(); +macro_rules! gen_signaler_run { + ($( { $feature:literal, $field:ident, $state:ty, [$($name:literal),+], [$($sig_name:literal),+], $module:path, $signal:ident, [$($default_arg:literal),*], $config:ident } )*) => { + impl WaybarSignaler { + pub async fn run(mut self, config_lock: Arc>, mut receivers: AppReceivers) { + let mut last_outputs: HashMap<&'static str, String> = HashMap::new(); - loop { - let signals = config_lock.read().await.signals.clone(); + loop { + let signals = config_lock.read().await.signals.clone(); - macro_rules! check_and_signal { - ($module_name:expr, $signal_opt:expr) => { - if let Some(sig) = $signal_opt { - let config = config_lock.read().await; - if let Some(out) = crate::daemon::evaluate_module_for_signaler( - $module_name, - &receivers, - &config, - ) - .await - { - if last_outputs.get($module_name) != Some(&out) { - last_outputs.insert($module_name, out); - self.send_signal(sig); + macro_rules! check_and_signal { + ($module_name:expr, $signal_opt:expr) => { + if let Some(sig) = $signal_opt { + let config = config_lock.read().await; + if let Some(out) = crate::daemon::evaluate_module_for_signaler( + $module_name, + &receivers, + &config, + ) + .await + { + if last_outputs.get($module_name) != Some(&out) { + last_outputs.insert($module_name, out); + self.send_signal(sig); + } + } } + }; + } + + // Generate cfg-gated futures for each watched module + $( + #[cfg(not(feature = $feature))] + let $field = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = $feature)] + let $field = receivers.$field.changed(); + )* + + // MPRIS scroll tick (special case — not a watched module) + #[cfg(not(feature = "mod-dbus"))] + let mpris_scroll_tick = std::future::pending::< + std::result::Result<(), tokio::sync::watch::error::RecvError>, + >(); + #[cfg(feature = "mod-dbus")] + let mpris_scroll_tick = receivers.mpris_scroll_tick.changed(); + + tokio::select! { + $( + res = $field, if signals.$signal.is_some() => { + if res.is_ok() { + $(check_and_signal!($sig_name, signals.$signal);)+ + } + } + )* + + // MPRIS scroll tick (separate from mpris data changes) + res = mpris_scroll_tick, if signals.mpris.is_some() => { + if res.is_ok() + && let Some(sig) = signals.mpris { self.send_signal(sig); } + } + + _ = sleep(Duration::from_secs(5)) => { + // loop and refresh config } } - }; - } - - // For disabled features, create futures that never resolve - #[cfg(not(feature = "mod-network"))] - let net_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-network")] - let net_changed = receivers.network.changed(); - - #[cfg(not(feature = "mod-hardware"))] - let cpu_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-hardware")] - let cpu_changed = receivers.cpu.changed(); - - #[cfg(not(feature = "mod-hardware"))] - let mem_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-hardware")] - let mem_changed = receivers.memory.changed(); - - #[cfg(not(feature = "mod-hardware"))] - let sys_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-hardware")] - let sys_changed = receivers.sys.changed(); - - #[cfg(not(feature = "mod-hardware"))] - let gpu_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-hardware")] - let gpu_changed = receivers.gpu.changed(); - - #[cfg(not(feature = "mod-hardware"))] - let disks_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-hardware")] - let disks_changed = receivers.disks.changed(); - - #[cfg(not(feature = "mod-bt"))] - let bt_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-bt")] - let bt_changed = receivers.bluetooth.changed(); - - #[cfg(not(feature = "mod-audio"))] - let audio_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-audio")] - let audio_changed = receivers.audio.changed(); - - #[cfg(not(feature = "mod-dbus"))] - let backlight_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-dbus")] - let backlight_changed = receivers.backlight.changed(); - - #[cfg(not(feature = "mod-dbus"))] - let keyboard_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-dbus")] - let keyboard_changed = receivers.keyboard.changed(); - - #[cfg(not(feature = "mod-dbus"))] - let dnd_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-dbus")] - let dnd_changed = receivers.dnd.changed(); - - #[cfg(not(feature = "mod-dbus"))] - let mpris_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-dbus")] - let mpris_changed = receivers.mpris.changed(); - - #[cfg(not(feature = "mod-dbus"))] - let mpris_scroll_tick_changed = std::future::pending::< - std::result::Result<(), tokio::sync::watch::error::RecvError>, - >(); - #[cfg(feature = "mod-dbus")] - let mpris_scroll_tick_changed = receivers.mpris_scroll_tick.changed(); - - tokio::select! { - res = net_changed, if signals.network.is_some() => { - if res.is_ok() { check_and_signal!("net", signals.network); } - } - res = cpu_changed, if signals.cpu.is_some() => { - if res.is_ok() { check_and_signal!("cpu", signals.cpu); } - } - res = mem_changed, if signals.memory.is_some() => { - if res.is_ok() { check_and_signal!("mem", signals.memory); } - } - res = sys_changed, if signals.sys.is_some() => { - if res.is_ok() { check_and_signal!("sys", signals.sys); } - } - res = gpu_changed, if signals.gpu.is_some() => { - if res.is_ok() { check_and_signal!("gpu", signals.gpu); } - } - res = disks_changed, if signals.disk.is_some() => { - if res.is_ok() { check_and_signal!("disk", signals.disk); } - } - res = bt_changed, if signals.bt.is_some() => { - if res.is_ok() { check_and_signal!("bt", signals.bt); } - } - res = audio_changed, if signals.audio.is_some() => { - if res.is_ok() { - check_and_signal!("vol", signals.audio); - check_and_signal!("mic", signals.audio); - } - } - res = backlight_changed, if signals.backlight.is_some() => { - if res.is_ok() { check_and_signal!("backlight", signals.backlight); } - } - res = keyboard_changed, if signals.keyboard.is_some() => { - if res.is_ok() { check_and_signal!("keyboard", signals.keyboard); } - } - res = dnd_changed, if signals.dnd.is_some() => { - if res.is_ok() { check_and_signal!("dnd", signals.dnd); } - } - res = mpris_changed, if signals.mpris.is_some() => { - if res.is_ok() { check_and_signal!("mpris", signals.mpris); } - } - res = mpris_scroll_tick_changed, if signals.mpris.is_some() => { - if res.is_ok() - && let Some(sig) = signals.mpris { self.send_signal(sig); } - } - _ = sleep(Duration::from_secs(5)) => { - // loop and refresh config } } } - } + }; } + +for_each_watched_module!(gen_signaler_run); diff --git a/src/state.rs b/src/state.rs index 557c2f3..a9149ce 100644 --- a/src/state.rs +++ b/src/state.rs @@ -4,44 +4,29 @@ use std::sync::Arc; use tokio::sync::{RwLock, mpsc, watch}; use tokio::time::Instant; -#[derive(Clone)] -pub struct AppReceivers { - #[cfg(feature = "mod-network")] - pub network: watch::Receiver, - #[cfg(feature = "mod-hardware")] - pub cpu: watch::Receiver, - #[cfg(feature = "mod-hardware")] - pub memory: watch::Receiver, - #[cfg(feature = "mod-hardware")] - pub sys: watch::Receiver, - #[cfg(feature = "mod-hardware")] - pub gpu: watch::Receiver, - #[cfg(feature = "mod-hardware")] - pub disks: watch::Receiver>, - #[cfg(feature = "mod-bt")] - pub bluetooth: watch::Receiver, - #[cfg(feature = "mod-bt")] - pub bt_cycle: Arc>, - #[cfg(feature = "mod-audio")] - pub audio: watch::Receiver, - #[cfg(feature = "mod-dbus")] - pub mpris: watch::Receiver, - #[cfg(feature = "mod-dbus")] - pub backlight: watch::Receiver, - #[cfg(feature = "mod-dbus")] - pub keyboard: watch::Receiver, - #[cfg(feature = "mod-dbus")] - pub dnd: watch::Receiver, - #[cfg(feature = "mod-dbus")] - pub mpris_scroll: Arc>, - #[cfg(feature = "mod-dbus")] - pub mpris_scroll_tick: watch::Receiver, - pub health: Arc>>, - #[cfg(feature = "mod-bt")] - pub bt_force_poll: mpsc::Sender<()>, - #[cfg(feature = "mod-audio")] - pub audio_cmd_tx: mpsc::Sender, +macro_rules! gen_app_receivers { + ($( { $feature:literal, $field:ident, $state:ty, [$($name:literal),+], [$($sig_name:literal),+], $module:path, $signal:ident, [$($default_arg:literal),*], $config:ident } )*) => { + #[derive(Clone)] + pub struct AppReceivers { + $( + #[cfg(feature = $feature)] + pub $field: watch::Receiver<$state>, + )* + #[cfg(feature = "mod-bt")] + pub bt_cycle: Arc>, + #[cfg(feature = "mod-dbus")] + pub mpris_scroll: Arc>, + #[cfg(feature = "mod-dbus")] + pub mpris_scroll_tick: watch::Receiver, + pub health: Arc>>, + #[cfg(feature = "mod-bt")] + pub bt_force_poll: mpsc::Sender<()>, + #[cfg(feature = "mod-audio")] + pub audio_cmd_tx: mpsc::Sender, + } + }; } +for_each_watched_module!(gen_app_receivers); #[derive(Clone, Default)] pub struct ModuleHealth {