diff --git a/example.config.toml b/example.config.toml new file mode 100644 index 0000000..de4a4df --- /dev/null +++ b/example.config.toml @@ -0,0 +1,44 @@ +# fluxo-rs example configuration +# place this at ~/.config/fluxo/config.toml + +# network module (net) +# tokens: {interface}, {ip}, {rx}, {tx}, {rx:>5.2}, {tx:>5.2} +[network] +format = "{interface} ({ip}):  {rx:>5.2} MB/s  {tx:>5.2} MB/s" + +# cpu module (cpu) +# tokens: {usage}, {temp}, {usage:>4.1}, {temp:>4.1} +[cpu] +format = "CPU: {usage:>4.1}% {temp:>4.1}C" + +# memory module (mem) +# tokens: {used}, {total}, {used:>5.2}, {total:>5.2} +[memory] +format = "{used:>5.2}/{total:>5.2}GB" + +# gpu module (gpu) +# tokens: {usage}, {vram_used}, {vram_total}, {temp}, {usage:>3.0}, {vram_used:>4.1}, {vram_total:>4.1}, {temp:>4.1} +[gpu] +format_amd = "AMD: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C" +format_intel = "iGPU: {usage:>3.0}%" +format_nvidia = "NV: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C" + +# system module (sys) +# tokens: {uptime}, {load1}, {load5}, {load15}, {load1:>4.2}, {load5:>4.2}, {load15:>4.2} +[sys] +format = "UP: {uptime} | LOAD: {load1:>4.2} {load5:>4.2} {load15:>4.2}" + +# disk module (disk ) +# tokens: {mount}, {used}, {total}, {used:>5.1}, {total:>5.1} +[disk] +format = "{mount} {used:>5.1}/{total:>5.1}G" + +# pool module (pool / btrfs) +# tokens: {used}, {total}, {used:>4.0}, {total:>4.0} +[pool] +format = "{used:>4.0}G / {total:>4.0}G" + +# power/battery module (power) +# tokens: {percentage}, {icon}, {percentage:>3} +[power] +format = "{percentage:>3}% {icon}" diff --git a/src/config.rs b/src/config.rs index 97bf934..2b9f12d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,20 @@ use std::path::PathBuf; pub struct Config { #[serde(default)] pub network: NetworkConfig, + #[serde(default)] + pub cpu: CpuConfig, + #[serde(default)] + pub memory: MemoryConfig, + #[serde(default)] + pub gpu: GpuConfig, + #[serde(default)] + pub sys: SysConfig, + #[serde(default)] + pub disk: DiskConfig, + #[serde(default)] + pub pool: PoolConfig, + #[serde(default)] + pub power: PowerConfig, } #[derive(Deserialize)] @@ -16,7 +30,102 @@ pub struct NetworkConfig { impl Default for NetworkConfig { fn default() -> Self { Self { - format: "{interface} ({ip}):  {rx} MB/s  {tx} MB/s".to_string(), + format: "{interface} ({ip}):  {rx:>5.2} MB/s  {tx:>5.2} MB/s".to_string(), + } + } +} + +#[derive(Deserialize)] +pub struct CpuConfig { + pub format: String, +} + +impl Default for CpuConfig { + fn default() -> Self { + Self { + format: "CPU: {usage:>4.1}% {temp:>4.1}C".to_string(), + } + } +} + +#[derive(Deserialize)] +pub struct MemoryConfig { + pub format: String, +} + +impl Default for MemoryConfig { + fn default() -> Self { + Self { + format: "{used:>5.2}/{total:>5.2}GB".to_string(), + } + } +} + +#[derive(Deserialize)] +pub struct GpuConfig { + pub format_amd: String, + pub format_intel: String, + pub format_nvidia: String, +} + +impl Default for GpuConfig { + fn default() -> Self { + Self { + format_amd: "AMD: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C".to_string(), + format_intel: "iGPU: {usage:>3.0}%".to_string(), + format_nvidia: "NV: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C".to_string(), + } + } +} + +#[derive(Deserialize)] +pub struct SysConfig { + pub format: String, +} + +impl Default for SysConfig { + fn default() -> Self { + Self { + format: "UP: {uptime} | LOAD: {load1:>4.2} {load5:>4.2} {load15:>4.2}".to_string(), + } + } +} + +#[derive(Deserialize)] +pub struct DiskConfig { + pub format: String, +} + +impl Default for DiskConfig { + fn default() -> Self { + Self { + format: "{mount} {used:>5.1}/{total:>5.1}G".to_string(), + } + } +} + +#[derive(Deserialize)] +pub struct PoolConfig { + pub format: String, +} + +impl Default for PoolConfig { + fn default() -> Self { + Self { + format: "{used:>4.0}G / {total:>4.0}G".to_string(), + } + } +} + +#[derive(Deserialize)] +pub struct PowerConfig { + pub format: String, +} + +impl Default for PowerConfig { + fn default() -> Self { + Self { + format: "{percentage:>3}% {icon}".to_string(), } } } diff --git a/src/modules/btrfs.rs b/src/modules/btrfs.rs index 242bfbc..3496605 100644 --- a/src/modules/btrfs.rs +++ b/src/modules/btrfs.rs @@ -8,7 +8,7 @@ use sysinfo::Disks; pub struct BtrfsModule; impl WaybarModule for BtrfsModule { - fn run(&self, _config: &Config, _state: &SharedState, _args: &[&str]) -> Result { + fn run(&self, config: &Config, _state: &SharedState, _args: &[&str]) -> Result { let disks = Disks::new_with_refreshed_list(); let mut total_used: f64 = 0.0; let mut total_size: f64 = 0.0; @@ -43,8 +43,14 @@ impl WaybarModule for BtrfsModule { "normal" }; + let text = config.pool.format + .replace("{used:>4.0}", &format!("{:>4.0}", used_gb)) + .replace("{total:>4.0}", &format!("{:>4.0}", size_gb)) + .replace("{used}", &format!("{:.0}", used_gb)) + .replace("{total}", &format!("{:.0}", size_gb)); + Ok(WaybarOutput { - text: format!("{:.0}G / {:.0}G", used_gb, size_gb), + text, tooltip: Some(format!("BTRFS Usage: {:.1}%", percentage)), class: Some(class.to_string()), percentage: Some(percentage as u8), diff --git a/src/modules/cpu.rs b/src/modules/cpu.rs index a885303..b079b3e 100644 --- a/src/modules/cpu.rs +++ b/src/modules/cpu.rs @@ -7,7 +7,7 @@ use anyhow::Result; pub struct CpuModule; impl WaybarModule for CpuModule { - fn run(&self, _config: &Config, state: &SharedState, _args: &[&str]) -> Result { + fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result { let (usage, temp, model) = { if let Ok(state_lock) = state.read() { ( @@ -20,7 +20,11 @@ impl WaybarModule for CpuModule { } }; - let text = format!("{:.1}% {:.1}C", usage, temp); + let text = config.cpu.format + .replace("{usage:>4.1}", &format!("{:>4.1}", usage)) + .replace("{temp:>4.1}", &format!("{:>4.1}", temp)) + .replace("{usage}", &format!("{:.1}", usage)) + .replace("{temp}", &format!("{:.1}", temp)); let class = if usage > 95.0 { "max" @@ -31,7 +35,7 @@ impl WaybarModule for CpuModule { }; Ok(WaybarOutput { - text: format!("CPU: {}", text), + text, tooltip: Some(model), class: Some(class.to_string()), percentage: Some(usage as u8), diff --git a/src/modules/disk.rs b/src/modules/disk.rs index 8664e4c..f28a545 100644 --- a/src/modules/disk.rs +++ b/src/modules/disk.rs @@ -8,7 +8,7 @@ use sysinfo::Disks; pub struct DiskModule; impl WaybarModule for DiskModule { - fn run(&self, _config: &Config, _state: &SharedState, args: &[&str]) -> Result { + fn run(&self, config: &Config, _state: &SharedState, args: &[&str]) -> Result { let mountpoint = args.first().unwrap_or(&"/"); let disks = Disks::new_with_refreshed_list(); @@ -32,8 +32,15 @@ impl WaybarModule for DiskModule { "normal" }; + let text = config.disk.format + .replace("{mount}", mountpoint) + .replace("{used:>5.1}", &format!("{:>5.1}", used_gb)) + .replace("{total:>5.1}", &format!("{:>5.1}", total_gb)) + .replace("{used}", &format!("{:.1}", used_gb)) + .replace("{total}", &format!("{:.1}", total_gb)); + return Ok(WaybarOutput { - text: format!("{} {:.1}G/{:.1}G", mountpoint, used_gb, total_gb), + text, tooltip: Some(format!("Used: {:.1}G\nTotal: {:.1}G\nFree: {:.1}G", used_gb, total_gb, free_gb)), class: Some(class.to_string()), percentage: Some(percentage as u8), diff --git a/src/modules/gpu.rs b/src/modules/gpu.rs index a6c7a7c..5b5d67a 100644 --- a/src/modules/gpu.rs +++ b/src/modules/gpu.rs @@ -7,7 +7,7 @@ use anyhow::Result; pub struct GpuModule; impl WaybarModule for GpuModule { - fn run(&self, _config: &Config, state: &SharedState, _args: &[&str]) -> Result { + fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result { let (active, vendor, usage, vram_used, vram_total, temp, model) = { if let Ok(state_lock) = state.read() { ( @@ -41,13 +41,22 @@ impl WaybarModule for GpuModule { "normal" }; - let text = if vendor == "Intel" { - // Intel usually doesn't expose easy VRAM or Temp without root - format!("iGPU: {:.0}%", usage) - } else { - format!("{}: {:.0}% {:.1}/{:.1}GB {:.1}C", vendor, usage, vram_used, vram_total, temp) + let format_str = match vendor.as_str() { + "Intel" => &config.gpu.format_intel, + "NVIDIA" => &config.gpu.format_nvidia, + _ => &config.gpu.format_amd, }; + let text = format_str + .replace("{usage:>3.0}", &format!("{:>3.0}", usage)) + .replace("{vram_used:>4.1}", &format!("{:>4.1}", vram_used)) + .replace("{vram_total:>4.1}", &format!("{:>4.1}", vram_total)) + .replace("{temp:>4.1}", &format!("{:>4.1}", temp)) + .replace("{usage}", &format!("{:.0}", usage)) + .replace("{vram_used}", &format!("{:.1}", vram_used)) + .replace("{vram_total}", &format!("{:.1}", vram_total)) + .replace("{temp}", &format!("{:.1}", temp)); + let tooltip = if vendor == "Intel" { format!("Model: {}\nApprox Usage: {:.0}%", model, usage) } else { diff --git a/src/modules/hardware.rs b/src/modules/hardware.rs index 94d3fec..07d0081 100644 --- a/src/modules/hardware.rs +++ b/src/modules/hardware.rs @@ -4,6 +4,7 @@ use sysinfo::{Components, System}; pub struct HardwareDaemon { sys: System, components: Components, + gpu_vendor: Option, } impl HardwareDaemon { @@ -11,19 +12,17 @@ impl HardwareDaemon { let mut sys = System::new_all(); sys.refresh_all(); let components = Components::new_with_refreshed_list(); - Self { sys, components } + Self { sys, components, gpu_vendor: None } } pub fn poll(&mut self, state: SharedState) { self.sys.refresh_cpu_usage(); self.sys.refresh_memory(); - self.sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true); self.components.refresh(true); let cpu_usage = self.sys.global_cpu_usage(); let cpu_model = self.sys.cpus().first().map(|c| c.brand().to_string()).unwrap_or_else(|| "Unknown".to_string()); - // Try to find a reasonable CPU temperature let mut cpu_temp = 0.0; for component in &self.components { let label = component.label().to_lowercase(); @@ -41,7 +40,17 @@ impl HardwareDaemon { let load_avg = System::load_average(); let uptime = System::uptime(); - let process_count = self.sys.processes().len(); + + // Fast O(1) process count by reading loadavg instead of heavy sysinfo process refresh + let mut process_count = 0; + if let Ok(loadavg_str) = std::fs::read_to_string("/proc/loadavg") { + let parts: Vec<&str> = loadavg_str.split_whitespace().collect(); + if parts.len() >= 4 { + if let Some(total_procs) = parts[3].split('/').nth(1) { + process_count = total_procs.parse().unwrap_or(0); + } + } + } if let Ok(mut state_lock) = state.write() { state_lock.cpu.usage = cpu_usage as f64; @@ -61,92 +70,100 @@ impl HardwareDaemon { } } - fn poll_gpu(&self, gpu: &mut crate::state::GpuState) { + fn poll_gpu(&mut self, gpu: &mut crate::state::GpuState) { gpu.active = false; - // 1. Try NVIDIA via nvidia-smi - if let Ok(output) = std::process::Command::new("nvidia-smi") - .args(["--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu,name", "--format=csv,noheader,nounits"]) - .output() - { - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - if let Some(line) = stdout.lines().next() { - let parts: Vec<&str> = line.split(',').collect(); - if parts.len() >= 5 { - gpu.active = true; - gpu.vendor = "NVIDIA".to_string(); - gpu.usage = parts[0].trim().parse().unwrap_or(0.0); - gpu.vram_used = parts[1].trim().parse::().unwrap_or(0.0) / 1024.0; // from MB to GB - gpu.vram_total = parts[2].trim().parse::().unwrap_or(0.0) / 1024.0; - gpu.temp = parts[3].trim().parse().unwrap_or(0.0); - gpu.model = parts[4].trim().to_string(); - return; + // Fast path: if we already detected NVIDIA, don't fallback to AMD/Intel scanning + if self.gpu_vendor.as_deref() == Some("NVIDIA") || self.gpu_vendor.is_none() { + if let Ok(output) = std::process::Command::new("nvidia-smi") + .args(["--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu,name", "--format=csv,noheader,nounits"]) + .output() + { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + if let Some(line) = stdout.lines().next() { + let parts: Vec<&str> = line.split(',').collect(); + if parts.len() >= 5 { + gpu.active = true; + gpu.vendor = "NVIDIA".to_string(); + gpu.usage = parts[0].trim().parse().unwrap_or(0.0); + gpu.vram_used = parts[1].trim().parse::().unwrap_or(0.0) / 1024.0; + gpu.vram_total = parts[2].trim().parse::().unwrap_or(0.0) / 1024.0; + gpu.temp = parts[3].trim().parse().unwrap_or(0.0); + gpu.model = parts[4].trim().to_string(); + self.gpu_vendor = Some("NVIDIA".to_string()); + return; + } } } } } - // Iterate over sysfs for AMD or Intel - for i in 0..=3 { - let base = format!("/sys/class/drm/card{}/device", i); - - // 2. Try AMD - if let Ok(usage_str) = std::fs::read_to_string(format!("{}/gpu_busy_percent", base)) { - gpu.active = true; - gpu.vendor = "AMD".to_string(); - gpu.usage = usage_str.trim().parse().unwrap_or(0.0); + // Fast path: if we detected AMD or Intel, scan sysfs + if self.gpu_vendor.as_deref() == Some("AMD") || self.gpu_vendor.as_deref() == Some("Intel") || self.gpu_vendor.is_none() { + for i in 0..=3 { + let base = format!("/sys/class/drm/card{}/device", i); - if let Ok(mem_used) = std::fs::read_to_string(format!("{}/mem_info_vram_used", base)) { - gpu.vram_used = mem_used.trim().parse::().unwrap_or(0.0) / 1024.0 / 1024.0 / 1024.0; + if self.gpu_vendor.as_deref() == Some("AMD") || self.gpu_vendor.is_none() { + if let Ok(usage_str) = std::fs::read_to_string(format!("{}/gpu_busy_percent", base)) { + gpu.active = true; + gpu.vendor = "AMD".to_string(); + gpu.usage = usage_str.trim().parse().unwrap_or(0.0); + + if let Ok(mem_used) = std::fs::read_to_string(format!("{}/mem_info_vram_used", base)) { + gpu.vram_used = mem_used.trim().parse::().unwrap_or(0.0) / 1024.0 / 1024.0 / 1024.0; + } + if let Ok(mem_total) = std::fs::read_to_string(format!("{}/mem_info_vram_total", base)) { + gpu.vram_total = mem_total.trim().parse::().unwrap_or(0.0) / 1024.0 / 1024.0 / 1024.0; + } + + if let Ok(entries) = std::fs::read_dir(format!("{}/hwmon", base)) { + for entry in entries.flatten() { + let temp_path = entry.path().join("temp1_input"); + if let Ok(temp_str) = std::fs::read_to_string(temp_path) { + gpu.temp = temp_str.trim().parse::().unwrap_or(0.0) / 1000.0; + break; + } + } + } + gpu.model = "AMD GPU".to_string(); + self.gpu_vendor = Some("AMD".to_string()); + return; + } } - if let Ok(mem_total) = std::fs::read_to_string(format!("{}/mem_info_vram_total", base)) { - gpu.vram_total = mem_total.trim().parse::().unwrap_or(0.0) / 1024.0 / 1024.0 / 1024.0; - } - - if let Ok(entries) = std::fs::read_dir(format!("{}/hwmon", base)) { - for entry in entries.flatten() { - let temp_path = entry.path().join("temp1_input"); - if let Ok(temp_str) = std::fs::read_to_string(temp_path) { - gpu.temp = temp_str.trim().parse::().unwrap_or(0.0) / 1000.0; - break; + + if self.gpu_vendor.as_deref() == Some("Intel") || self.gpu_vendor.is_none() { + let freq_path = if std::path::Path::new(&format!("{}/gt_cur_freq_mhz", base)).exists() { + Some(format!("{}/gt_cur_freq_mhz", base)) + } else if std::path::Path::new(&format!("/sys/class/drm/card{}/gt_cur_freq_mhz", i)).exists() { + Some(format!("/sys/class/drm/card{}/gt_cur_freq_mhz", i)) + } else { + None + }; + + if let Some(path) = freq_path { + if let Ok(freq_str) = std::fs::read_to_string(&path) { + gpu.active = true; + gpu.vendor = "Intel".to_string(); + + let cur_freq = freq_str.trim().parse::().unwrap_or(0.0); + let mut max_freq = 0.0; + + let max_path = path.replace("gt_cur_freq_mhz", "gt_max_freq_mhz"); + if let Ok(max_str) = std::fs::read_to_string(max_path) { + max_freq = max_str.trim().parse::().unwrap_or(0.0); + } + + gpu.usage = if max_freq > 0.0 { (cur_freq / max_freq) * 100.0 } else { 0.0 }; + gpu.temp = 0.0; + gpu.vram_used = 0.0; + gpu.vram_total = 0.0; + gpu.model = format!("Intel iGPU ({}MHz)", cur_freq); + self.gpu_vendor = Some("Intel".to_string()); + return; } } } - gpu.model = "AMD GPU".to_string(); - return; - } - - // 3. Try Intel (iGPU) - let freq_path = if std::path::Path::new(&format!("{}/gt_cur_freq_mhz", base)).exists() { - Some(format!("{}/gt_cur_freq_mhz", base)) - } else if std::path::Path::new(&format!("/sys/class/drm/card{}/gt_cur_freq_mhz", i)).exists() { - Some(format!("/sys/class/drm/card{}/gt_cur_freq_mhz", i)) - } else { - None - }; - - if let Some(path) = freq_path { - if let Ok(freq_str) = std::fs::read_to_string(&path) { - gpu.active = true; - gpu.vendor = "Intel".to_string(); - - let cur_freq = freq_str.trim().parse::().unwrap_or(0.0); - let mut max_freq = 0.0; - - let max_path = path.replace("gt_cur_freq_mhz", "gt_max_freq_mhz"); - if let Ok(max_str) = std::fs::read_to_string(max_path) { - max_freq = max_str.trim().parse::().unwrap_or(0.0); - } - - // Approximate usage via frequency scaling - gpu.usage = if max_freq > 0.0 { (cur_freq / max_freq) * 100.0 } else { 0.0 }; - gpu.temp = 0.0; // Intel iGPU temps are usually tied to CPU package temp - gpu.vram_used = 0.0; // iGPU shares system memory - gpu.vram_total = 0.0; - gpu.model = format!("Intel iGPU ({}MHz)", cur_freq); - return; - } } } } diff --git a/src/modules/memory.rs b/src/modules/memory.rs index c949aa8..42c587a 100644 --- a/src/modules/memory.rs +++ b/src/modules/memory.rs @@ -7,7 +7,7 @@ use anyhow::Result; pub struct MemoryModule; impl WaybarModule for MemoryModule { - fn run(&self, _config: &Config, state: &SharedState, _args: &[&str]) -> Result { + fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result { let (used_gb, total_gb) = { if let Ok(state_lock) = state.read() { ( @@ -21,6 +21,12 @@ impl WaybarModule for MemoryModule { let ratio = if total_gb > 0.0 { (used_gb / total_gb) * 100.0 } else { 0.0 }; + let text = config.memory.format + .replace("{used:>5.2}", &format!("{:>5.2}", used_gb)) + .replace("{total:>5.2}", &format!("{:>5.2}", total_gb)) + .replace("{used}", &format!("{:.2}", used_gb)) + .replace("{total}", &format!("{:.2}", total_gb)); + let class = if ratio > 95.0 { "max" } else if ratio > 75.0 { @@ -30,7 +36,7 @@ impl WaybarModule for MemoryModule { }; Ok(WaybarOutput { - text: format!("{:.2}/{:.2}GB", used_gb, total_gb), + text, tooltip: None, class: Some(class.to_string()), percentage: Some(ratio as u8), diff --git a/src/modules/network.rs b/src/modules/network.rs index e66d2fe..c0ccb2e 100644 --- a/src/modules/network.rs +++ b/src/modules/network.rs @@ -13,6 +13,8 @@ pub struct NetworkDaemon { last_time: u64, last_rx_bytes: u64, last_tx_bytes: u64, + cached_interface: Option, + cached_ip: Option, } impl NetworkDaemon { @@ -21,40 +23,57 @@ impl NetworkDaemon { last_time: 0, last_rx_bytes: 0, last_tx_bytes: 0, + cached_interface: None, + cached_ip: None, } } pub fn poll(&mut self, state: SharedState) { - if let Ok(interface) = get_primary_interface() { - if !interface.is_empty() { - let time_now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_secs(); + // Cache invalidation: if the interface directory doesn't exist, clear cache + if let Some(ref iface) = self.cached_interface { + if !std::path::Path::new(&format!("/sys/class/net/{}", iface)).exists() { + self.cached_interface = None; + self.cached_ip = None; + } + } - if let Ok((rx_bytes_now, tx_bytes_now)) = get_bytes(&interface) { - if self.last_time > 0 && time_now > self.last_time { - let time_diff = time_now - self.last_time; - let rx_bps = (rx_bytes_now.saturating_sub(self.last_rx_bytes)) / time_diff; - let tx_bps = (tx_bytes_now.saturating_sub(self.last_tx_bytes)) / time_diff; - - let rx_mbps = (rx_bps as f64) / 1024.0 / 1024.0; - let tx_mbps = (tx_bps as f64) / 1024.0 / 1024.0; - - debug!(interface, rx = rx_mbps, tx = tx_mbps, "Network stats updated"); - - if let Ok(mut state_lock) = state.write() { - state_lock.network.rx_mbps = rx_mbps; - state_lock.network.tx_mbps = tx_mbps; - } - } - - self.last_time = time_now; - self.last_rx_bytes = rx_bytes_now; - self.last_tx_bytes = tx_bytes_now; + // Re-detect interface if needed + if self.cached_interface.is_none() { + if let Ok(iface) = get_primary_interface() { + if !iface.is_empty() { + self.cached_interface = Some(iface.clone()); + self.cached_ip = get_ip_address(&iface); } + } + } + + if let Some(ref interface) = self.cached_interface { + let time_now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + + if let Ok((rx_bytes_now, tx_bytes_now)) = get_bytes(interface) { + if self.last_time > 0 && time_now > self.last_time { + let time_diff = time_now - self.last_time; + let rx_bps = (rx_bytes_now.saturating_sub(self.last_rx_bytes)) / time_diff; + let tx_bps = (tx_bytes_now.saturating_sub(self.last_tx_bytes)) / time_diff; + + let rx_mbps = (rx_bps as f64) / 1024.0 / 1024.0; + let tx_mbps = (tx_bps as f64) / 1024.0 / 1024.0; + + if let Ok(mut state_lock) = state.write() { + state_lock.network.rx_mbps = rx_mbps; + state_lock.network.tx_mbps = tx_mbps; + } + } + + self.last_time = time_now; + self.last_rx_bytes = rx_bytes_now; + self.last_tx_bytes = tx_bytes_now; } else { - warn!("No primary network interface found during poll"); + // Read failed, might be down + self.cached_interface = None; } } } @@ -87,6 +106,8 @@ impl WaybarModule for NetworkModule { .format .replace("{interface}", &interface) .replace("{ip}", &ip) + .replace("{rx:>5.2}", &format!("{:>5.2}", rx_mbps)) + .replace("{tx:>5.2}", &format!("{:>5.2}", tx_mbps)) .replace("{rx}", &format!("{:.2}", rx_mbps)) .replace("{tx}", &format!("{:.2}", tx_mbps)); diff --git a/src/modules/power.rs b/src/modules/power.rs index 469ce1c..146a098 100644 --- a/src/modules/power.rs +++ b/src/modules/power.rs @@ -8,7 +8,7 @@ use std::fs; pub struct PowerModule; impl WaybarModule for PowerModule { - fn run(&self, _config: &Config, _state: &SharedState, _args: &[&str]) -> Result { + fn run(&self, config: &Config, _state: &SharedState, _args: &[&str]) -> Result { let critical_threshold = 15; let warning_threshold = 50; @@ -24,7 +24,7 @@ impl WaybarModule for PowerModule { } } - // Check AC status as fallback or TLP proxy + // Check AC status let mut ac_online = false; if let Ok(entries) = fs::read_dir("/sys/class/power_supply") { for entry in entries.flatten() { @@ -59,7 +59,6 @@ impl WaybarModule for PowerModule { } }; - // Read battery capacity and status let capacity_str = fs::read_to_string(bat_path.join("capacity")).unwrap_or_else(|_| "0".to_string()); let percentage: u8 = capacity_str.trim().parse().unwrap_or(0); let status_str = fs::read_to_string(bat_path.join("status")).unwrap_or_else(|_| "Unknown".to_string()); @@ -90,8 +89,13 @@ impl WaybarModule for PowerModule { ) }; + let text = config.power.format + .replace("{percentage:>3}", &format!("{:>3}", percentage)) + .replace("{percentage}", &format!("{}", percentage)) + .replace("{icon}", icon); + Ok(WaybarOutput { - text: format!("{}% {}", percentage, icon), + text, tooltip: Some(tooltip), class: Some(class.to_string()), percentage: Some(percentage), diff --git a/src/modules/sys.rs b/src/modules/sys.rs index bfd4858..f2aa89a 100644 --- a/src/modules/sys.rs +++ b/src/modules/sys.rs @@ -7,7 +7,7 @@ use anyhow::Result; pub struct SysModule; impl WaybarModule for SysModule { - fn run(&self, _config: &Config, state: &SharedState, _args: &[&str]) -> Result { + fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result { let (load1, load5, load15, uptime_secs, process_count) = { if let Ok(state_lock) = state.read() { ( @@ -30,8 +30,17 @@ impl WaybarModule for SysModule { format!("{}m", minutes) }; + let text = config.sys.format + .replace("{uptime}", &uptime_str) + .replace("{load1:>4.2}", &format!("{:>4.2}", load1)) + .replace("{load5:>4.2}", &format!("{:>4.2}", load5)) + .replace("{load15:>4.2}", &format!("{:>4.2}", load15)) + .replace("{load1}", &format!("{:.2}", load1)) + .replace("{load5}", &format!("{:.2}", load5)) + .replace("{load15}", &format!("{:.2}", load15)); + Ok(WaybarOutput { - text: format!("UP: {} | LOAD: {:.2} {:.2} {:.2}", uptime_str, load1, load5, load15), + text, tooltip: Some(format!( "Uptime: {}\nProcesses: {}\nLoad Avg: {:.2}, {:.2}, {:.2}", uptime_str, process_count, load1, load5, load15