From ffdb689ef93b88b8b71dc8bdb62b574289c94037 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sun, 5 Apr 2026 21:12:11 +0200 Subject: [PATCH] added missing docs --- src/modules/audio.rs | 29 +++++++++++++++++++++-------- src/modules/backlight.rs | 15 ++++++++++----- src/modules/bt/buds.rs | 12 ++++++++++++ src/modules/bt/maestro.rs | 36 ++++++++++++++++++++++++++++-------- src/modules/bt/mod.rs | 18 +++++++++++++++--- src/modules/btrfs.rs | 5 +++++ src/modules/cpu.rs | 3 +++ src/modules/disk.rs | 4 ++++ src/modules/dnd.rs | 17 +++++++++++++---- src/modules/game.rs | 5 +++++ src/modules/gpu.rs | 4 ++++ src/modules/hardware.rs | 27 ++++++++++++++++++++++----- src/modules/keyboard.rs | 16 ++++++++++++---- src/modules/memory.rs | 3 +++ src/modules/mpris.rs | 31 ++++++++++++++++++++++++++----- src/modules/network.rs | 23 ++++++++++++++++++----- src/modules/power.rs | 6 ++++-- src/modules/sys.rs | 3 +++ 18 files changed, 208 insertions(+), 49 deletions(-) diff --git a/src/modules/audio.rs b/src/modules/audio.rs index 5808326..c1f7e23 100644 --- a/src/modules/audio.rs +++ b/src/modules/audio.rs @@ -1,3 +1,10 @@ +//! PulseAudio/PipeWire sink + source indicator with live event subscription. +//! +//! The daemon runs on its own OS thread because libpulse's threaded mainloop +//! must drive callbacks inside its own lock scope. Volume/mute changes are +//! routed back via an async [`mpsc`] channel — the module handlers [`run`]s +//! only push commands; the thread performs the actual libpulse calls. + use crate::config::Config; use crate::error::{FluxoError, Result}; use crate::modules::WaybarModule; @@ -13,6 +20,7 @@ use std::sync::Arc; use tokio::sync::{mpsc, watch}; use tracing::error; +/// Commands the module handler sends to the audio daemon thread. pub enum AudioCommand { ChangeVolume { is_sink: bool, @@ -27,13 +35,17 @@ pub enum AudioCommand { }, } +/// Long-lived daemon driving libpulse's threaded mainloop. pub struct AudioDaemon; impl AudioDaemon { + /// Construct a new (stateless) daemon. pub fn new() -> Self { Self } + /// Spawn the audio thread, subscribe to sink/source/server events, and + /// start consuming [`AudioCommand`]s. pub fn start( &self, state_tx: &watch::Sender, @@ -56,7 +68,6 @@ impl AudioDaemon { mainloop.lock(); - // Wait for context to be ready loop { match context.get_state() { libpulse_binding::context::State::Ready => break, @@ -74,10 +85,8 @@ impl AudioDaemon { } } - // Initial fetch let _ = fetch_audio_data_sync(&mut context, &state_tx); - // Subscribe to events let interest = InterestMaskSet::SINK | InterestMaskSet::SOURCE | InterestMaskSet::SERVER; context.subscribe(interest, |_| {}); @@ -196,7 +205,6 @@ impl AudioDaemon { mainloop.lock(); - // Fetch data and update available sinks/sources let _ = fetch_audio_data_sync(&mut context, &state_tx); mainloop.unlock(); @@ -207,13 +215,12 @@ impl AudioDaemon { use std::time::Duration; +/// Trigger async libpulse introspection: server defaults + sink/source lists. +/// Callbacks publish onto `state_tx` as results land. fn fetch_audio_data_sync( context: &mut Context, state_tx: &watch::Sender, ) -> Result<()> { - // We fetch all sinks and sources, and also server info to know defaults. - // The order doesn't strictly matter as long as we update correctly. - let tx_server = state_tx.clone(); context.introspect().get_server_info(move |info| { let mut current = tx_server.borrow().clone(); @@ -269,6 +276,8 @@ fn device_info_from( (desc, vol, muted, channels) } +/// Write `info` into `target` only when `item_name` matches the currently +/// selected default device — other sinks/sources are ignored here. fn apply_device_info(target: &mut AudioDeviceInfo, item_name: &str, info: (String, u8, bool, u8)) { if item_name == target.name { target.description = info.0; @@ -278,6 +287,7 @@ fn apply_device_info(target: &mut AudioDeviceInfo, item_name: &str, info: (Strin } } +/// Dispatch `get_sink_info_list` and collect names into `available_sinks`. fn fetch_sinks(context: &mut Context, state_tx: &watch::Sender) { let tx = state_tx.clone(); let pending = PendingList::new(); @@ -313,6 +323,8 @@ fn fetch_sinks(context: &mut Context, state_tx: &watch::Sender) { }); } +/// Dispatch `get_source_info_list` and collect names (skipping `.monitor` +/// virtual sources) into `available_sources`. fn fetch_sources(context: &mut Context, state_tx: &watch::Sender) { let tx = state_tx.clone(); let pending = PendingList::new(); @@ -348,6 +360,8 @@ fn fetch_sources(context: &mut Context, state_tx: &watch::Sender) { }); } +/// Renders sink/source + dispatches volume/mute/cycle commands. +/// Args: `[sink|source] [show|up|down|mute|cycle] [step]`. pub struct AudioModule; impl WaybarModule for AudioModule { @@ -413,7 +427,6 @@ impl AudioModule { }; if name.is_empty() { - // Fallback if daemon hasn't populated state yet return Ok(WaybarOutput { text: "Audio Loading...".to_string(), ..Default::default() diff --git a/src/modules/backlight.rs b/src/modules/backlight.rs index 5c66ac6..6c6be1b 100644 --- a/src/modules/backlight.rs +++ b/src/modules/backlight.rs @@ -1,3 +1,7 @@ +//! Screen backlight indicator, driven by `inotify` on +//! `/sys/class/backlight/*/actual_brightness`. Falls back to a 5 s poll loop +//! to catch any missed events. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -11,6 +15,7 @@ use std::time::Duration; use tokio::sync::watch; use tracing::{error, info}; +/// Renders the brightness percentage with a vendor-agnostic icon bucket. pub struct BacklightModule; impl WaybarModule for BacklightModule { @@ -47,13 +52,16 @@ impl WaybarModule for BacklightModule { } } +/// Background `inotify` watcher thread for the sysfs backlight file. pub struct BacklightDaemon; impl BacklightDaemon { + /// Construct a new (stateless) daemon. pub fn new() -> Self { Self } + /// Spawn an OS thread that publishes brightness changes onto `tx`. pub fn start(&self, tx: watch::Sender) { std::thread::spawn(move || { let base_dir = PathBuf::from("/sys/class/backlight"); @@ -105,12 +113,10 @@ impl BacklightDaemon { } }; - // Initial poll let _ = tx.send(BacklightState { percentage: get_percentage(), }); - // Set up notify watcher let (ev_tx, ev_rx) = mpsc::channel(); let mut watcher = RecommendedWatcher::new( move |res: notify::Result| { @@ -130,9 +136,8 @@ impl BacklightDaemon { } loop { - // Block until an event occurs or a timeout to catch missed events if ev_rx.recv_timeout(Duration::from_secs(5)).is_ok() { - // Debounce rapid events + // Debounce bursts from scroll-driven brightness changes. std::thread::sleep(Duration::from_millis(50)); while ev_rx.try_recv().is_ok() {} @@ -140,7 +145,7 @@ impl BacklightDaemon { percentage: get_percentage(), }); } else { - // Timeout hit, poll just in case + // Timeout reached — resync in case an event was missed. let current = get_percentage(); if tx.borrow().percentage != current { let _ = tx.send(BacklightState { diff --git a/src/modules/bt/buds.rs b/src/modules/bt/buds.rs index 403b06a..3ad95f9 100644 --- a/src/modules/bt/buds.rs +++ b/src/modules/bt/buds.rs @@ -1,3 +1,6 @@ +//! Per-device BT plugin trait + PixelBuds Pro implementation on top of the +//! Maestro GATT connection. + use crate::config::Config; use crate::error::{FluxoError, Result as FluxoResult}; use crate::modules::bt::maestro::BudsCommand; @@ -5,29 +8,38 @@ use crate::state::AppReceivers; use crate::utils::TokenValue; use futures::future::BoxFuture; +/// A device-specific adapter that can enrich [`BtState`](crate::state::BtState) +/// with extra metadata and expose control actions (modes). pub trait BtPlugin: Send + Sync { + /// Plugin identifier used for logging. fn name(&self) -> &str; + /// Return true if this plugin handles a device with `alias`/`mac`. fn can_handle(&self, alias: &str, mac: &str) -> bool; + /// Return `(token_name, value)` pairs merged into the rendered template. fn get_data( &self, config: &Config, state: &AppReceivers, mac: &str, ) -> BoxFuture<'static, FluxoResult>>; + /// List of mode identifiers the plugin can switch between. fn get_modes( &self, mac: &str, state: &AppReceivers, ) -> BoxFuture<'static, FluxoResult>>; + /// Switch device to `mode` (must be one returned by `get_modes`). fn set_mode( &self, mode: &str, mac: &str, state: &AppReceivers, ) -> BoxFuture<'static, FluxoResult<()>>; + /// Advance to the next mode in the list (wraps around). fn cycle_mode(&self, mac: &str, state: &AppReceivers) -> BoxFuture<'static, FluxoResult<()>>; } +/// Google Pixel Buds Pro plugin. Reads battery + ANC state via Maestro GATT. pub struct PixelBudsPlugin; impl BtPlugin for PixelBudsPlugin { diff --git a/src/modules/bt/maestro.rs b/src/modules/bt/maestro.rs index ca33ef0..5632dc8 100644 --- a/src/modules/bt/maestro.rs +++ b/src/modules/bt/maestro.rs @@ -1,3 +1,11 @@ +//! Google Maestro (PixelBuds GATT) integration. +//! +//! Each connected device gets its own [`buds_task`] running on a dedicated +//! single-threaded runtime. The task opens an RFCOMM channel, speaks the +//! Maestro protocol to read battery + ANC state, and listens for settings +//! changes. External callers interact via [`MaestroManager::send_command`] +//! and [`MaestroManager::get_status`]. + use crate::state::AppReceivers; use anyhow::{Context, Result}; use futures::StreamExt; @@ -7,12 +15,12 @@ use std::time::{Duration, Instant}; use tokio::sync::mpsc; use tracing::{debug, error, info, warn}; -// Maestro imports use maestro::protocol::codec::Codec; use maestro::pwrpc::client::Client; use maestro::service::MaestroService; use maestro::service::settings::{self, SettingValue}; +/// Cached per-device snapshot returned to BT plugin consumers. #[derive(Clone, Default)] pub struct BudsStatus { pub left_battery: Option, @@ -24,28 +32,35 @@ pub struct BudsStatus { pub error: Option, } +/// Command that can be issued against a connected buds device. pub enum BudsCommand { + /// Set the ANC mode: `active`, `aware`, or `off`. SetAnc(String), } +/// Messages sent to the [`MaestroManager`] control thread. pub enum ManagerCommand { + /// Ensure a [`buds_task`] is running for `mac`; spawn if absent. EnsureTask(String), + /// Forward a [`BudsCommand`] to the task for `mac`. SendCommand(String, BudsCommand), } +/// Owns all buds-task lifetimes and a shared status cache. pub struct MaestroManager { statuses: Arc>>, management_tx: mpsc::UnboundedSender, } impl MaestroManager { + /// Spawn the management thread + runtime and return a handle. pub fn new(state: AppReceivers) -> Self { let (tx, mut rx) = mpsc::unbounded_channel::(); let statuses = Arc::new(Mutex::new(HashMap::new())); let statuses_clone = Arc::clone(&statuses); let state_clone = state.clone(); - // Start dedicated BT management thread + // Dedicated thread — bluer uses per-thread local tasks. std::thread::spawn(move || { let rt = tokio::runtime::Builder::new_current_thread() .enable_all() @@ -85,7 +100,7 @@ impl MaestroManager { } } _ = tokio::time::sleep(Duration::from_millis(100)) => { - // Cleanup dropped tasks if needed + // Wake tick: future hook for task-lifecycle cleanup. } } } @@ -98,17 +113,20 @@ impl MaestroManager { } } + /// Return the cached [`BudsStatus`] for `mac` (default if absent). pub fn get_status(&self, mac: &str) -> BudsStatus { let statuses = self.statuses.lock().unwrap(); statuses.get(mac).cloned().unwrap_or_default() } + /// Request that a buds task be running for `mac`. Idempotent. pub fn ensure_task(&self, mac: &str) { let _ = self .management_tx .send(ManagerCommand::EnsureTask(mac.to_string())); } + /// Ensure a task exists and forward `cmd` to it. pub fn send_command(&self, mac: &str, cmd: BudsCommand) -> Result<()> { self.ensure_task(mac); let _ = self @@ -118,6 +136,8 @@ impl MaestroManager { } } +/// Per-device async task: opens RFCOMM, runs the Maestro codec, mirrors +/// battery/ANC state into the shared status map, and consumes commands. async fn buds_task( mac: &str, statuses: Arc>>, @@ -150,7 +170,7 @@ async fn buds_task( break; } - // Connect to Maestro RFCOMM service + // Maestro historically listens on channel 1 or 2 — probe both. let mut stream = None; for channel in [1, 2] { let socket = match bluer::rfcomm::Socket::new() { @@ -190,13 +210,11 @@ async fn buds_task( info!("Connected Maestro RFCOMM to {} on channel", mac); - // Initialize Maestro communication stack let codec = Codec::new(); let stream = codec.wrap(stream); let mut client = Client::new(stream); let handle = client.handle(); - // Resolve Maestro channel let channel = match maestro::protocol::utils::resolve_channel(&mut client).await { Ok(c) => c, Err(e) => { @@ -213,7 +231,7 @@ async fn buds_task( let mut service = MaestroService::new(handle, channel); - // Update health + // Successful connect — clear health backoff for bt.buds. { let mut lock = state.health.write().await; let health = lock.entry("bt.buds".to_string()).or_default(); @@ -221,7 +239,6 @@ async fn buds_task( health.backoff_until = None; } - // Query initial ANC state if let Ok(val) = service .read_setting_var(settings::SettingId::CurrentAncrState) .await @@ -337,6 +354,7 @@ async fn buds_task( Ok(()) } +/// String ("active"/"aware"/"off") → Maestro enum; unknown falls back to `Off`. fn mode_to_anc_state(mode: &str) -> settings::AncState { match mode { "active" => settings::AncState::Active, @@ -346,6 +364,7 @@ fn mode_to_anc_state(mode: &str) -> settings::AncState { } } +/// Inverse of [`mode_to_anc_state`] for status readout. pub fn anc_state_to_string(state: &settings::AncState) -> String { match state { settings::AncState::Active => "active".to_string(), @@ -357,6 +376,7 @@ pub fn anc_state_to_string(state: &settings::AncState) -> String { static MAESTRO: OnceLock = OnceLock::new(); +/// Lazily initialise the process-wide [`MaestroManager`] and return a reference. pub fn get_maestro(state: &AppReceivers) -> &MaestroManager { MAESTRO.get_or_init(|| MaestroManager::new(state.clone())) } diff --git a/src/modules/bt/mod.rs b/src/modules/bt/mod.rs index dff02a2..1f06f80 100644 --- a/src/modules/bt/mod.rs +++ b/src/modules/bt/mod.rs @@ -1,3 +1,11 @@ +//! Bluetooth indicator + control (BlueZ via `bluer`). +//! +//! Core loop: filter paired+connected audio-sink devices, enrich them via +//! per-device [`BtPlugin`]s (currently PixelBuds via the Maestro GATT +//! protocol), and publish the result as [`BtState`]. The module handler +//! exposes `connect`, `disconnect`, `cycle`, `menu_data`, `get_modes`, +//! `set_mode`, `cycle_mode` actions for the Waybar menu. + pub mod buds; pub mod maestro; @@ -14,15 +22,18 @@ use tracing::{error, warn}; use self::buds::{BtPlugin, PixelBudsPlugin}; +/// Background poller that syncs connected BlueZ devices into [`BtState`]. pub struct BtDaemon { session: Option, } impl BtDaemon { + /// Construct a new daemon. The BlueZ session is lazily created on first poll. pub fn new() -> Self { Self { session: None } } + /// Poll wrapper that logs + swallows errors so the outer loop keeps running. pub async fn poll( &mut self, tx: &watch::Sender, @@ -113,6 +124,8 @@ impl BtDaemon { static PLUGINS: LazyLock>> = LazyLock::new(|| vec![Box::new(PixelBudsPlugin)]); +/// After a user-initiated connect/disconnect, schedule a staircase of +/// forced polls so the UI catches up even if BlueZ is slow to settle. fn trigger_robust_poll(state: AppReceivers) { tokio::spawn(async move { for delay in [200, 500, 1000, 2000, 3000] { @@ -142,6 +155,7 @@ fn find_device<'a>(bt_state: &'a BtState, mac: &str) -> Option<&'a BtDeviceInfo> bt_state.devices.iter().find(|d| d.device_address == mac) } +/// Renders the current BT status + handles control actions. pub struct BtModule; impl WaybarModule for BtModule { @@ -194,7 +208,6 @@ impl WaybarModule for BtModule { "menu_data" => { let mut lines = Vec::new(); - // Connected devices for dev in &bt_state.devices { lines.push(format!( "CONNECTED:{}|{}", @@ -202,7 +215,7 @@ impl WaybarModule for BtModule { )); } - // Paired-but-not-connected devices + // Also surface paired-but-not-connected devices for the menu. if let Ok(session) = bluer::Session::new().await && let Ok(adapter) = session.default_adapter().await && let Ok(addresses) = adapter.device_addresses().await @@ -286,7 +299,6 @@ impl WaybarModule for BtModule { _ => {} } - // "show" and fallthrough if !bt_state.adapter_powered { return Ok(WaybarOutput { text: config.bt.format_disabled.clone(), diff --git a/src/modules/btrfs.rs b/src/modules/btrfs.rs index 3a268fc..6c39b9f 100644 --- a/src/modules/btrfs.rs +++ b/src/modules/btrfs.rs @@ -1,3 +1,6 @@ +//! Btrfs pool renderer: sums usage across all btrfs-typed mounts seen in the +//! `disks` watch channel. Dispatch-only (no dedicated poll task). + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -5,6 +8,8 @@ use crate::output::WaybarOutput; use crate::state::AppReceivers; use crate::utils::{TokenValue, classify_usage, format_template}; +/// Aggregates used/total across every mount whose filesystem name contains +/// `btrfs`. Emits `No BTRFS` when none are present. pub struct BtrfsModule; impl WaybarModule for BtrfsModule { diff --git a/src/modules/cpu.rs b/src/modules/cpu.rs index fd91578..4a7a290 100644 --- a/src/modules/cpu.rs +++ b/src/modules/cpu.rs @@ -1,3 +1,5 @@ +//! CPU usage + temperature renderer. Reads from the `cpu` watch channel. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -5,6 +7,7 @@ use crate::output::WaybarOutput; use crate::state::AppReceivers; use crate::utils::{TokenValue, classify_usage, format_template}; +/// Renders CPU usage/temp using [`CpuConfig::format`](crate::config::CpuConfig). pub struct CpuModule; impl WaybarModule for CpuModule { diff --git a/src/modules/disk.rs b/src/modules/disk.rs index f350e28..00b4e07 100644 --- a/src/modules/disk.rs +++ b/src/modules/disk.rs @@ -1,3 +1,5 @@ +//! Filesystem usage renderer. Args: `[mountpoint]` (default `/`). + use crate::config::Config; use crate::error::{FluxoError, Result}; use crate::modules::WaybarModule; @@ -5,6 +7,8 @@ use crate::output::WaybarOutput; use crate::state::AppReceivers; use crate::utils::{TokenValue, classify_usage, format_template}; +/// Renders used/total for a given mount point. Returns [`FluxoError::Module`] +/// if the mount point isn't present in the current disk snapshot. pub struct DiskModule; impl WaybarModule for DiskModule { diff --git a/src/modules/dnd.rs b/src/modules/dnd.rs index b48316f..d073fd8 100644 --- a/src/modules/dnd.rs +++ b/src/modules/dnd.rs @@ -1,3 +1,9 @@ +//! Do-Not-Disturb toggle + status for SwayNC or Dunst. +//! +//! SwayNC exposes a `dnd` property on its `org.erikreider.swaync.control` +//! interface that fires PropertiesChanged signals, so we subscribe. Dunst has +//! no change signal for its `paused` property, so we fall back to a 2 s poll. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -11,6 +17,7 @@ use zbus::proxy; use zbus::zvariant::OwnedValue; use zbus::{Connection, fdo::PropertiesProxy}; +/// Renders + toggles DND state. Args: `["show"]` (default) or `["toggle"]`. pub struct DndModule; /// Read dunst's `paused` property via raw D-Bus call. @@ -61,7 +68,6 @@ impl WaybarModule for DndModule { message: format!("DBus connection failed: {}", e), })?; - // Try SwayNC if let Ok(proxy) = SwayncControlProxy::new(&connection).await && let Ok(is_dnd) = proxy.dnd().await { @@ -69,7 +75,6 @@ impl WaybarModule for DndModule { return Ok(WaybarOutput::default()); } - // Try Dunst via raw D-Bus if let Ok(is_paused) = dunst_get_paused(&connection).await { let _ = dunst_set_paused(&connection, !is_paused).await; return Ok(WaybarOutput::default()); @@ -101,6 +106,8 @@ impl WaybarModule for DndModule { } } +/// Background watcher that keeps [`DndState`] in sync with the active +/// notification daemon (SwayNC via signals, Dunst via polling). pub struct DndDaemon; #[proxy( @@ -116,10 +123,12 @@ trait SwayncControl { } impl DndDaemon { + /// Construct a new (stateless) daemon. pub fn new() -> Self { Self } + /// Spawn a supervised listen loop that reconnects with a 5 s backoff. pub fn start(&self, tx: watch::Sender) { tokio::spawn(async move { loop { @@ -136,7 +145,6 @@ impl DndDaemon { info!("Connected to D-Bus for DND monitoring"); - // Try SwayNC first (signal-based) if let Ok(proxy) = SwayncControlProxy::new(&connection).await && let Ok(is_dnd) = proxy.dnd().await { @@ -164,7 +172,8 @@ impl DndDaemon { return Err(anyhow::anyhow!("SwayNC DND stream ended")); } - // Try Dunst via raw D-Bus calls (bypasses zbus proxy issues) + // Dunst: raw D-Bus call avoids zbus proxy typing quirks with its + // non-standard `org.dunstproject.cmd0` interface. match dunst_get_paused(&connection).await { Ok(is_paused) => { info!("Found Dunst, using polling-based DND monitoring"); diff --git a/src/modules/game.rs b/src/modules/game.rs index a8e1fd0..84795c0 100644 --- a/src/modules/game.rs +++ b/src/modules/game.rs @@ -1,3 +1,6 @@ +//! Gamemode indicator. Queries Hyprland's animation setting over its IPC +//! socket; animations disabled => gamemode active. Dispatch-only. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -6,6 +9,7 @@ use crate::state::AppReceivers; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::UnixStream; +/// Renders a glyph depending on whether Hyprland animations are disabled. pub struct GameModule; impl WaybarModule for GameModule { @@ -38,6 +42,7 @@ impl WaybarModule for GameModule { } } +/// Send `cmd` to Hyprland's `.socket.sock` and return the response body. async fn hyprland_ipc(cmd: &str) -> Result { let path = crate::utils::get_hyprland_socket(".socket.sock")?; diff --git a/src/modules/gpu.rs b/src/modules/gpu.rs index 2c1ee76..c411d51 100644 --- a/src/modules/gpu.rs +++ b/src/modules/gpu.rs @@ -1,3 +1,6 @@ +//! GPU renderer. Picks a vendor-specific format string (AMD/Intel/NVIDIA) and +//! reads from the `gpu` watch channel. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -5,6 +8,7 @@ use crate::output::WaybarOutput; use crate::state::AppReceivers; use crate::utils::{TokenValue, classify_usage, format_template}; +/// Renders GPU usage / VRAM / temp using the per-vendor format from config. pub struct GpuModule; impl WaybarModule for GpuModule { diff --git a/src/modules/hardware.rs b/src/modules/hardware.rs index 22152a9..946d9f6 100644 --- a/src/modules/hardware.rs +++ b/src/modules/hardware.rs @@ -1,7 +1,16 @@ +//! Unified CPU/memory/sys/GPU/disk poller. +//! +//! CPU/memory/sys are sampled every fast tick (1 s). GPU polls every 5th fast +//! tick via [`poll_slow`], and disks every 10th (they rarely change). GPU +//! vendor is detected once by probing nvidia-smi / `/sys/class/drm/*`, then +//! cached so subsequent polls take the fast path. + use crate::state::{CpuState, DiskInfo, GpuState, MemoryState, SysState}; use sysinfo::{Components, Disks, System}; use tokio::sync::watch; +/// Long-lived hardware sampler. Holds the `sysinfo::System` handle so +/// successive refreshes can diff against prior samples. pub struct HardwareDaemon { sys: System, components: Components, @@ -11,6 +20,7 @@ pub struct HardwareDaemon { } impl HardwareDaemon { + /// Build a new daemon with an initial `sysinfo` snapshot. pub fn new() -> Self { let mut sys = System::new(); sys.refresh_cpu_usage(); @@ -21,10 +31,13 @@ impl HardwareDaemon { components, gpu_vendor: None, gpu_poll_counter: 0, - disk_poll_counter: 9, // Start at 9 to poll on the first tick + // Start at 9 so (counter + 1) % 10 == 0 on the first tick. + disk_poll_counter: 9, } } + /// Fast path: refresh CPU usage, memory, temperatures, load avg, uptime. + /// Called every daemon tick. pub async fn poll_fast( &mut self, cpu_tx: &watch::Sender, @@ -96,12 +109,13 @@ impl HardwareDaemon { let _ = sys_tx.send(sys); } + /// Slow path: GPU every 5 ticks, disks every 10 ticks. Each sub-poll + /// runs off the hot loop before any state is published. pub async fn poll_slow( &mut self, gpu_tx: &watch::Sender, disks_tx: &watch::Sender>, ) { - // 1. Gather GPU data outside of lock let mut gpu_state = crate::state::GpuState::default(); self.gpu_poll_counter = (self.gpu_poll_counter + 1) % 5; let should_poll_gpu = self.gpu_poll_counter == 0; @@ -109,7 +123,6 @@ impl HardwareDaemon { self.poll_gpu(&mut gpu_state).await; } - // 2. Gather Disk data outside of lock let mut disks_data = None; self.disk_poll_counter = (self.disk_poll_counter + 1) % 10; if self.disk_poll_counter == 0 { @@ -130,7 +143,6 @@ impl HardwareDaemon { ); } - // 3. Apply to state if should_poll_gpu { let _ = gpu_tx.send(gpu_state); } @@ -140,6 +152,7 @@ impl HardwareDaemon { } } + /// Dispatch to the cached vendor's probe, or run detection on first call. async fn poll_gpu(&mut self, gpu: &mut crate::state::GpuState) { gpu.active = false; @@ -154,7 +167,7 @@ impl HardwareDaemon { Self::poll_intel(gpu); } _ => { - // Detection pass: try each vendor, cache the first that responds. + // First run — probe each vendor and cache the first hit. Self::poll_nvidia(gpu).await; if gpu.active { self.gpu_vendor = Some("NVIDIA".to_string()); @@ -173,6 +186,7 @@ impl HardwareDaemon { } } + /// Shell out to `nvidia-smi --query-gpu=...` for utilization/VRAM/temp. async fn poll_nvidia(gpu: &mut crate::state::GpuState) { let Ok(output) = tokio::process::Command::new("nvidia-smi") .args([ @@ -202,6 +216,7 @@ impl HardwareDaemon { } } + /// Read amdgpu sysfs entries under `/sys/class/drm/card*/device`. fn poll_amd(gpu: &mut crate::state::GpuState) { for i in 0..=3 { let base = format!("/sys/class/drm/card{}/device", i); @@ -238,6 +253,8 @@ impl HardwareDaemon { } } + /// Read i915/xe sysfs `gt_cur_freq_mhz`; approximate "usage" as + /// current/max frequency since Intel has no direct utilization counter. fn poll_intel(gpu: &mut crate::state::GpuState) { for i in 0..=3 { let base = format!("/sys/class/drm/card{}/device", i); diff --git a/src/modules/keyboard.rs b/src/modules/keyboard.rs index d25c4eb..aa41d59 100644 --- a/src/modules/keyboard.rs +++ b/src/modules/keyboard.rs @@ -1,3 +1,7 @@ +//! Keyboard layout indicator backed by Hyprland's event socket +//! (`.socket2.sock`). Also seeds the initial layout by shelling out to +//! `hyprctl devices -j` once at startup. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -10,6 +14,7 @@ use tokio::net::UnixStream; use tokio::sync::watch; use tracing::{error, info}; +/// Renders the current keyboard layout from [`KeyboardState`]. pub struct KeyboardModule; impl WaybarModule for KeyboardModule { @@ -44,19 +49,22 @@ impl WaybarModule for KeyboardModule { } } +/// Background watcher that subscribes to `activelayout>>` events emitted by +/// Hyprland's event socket. pub struct KeyboardDaemon; impl KeyboardDaemon { + /// Construct a new (stateless) daemon. pub fn new() -> Self { Self } + /// Spawn a supervised listen loop that reconnects with a 5 s backoff. pub fn start(&self, tx: watch::Sender) { tokio::spawn(async move { loop { if let Err(e) = Self::listen_loop(&tx).await { error!("Keyboard layout listener error: {}", e); - // Fallback to waiting before reconnecting to prevent tight loop tokio::time::sleep(tokio::time::Duration::from_secs(5)).await; } } @@ -71,7 +79,6 @@ impl KeyboardDaemon { let reader = BufReader::new(stream); let mut lines = reader.lines(); - // Fetch initial layout natively via hyprctl if let Ok(output) = tokio::process::Command::new("hyprctl") .args(["devices", "-j"]) .output() @@ -80,7 +87,8 @@ impl KeyboardDaemon { && let Some(keyboards) = json.get("keyboards").and_then(|v| v.as_array()) && let Some(main_kb) = keyboards.last() { - // The last active one is usually the main one + // `keyboards.last()` is the most recently registered device, + // which is typically the main one for single-keyboard setups. if let Some(layout) = main_kb.get("active_keymap").and_then(|v| v.as_str()) { let _ = tx.send(KeyboardState { layout: layout.to_string(), @@ -89,8 +97,8 @@ impl KeyboardDaemon { } while let Ok(Some(line)) = lines.next_line().await { + // Event payload: `keyboard_name,layout_name`. if let Some(payload) = line.strip_prefix("activelayout>>") { - // payload format: keyboard_name,layout_name let parts: Vec<&str> = payload.splitn(2, ',').collect(); if parts.len() == 2 { let layout = parts[1].to_string(); diff --git a/src/modules/memory.rs b/src/modules/memory.rs index 29fc178..f012e96 100644 --- a/src/modules/memory.rs +++ b/src/modules/memory.rs @@ -1,3 +1,5 @@ +//! RAM usage renderer. Reads from the `memory` watch channel. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -5,6 +7,7 @@ use crate::output::WaybarOutput; use crate::state::AppReceivers; use crate::utils::{TokenValue, classify_usage, format_template}; +/// Renders used/total GB with usage classification for Waybar CSS. pub struct MemoryModule; impl WaybarModule for MemoryModule { diff --git a/src/modules/mpris.rs b/src/modules/mpris.rs index 34e9dbe..cc994d0 100644 --- a/src/modules/mpris.rs +++ b/src/modules/mpris.rs @@ -1,3 +1,11 @@ +//! MPRIS media player indicator. +//! +//! Subscribes to `PlaybackStatus` and `Metadata` property-changed streams on +//! the first `org.mpris.MediaPlayer2.*` name that appears on the session bus, +//! so the indicator is truly signal-driven (no 2 s polling). A 10 s heartbeat +//! verifies the player is still there. Optional marquee scrolling is driven +//! by [`mpris_scroll_ticker`] from [`crate::daemon`]. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -10,6 +18,7 @@ use tokio::time::Duration; use tracing::{debug, info}; use zbus::{Connection, proxy}; +/// Render the user's format string + derive the Waybar CSS class from state. fn format_mpris_text(format: &str, mpris: &MprisState) -> (String, &'static str) { let status_icon = if mpris.is_playing { "󰏤" @@ -40,6 +49,7 @@ fn format_mpris_text(format: &str, mpris: &MprisState) -> (String, &'static str) (text, class) } +/// Return a cyclic `max_len`-wide window over `full_text + separator`. fn apply_scroll_window(full_text: &str, max_len: usize, offset: usize, separator: &str) -> String { let char_count = full_text.chars().count(); let total_len = char_count + separator.chars().count(); @@ -53,6 +63,7 @@ fn apply_scroll_window(full_text: &str, max_len: usize, offset: usize, separator .collect() } +/// Truncate `text` to `max_len` chars, appending `...` when cut. fn truncate_with_ellipsis(text: &str, max_len: usize) -> String { let char_count = text.chars().count(); if char_count <= max_len { @@ -62,6 +73,7 @@ fn truncate_with_ellipsis(text: &str, max_len: usize) -> String { format!("{}...", truncated) } +/// Renders the current player state, applying scroll/truncate per config. pub struct MprisModule; impl WaybarModule for MprisModule { @@ -111,6 +123,9 @@ impl WaybarModule for MprisModule { } } +/// Drive the marquee animation: advance the offset every `scroll_speed` ms +/// while a track is playing, and emit a fresh generation on `tick_tx` so the +/// mpris signaler arm fires. Resets offset when the track changes. pub async fn mpris_scroll_ticker( config: Arc>, mut mpris_rx: watch::Receiver, @@ -152,13 +167,15 @@ pub async fn mpris_scroll_ticker( continue; } - // Not scrolling — wait for next state change + // Not scrolling — sleep until the next player state change. if mpris_rx.changed().await.is_err() { break; } } } +/// Background watcher that discovers the active MPRIS player and mirrors +/// its `PlaybackStatus` + `Metadata` properties into [`MprisState`]. pub struct MprisDaemon; #[proxy( @@ -185,10 +202,12 @@ trait MprisPlayer { } impl MprisDaemon { + /// Construct a new (stateless) daemon. pub fn new() -> Self { Self } + /// Spawn a supervised listen loop with a 5 s reconnect backoff. pub fn start(&self, tx: watch::Sender) { tokio::spawn(async move { loop { @@ -209,7 +228,6 @@ impl MprisDaemon { let dbus_proxy = DBusProxy::new(&connection).await?; loop { - // Discovery pass: find an active MPRIS player. let names = dbus_proxy.list_names().await?; let active_player = names .into_iter() @@ -217,7 +235,6 @@ impl MprisDaemon { let Some(player_name) = active_player else { send_stopped_if_changed(tx); - // No player — wait and re-discover. tokio::time::sleep(Duration::from_secs(5)).await; continue; }; @@ -234,7 +251,6 @@ impl MprisDaemon { } }; - // Initial fetch and then signal-driven updates via PropertiesChanged. update_from_player(&player_proxy, tx).await; let mut status_stream = player_proxy.receive_playback_status_changed().await; @@ -249,7 +265,7 @@ impl MprisDaemon { update_from_player(&player_proxy, tx).await; } _ = tokio::time::sleep(Duration::from_secs(10)) => { - // Heartbeat: verify the player is still on the bus. + // Heartbeat: re-check that the player name is still owned. let current = dbus_proxy.list_names().await.unwrap_or_default(); if !current.iter().any(|n| n == &player_name) { break; @@ -261,6 +277,8 @@ impl MprisDaemon { } } +/// Fetch `PlaybackStatus` + `Metadata` and publish only when they differ +/// from the previous [`MprisState`] (to avoid spurious watch wake-ups). async fn update_from_player(player: &MprisPlayerProxy<'_>, tx: &watch::Sender) { let status = player.playback_status().await.unwrap_or_default(); let metadata = player.metadata().await.unwrap_or_default(); @@ -291,6 +309,7 @@ async fn update_from_player(player: &MprisPlayerProxy<'_>, tx: &watch::Sender>, ) -> (String, String, String) { @@ -325,6 +344,8 @@ fn parse_metadata( (artist, title, album) } +/// Publish a cleared/stopped [`MprisState`] if the current state isn't already +/// that. Called when no player is on the bus. fn send_stopped_if_changed(tx: &watch::Sender) { let current = tx.borrow(); if !current.is_stopped || !current.title.is_empty() { diff --git a/src/modules/network.rs b/src/modules/network.rs index 10e0809..65e5f35 100644 --- a/src/modules/network.rs +++ b/src/modules/network.rs @@ -1,3 +1,10 @@ +//! Primary-interface throughput renderer + polling daemon. +//! +//! The daemon picks the interface with the longest-prefix default route (see +//! [`get_primary_interface`]) and computes rx/tx rates as byte-count deltas +//! between successive polls. Well-known VPN interface prefixes get a lock +//! glyph prepended to the rendered text. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -9,8 +16,10 @@ use std::fs; use std::time::{SystemTime, UNIX_EPOCH}; use tokio::sync::watch; +/// Renders interface / IP / rx / tx for the detected primary route. pub struct NetworkModule; +/// Background poller that tracks byte counters across ticks to derive rates. pub struct NetworkDaemon { last_time: u64, last_rx_bytes: u64, @@ -22,6 +31,7 @@ pub struct NetworkDaemon { type PollResult = crate::error::Result<(String, Option, Option<(u64, u64)>)>; impl NetworkDaemon { + /// Build a fresh daemon with no prior byte-count samples. pub fn new() -> Self { Self { last_time: 0, @@ -32,6 +42,9 @@ impl NetworkDaemon { } } + /// Detect the primary interface, read `/sys/class/net/*/statistics`, and + /// publish a new [`NetworkState`] onto `state_tx`. Interface/byte reads + /// run via [`tokio::task::spawn_blocking`] so the runtime isn't starved. pub async fn poll( &mut self, state_tx: &watch::Sender, @@ -56,7 +69,6 @@ impl NetworkDaemon { } else { self.cached_interface = None; self.cached_ip = None; - // Provide a default state for "No connection" let mut network = state_tx.borrow().clone(); network.interface.clear(); network.ip.clear(); @@ -71,7 +83,6 @@ impl NetworkDaemon { let interface = if let Some(ref interface) = self.cached_interface { interface.clone() } else { - // No interface detected let mut network = state_tx.borrow().clone(); network.interface.clear(); network.ip.clear(); @@ -107,7 +118,7 @@ impl NetworkDaemon { network.ip = self.cached_ip.clone().unwrap_or_default(); let _ = state_tx.send(network); } else { - // First poll: no speed data yet, but update interface/ip + // First poll has no prior sample — publish iface/ip only. let mut network = state_tx.borrow().clone(); network.interface = interface.clone(); network.ip = self.cached_ip.clone().unwrap_or_default(); @@ -118,7 +129,6 @@ impl NetworkDaemon { self.last_rx_bytes = rx_bytes_now; self.last_tx_bytes = tx_bytes_now; } else { - // Read failed, might be down self.cached_interface = None; return Err(crate::error::FluxoError::Network(format!( "Failed to read bytes for {}", @@ -182,6 +192,8 @@ impl WaybarModule for NetworkModule { } } +/// Parse `/proc/net/route` to find the default-route interface. When several +/// defaults exist, prefer the one with the longest netmask, then lowest metric. fn get_primary_interface() -> Result { let content = std::fs::read_to_string("/proc/net/route")?; @@ -200,7 +212,6 @@ fn get_primary_interface() -> Result { } } - // Sort by mask descending (longest prefix match first), then by metric ascending defaults.sort_by(|a, b| b.0.cmp(&a.0).then(a.1.cmp(&b.1))); if let Some((_, _, dev)) = defaults.first() { Ok(dev.clone()) @@ -209,6 +220,7 @@ fn get_primary_interface() -> Result { } } +/// First IPv4 address for `interface`, via `getifaddrs`. `None` if absent. fn get_ip_address(interface: &str) -> Option { let addrs = getifaddrs().ok()?; for ifaddr in addrs { @@ -222,6 +234,7 @@ fn get_ip_address(interface: &str) -> Option { None } +/// Read `(rx_bytes, tx_bytes)` counters from sysfs for `interface`. fn get_bytes(interface: &str) -> Result<(u64, u64)> { let rx_path = format!("/sys/class/net/{}/statistics/rx_bytes", interface); let tx_path = format!("/sys/class/net/{}/statistics/tx_bytes", interface); diff --git a/src/modules/power.rs b/src/modules/power.rs index af2524a..1bac9ef 100644 --- a/src/modules/power.rs +++ b/src/modules/power.rs @@ -1,3 +1,6 @@ +//! Battery/AC indicator via `/sys/class/power_supply`. Dispatch-only - reads +//! sysfs on demand rather than polling into a watch channel. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -6,6 +9,7 @@ use crate::state::AppReceivers; use crate::utils::{TokenValue, format_template}; use std::fs; +/// Renders battery percentage + charge state (critical/warning/bat/charging/ac). pub struct PowerModule; impl WaybarModule for PowerModule { @@ -18,7 +22,6 @@ impl WaybarModule for PowerModule { let critical_threshold = 15; let warning_threshold = 50; - // Find the first battery let mut battery_path = None; if let Ok(entries) = fs::read_dir("/sys/class/power_supply") { for entry in entries.flatten() { @@ -30,7 +33,6 @@ impl WaybarModule for PowerModule { } } - // Check AC status let mut ac_online = false; if let Ok(entries) = fs::read_dir("/sys/class/power_supply") { for entry in entries.flatten() { diff --git a/src/modules/sys.rs b/src/modules/sys.rs index ad0866b..b7e6aae 100644 --- a/src/modules/sys.rs +++ b/src/modules/sys.rs @@ -1,3 +1,5 @@ +//! Uptime + load average renderer. Reads from the `sys` watch channel. + use crate::config::Config; use crate::error::Result; use crate::modules::WaybarModule; @@ -5,6 +7,7 @@ use crate::output::WaybarOutput; use crate::state::AppReceivers; use crate::utils::{TokenValue, format_template}; +/// Renders uptime and load averages with a detailed tooltip. pub struct SysModule; impl WaybarModule for SysModule {