This commit is contained in:
+21
-8
@@ -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<AudioState>,
|
||||
@@ -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<AudioState>,
|
||||
) -> 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<AudioState>) {
|
||||
let tx = state_tx.clone();
|
||||
let pending = PendingList::new();
|
||||
@@ -313,6 +323,8 @@ fn fetch_sinks(context: &mut Context, state_tx: &watch::Sender<AudioState>) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 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<AudioState>) {
|
||||
let tx = state_tx.clone();
|
||||
let pending = PendingList::new();
|
||||
@@ -348,6 +360,8 @@ fn fetch_sources(context: &mut Context, state_tx: &watch::Sender<AudioState>) {
|
||||
});
|
||||
}
|
||||
|
||||
/// 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()
|
||||
|
||||
@@ -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<BacklightState>) {
|
||||
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<Event>| {
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Vec<(String, TokenValue)>>>;
|
||||
/// List of mode identifiers the plugin can switch between.
|
||||
fn get_modes(
|
||||
&self,
|
||||
mac: &str,
|
||||
state: &AppReceivers,
|
||||
) -> BoxFuture<'static, FluxoResult<Vec<String>>>;
|
||||
/// 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 {
|
||||
|
||||
@@ -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<u8>,
|
||||
@@ -24,28 +32,35 @@ pub struct BudsStatus {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<Mutex<HashMap<String, BudsStatus>>>,
|
||||
management_tx: mpsc::UnboundedSender<ManagerCommand>,
|
||||
}
|
||||
|
||||
impl MaestroManager {
|
||||
/// Spawn the management thread + runtime and return a handle.
|
||||
pub fn new(state: AppReceivers) -> Self {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel::<ManagerCommand>();
|
||||
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<Mutex<HashMap<String, BudsStatus>>>,
|
||||
@@ -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<MaestroManager> = 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()))
|
||||
}
|
||||
|
||||
+15
-3
@@ -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<bluer::Session>,
|
||||
}
|
||||
|
||||
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<BtState>,
|
||||
@@ -113,6 +124,8 @@ impl BtDaemon {
|
||||
static PLUGINS: LazyLock<Vec<Box<dyn BtPlugin>>> =
|
||||
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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+13
-4
@@ -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<DndState>) {
|
||||
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");
|
||||
|
||||
@@ -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<String> {
|
||||
let path = crate::utils::get_hyprland_socket(".socket.sock")?;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+22
-5
@@ -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<CpuState>,
|
||||
@@ -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<GpuState>,
|
||||
disks_tx: &watch::Sender<Vec<DiskInfo>>,
|
||||
) {
|
||||
// 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);
|
||||
|
||||
+12
-4
@@ -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<KeyboardState>) {
|
||||
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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+26
-5
@@ -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<RwLock<Config>>,
|
||||
mut mpris_rx: watch::Receiver<MprisState>,
|
||||
@@ -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<MprisState>) {
|
||||
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<MprisState>) {
|
||||
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<Mp
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract `xesam:artist` (string or array), `xesam:title`, `xesam:album`.
|
||||
fn parse_metadata(
|
||||
metadata: &std::collections::HashMap<String, zbus::zvariant::Value<'_>>,
|
||||
) -> (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<MprisState>) {
|
||||
let current = tx.borrow();
|
||||
if !current.is_stopped || !current.title.is_empty() {
|
||||
|
||||
+18
-5
@@ -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<String>, 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<NetworkState>,
|
||||
@@ -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<String> {
|
||||
let content = std::fs::read_to_string("/proc/net/route")?;
|
||||
|
||||
@@ -200,7 +212,6 @@ fn get_primary_interface() -> Result<String> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// First IPv4 address for `interface`, via `getifaddrs`. `None` if absent.
|
||||
fn get_ip_address(interface: &str) -> Option<String> {
|
||||
let addrs = getifaddrs().ok()?;
|
||||
for ifaddr in addrs {
|
||||
@@ -222,6 +234,7 @@ fn get_ip_address(interface: &str) -> Option<String> {
|
||||
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);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user