diff --git a/Cargo.lock b/Cargo.lock index e0fd601..2289025 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,7 +47,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -58,7 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys", ] [[package]] @@ -73,12 +73,27 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.6.0" @@ -125,29 +140,42 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - [[package]] name = "fluxo-rs" -version = "0.1.0" +version = "0.2.0" dependencies = [ "anyhow", "clap", - "fs4", + "ctrlc", "regex", "serde", "serde_json", @@ -157,16 +185,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "fs4" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" -dependencies = [ - "rustix", - "windows-sys 0.59.0", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -213,12 +231,6 @@ version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - [[package]] name = "log" version = "0.4.29" @@ -240,6 +252,18 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "ntapi" version = "0.4.3" @@ -255,7 +279,16 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", ] [[package]] @@ -267,6 +300,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "objc2-io-kit" version = "0.3.2" @@ -342,19 +381,6 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - [[package]] name = "serde" version = "1.0.228" @@ -703,15 +729,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -721,22 +738,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - [[package]] name = "windows-threading" version = "0.2.1" @@ -746,54 +747,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "winnow" version = "0.7.15" diff --git a/Cargo.toml b/Cargo.toml index ff39b82..ad5b0cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "fluxo-rs" -version = "0.1.0" +version = "0.2.0" edition = "2024" [dependencies] anyhow = "1.0.102" clap = { version = "4.6.0", features = ["derive"] } -fs4 = "0.13.1" +ctrlc = "3" regex = "1.10" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" diff --git a/src/config.rs b/src/config.rs index e87a461..cc148c7 100644 --- a/src/config.rs +++ b/src/config.rs @@ -95,9 +95,11 @@ pub struct GpuConfig { 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_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(), + format_nvidia: "NV: {usage:>3.0}% {vram_used:>4.1}/{vram_total:>4.1}GB {temp:>4.1}C" + .to_string(), } } } @@ -246,7 +248,10 @@ pub fn load_config(custom_path: Option) -> Config { } } } else { - debug!("No config file found at {:?}, using default settings.", config_path); + debug!( + "No config file found at {:?}, using default settings.", + config_path + ); Config::default() } } diff --git a/src/daemon.rs b/src/daemon.rs index 4193c62..243ba42 100644 --- a/src/daemon.rs +++ b/src/daemon.rs @@ -1,62 +1,95 @@ use crate::config::Config; -use crate::ipc::SOCKET_PATH; -use crate::modules::network::NetworkDaemon; -use crate::modules::hardware::HardwareDaemon; +use crate::ipc::socket_path; use crate::modules::WaybarModule; +use crate::modules::hardware::HardwareDaemon; +use crate::modules::network::NetworkDaemon; use crate::state::{AppState, SharedState}; use anyhow::Result; use std::fs; use std::io::{BufRead, BufReader, Write}; +use std::net::Shutdown; use std::os::unix::net::UnixListener; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, RwLock}; use std::thread; use std::time::Duration; -use tracing::{info, warn, error, debug}; -use std::path::PathBuf; +use tracing::{debug, error, info, warn}; + +struct SocketGuard { + path: String, +} + +impl Drop for SocketGuard { + fn drop(&mut self) { + debug!("Cleaning up socket file: {}", self.path); + let _ = fs::remove_file(&self.path); + } +} pub fn run_daemon(config_path: Option) -> Result<()> { - if fs::metadata(SOCKET_PATH).is_ok() { - debug!("Removing stale socket file: {}", SOCKET_PATH); - fs::remove_file(SOCKET_PATH)?; + let sock_path = socket_path(); + + if fs::metadata(&sock_path).is_ok() { + debug!("Removing stale socket file: {}", sock_path); + fs::remove_file(&sock_path)?; } let state: SharedState = Arc::new(RwLock::new(AppState::default())); - let listener = UnixListener::bind(SOCKET_PATH)?; - + let listener = UnixListener::bind(&sock_path)?; + let _guard = SocketGuard { + path: sock_path.clone(), + }; + + // Signal handling: set flag so main loop exits cleanly + let running = Arc::new(AtomicBool::new(true)); + let running_clone = Arc::clone(&running); + ctrlc::set_handler(move || { + info!("Received shutdown signal, exiting..."); + running_clone.store(false, Ordering::SeqCst); + })?; + // We store the original config_path to allow proper reloading later let config_path_clone = config_path.clone(); let config = Arc::new(RwLock::new(crate::config::load_config(config_path))); let poll_state = Arc::clone(&state); + let poll_running = Arc::clone(&running); thread::spawn(move || { info!("Starting background polling thread"); let mut network_daemon = NetworkDaemon::new(); let mut hardware_daemon = HardwareDaemon::new(); - loop { + while poll_running.load(Ordering::SeqCst) { network_daemon.poll(Arc::clone(&poll_state)); hardware_daemon.poll(Arc::clone(&poll_state)); thread::sleep(Duration::from_secs(1)); } }); - info!("Fluxo daemon successfully bound to socket: {}", SOCKET_PATH); + info!("Fluxo daemon successfully bound to socket: {}", sock_path); - for stream in listener.incoming() { - match stream { - Ok(mut stream) => { + // Use non-blocking accept so we can check the running flag + listener.set_nonblocking(true)?; + + while running.load(Ordering::SeqCst) { + match listener.accept() { + Ok((mut stream, _)) => { let state_clone = Arc::clone(&state); let config_clone = Arc::clone(&config); let cp_clone = config_path_clone.clone(); thread::spawn(move || { - let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut reader = BufReader::new(&stream); let mut request = String::new(); if let Err(e) = reader.read_line(&mut request) { error!("Failed to read from IPC stream: {}", e); return; } - + drop(reader); + let request = request.trim(); - if request.is_empty() { return; } + if request.is_empty() { + return; + } let parts: Vec<&str> = request.split_whitespace().collect(); if let Some(module_name) = parts.first() { @@ -70,25 +103,37 @@ pub fn run_daemon(config_path: Option) -> Result<()> { } else { error!("Failed to acquire write lock for configuration reload."); } + let _ = stream.shutdown(Shutdown::Write); return; } debug!(module = module_name, args = ?&parts[1..], "Handling IPC request"); - let response = handle_request(*module_name, &parts[1..], &state_clone, &config_clone); + let response = + handle_request(module_name, &parts[1..], &state_clone, &config_clone); if let Err(e) = stream.write_all(response.as_bytes()) { error!("Failed to write IPC response: {}", e); } + let _ = stream.shutdown(Shutdown::Write); } }); } + Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => { + thread::sleep(Duration::from_millis(50)); + } Err(e) => error!("Failed to accept incoming connection: {}", e), } } + info!("Daemon shutting down gracefully."); Ok(()) } -fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_lock: &Arc>) -> String { +fn handle_request( + module_name: &str, + args: &[&str], + state: &SharedState, + config_lock: &Arc>, +) -> String { let config = if let Ok(c) = config_lock.read() { c } else { @@ -102,8 +147,16 @@ fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_ "mem" | "memory" => crate::modules::memory::MemoryModule.run(&config, state, args), "disk" => crate::modules::disk::DiskModule.run(&config, state, args), "pool" | "btrfs" => crate::modules::btrfs::BtrfsModule.run(&config, state, args), - "vol" => crate::modules::audio::AudioModule.run(&config, state, &["sink", args.get(0).unwrap_or(&"show")]), - "mic" => crate::modules::audio::AudioModule.run(&config, state, &["source", args.get(0).unwrap_or(&"show")]), + "vol" => crate::modules::audio::AudioModule.run( + &config, + state, + &["sink", args.first().unwrap_or(&"show")], + ), + "mic" => crate::modules::audio::AudioModule.run( + &config, + state, + &["source", args.first().unwrap_or(&"show")], + ), "gpu" => crate::modules::gpu::GpuModule.run(&config, state, args), "sys" => crate::modules::sys::SysModule.run(&config, state, args), "bt" | "bluetooth" => crate::modules::bt::BtModule.run(&config, state, args), @@ -113,7 +166,7 @@ fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_ _ => { warn!("Received request for unknown module: '{}'", module_name); Err(anyhow::anyhow!("Unknown module: {}", module_name)) - }, + } }; match result { @@ -121,7 +174,7 @@ fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_ Err(e) => { error!(module = module_name, error = %e, "Module execution failed"); let err_out = crate::output::WaybarOutput { - text: format!("\u{200B}Error\u{200B}"), + text: "\u{200B}Error\u{200B}".to_string(), tooltip: Some(e.to_string()), class: Some("error".to_string()), percentage: None, diff --git a/src/ipc.rs b/src/ipc.rs index 1ac41cf..2cf0f29 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,13 +1,22 @@ use std::io::{Read, Write}; use std::os::unix::net::UnixStream; +use std::time::Duration; use tracing::debug; -pub const SOCKET_PATH: &str = "/tmp/fluxo.sock"; +pub fn socket_path() -> String { + if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") { + format!("{}/fluxo.sock", dir) + } else { + "/tmp/fluxo.sock".to_string() + } +} + +pub fn request_data(module: &str, args: &[&str]) -> anyhow::Result { + let sock = socket_path(); + debug!(module, ?args, "Connecting to daemon socket: {}", sock); + let mut stream = UnixStream::connect(&sock)?; + stream.set_read_timeout(Some(Duration::from_secs(5)))?; -pub fn request_data(module: &str, args: &[String]) -> anyhow::Result { - debug!(module, ?args, "Connecting to daemon socket: {}", SOCKET_PATH); - let mut stream = UnixStream::connect(SOCKET_PATH)?; - // Send module and args let mut request = module.to_string(); for arg in args { @@ -15,13 +24,13 @@ pub fn request_data(module: &str, args: &[String]) -> anyhow::Result { request.push_str(arg); } request.push('\n'); - + debug!("Sending IPC request: {}", request.trim()); stream.write_all(request.as_bytes())?; - + let mut response = String::new(); stream.read_to_string(&mut response)?; debug!("Received IPC response: {}", response); - + Ok(response) } diff --git a/src/main.rs b/src/main.rs index 258c9dc..cb16a3c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use clap::{Parser, Subcommand}; use std::path::PathBuf; use std::process; use tracing::{error, info}; -use tracing_subscriber::{fmt, prelude::*, EnvFilter}; +use tracing_subscriber::{EnvFilter, fmt, prelude::*}; #[derive(Parser)] #[command(name = "fluxo")] @@ -98,27 +98,25 @@ fn main() { process::exit(1); } } - Commands::Reload => { - match ipc::request_data("reload", &[]) { - Ok(_) => info!("Reload signal sent to daemon"), - Err(e) => { - error!("Failed to send reload signal: {}", e); - process::exit(1); - } + Commands::Reload => match ipc::request_data("reload", &[]) { + Ok(_) => info!("Reload signal sent to daemon"), + Err(e) => { + error!("Failed to send reload signal: {}", e); + process::exit(1); } - } + }, Commands::Net => handle_ipc_response(ipc::request_data("net", &[])), Commands::Cpu => handle_ipc_response(ipc::request_data("cpu", &[])), Commands::Mem => handle_ipc_response(ipc::request_data("mem", &[])), - Commands::Disk { path } => handle_ipc_response(ipc::request_data("disk", &[path.clone()])), - Commands::Pool { kind } => handle_ipc_response(ipc::request_data("pool", &[kind.clone()])), + Commands::Disk { path } => handle_ipc_response(ipc::request_data("disk", &[path])), + Commands::Pool { kind } => handle_ipc_response(ipc::request_data("pool", &[kind])), Commands::Vol { cycle } => { let action = if *cycle { "cycle" } else { "show" }; - handle_ipc_response(ipc::request_data("vol", &[action.to_string()])); + handle_ipc_response(ipc::request_data("vol", &[action])); } Commands::Mic { cycle } => { let action = if *cycle { "cycle" } else { "show" }; - handle_ipc_response(ipc::request_data("mic", &[action.to_string()])); + handle_ipc_response(ipc::request_data("mic", &[action])); } Commands::Gpu => handle_ipc_response(ipc::request_data("gpu", &[])), Commands::Sys => handle_ipc_response(ipc::request_data("sys", &[])), @@ -126,13 +124,19 @@ fn main() { if action == "menu" { // Client-side execution of the menu let config = config::load_config(None); - - let devices_out = std::process::Command::new("bluetoothctl") + + let devices_out = match std::process::Command::new("bluetoothctl") .args(["devices"]) .output() - .expect("Failed to run bluetoothctl"); + { + Ok(out) => out, + Err(e) => { + error!("bluetoothctl not found or failed: {}", e); + return; + } + }; let stdout = String::from_utf8_lossy(&devices_out.stdout); - + let mut items = Vec::new(); for line in stdout.lines() { if line.starts_with("Device ") { @@ -144,24 +148,24 @@ fn main() { } if !items.is_empty() { - if let Ok(selected) = utils::show_menu("Connect BT: ", &items, &config.general.menu_command) { - if let Some(mac_start) = selected.rfind('(') { - if let Some(mac_end) = selected.rfind(')') { - let mac = &selected[mac_start + 1..mac_end]; - let _ = std::process::Command::new("bluetoothctl") - .args(["connect", mac]) - .status(); - } - } + if let Ok(selected) = + utils::show_menu("Connect BT: ", &items, &config.general.menu_command) + && let Some(mac_start) = selected.rfind('(') + && let Some(mac_end) = selected.rfind(')') + { + let mac = &selected[mac_start + 1..mac_end]; + let _ = std::process::Command::new("bluetoothctl") + .args(["connect", mac]) + .status(); } } else { info!("No paired Bluetooth devices found."); } return; } - handle_ipc_response(ipc::request_data("bt", &[action.clone()])); + handle_ipc_response(ipc::request_data("bt", &[action])); } - Commands::Buds { action } => handle_ipc_response(ipc::request_data("buds", &[action.clone()])), + Commands::Buds { action } => handle_ipc_response(ipc::request_data("buds", &[action])), Commands::Power => handle_ipc_response(ipc::request_data("power", &[])), Commands::Game => handle_ipc_response(ipc::request_data("game", &[])), } @@ -169,24 +173,22 @@ fn main() { fn handle_ipc_response(response: anyhow::Result) { match response { - Ok(json_str) => { - match serde_json::from_str::(&json_str) { - Ok(mut val) => { - if let Some(text) = val.get_mut("text").and_then(|t| t.as_str()) { - let processed_text = if text.contains('<') { - text.to_string() - } else { - text.replace(' ', "\u{2007}") - }; + Ok(json_str) => match serde_json::from_str::(&json_str) { + Ok(mut val) => { + if let Some(text) = val.get_mut("text").and_then(|t| t.as_str()) { + let processed_text = if text.contains('<') { + text.to_string() + } else { + text.replace(' ', "\u{2007}") + }; - let fixed_text = format!("\u{200B}{}\u{200B}", processed_text); - val["text"] = serde_json::Value::String(fixed_text); - } - println!("{}", serde_json::to_string(&val).unwrap()); + let fixed_text = format!("\u{200B}{}\u{200B}", processed_text); + val["text"] = serde_json::Value::String(fixed_text); } - Err(_) => println!("{}", json_str), + println!("{}", serde_json::to_string(&val).unwrap()); } - } + Err(_) => println!("{}", json_str), + }, Err(e) => { let err_out = output::WaybarOutput { text: format!("\u{200B}Daemon offline ({})\u{200B}", e), diff --git a/src/modules/audio.rs b/src/modules/audio.rs index 639e053..2d3393d 100644 --- a/src/modules/audio.rs +++ b/src/modules/audio.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template, run_command}; use anyhow::{Result, anyhow}; use std::process::Command; @@ -16,30 +16,30 @@ impl WaybarModule for AudioModule { match *action { "cycle" => { self.cycle_device(target_type)?; - return Ok(WaybarOutput { + Ok(WaybarOutput { text: String::new(), tooltip: None, class: None, percentage: None, - }); - } - "show" | _ => { - self.get_status(config, target_type) + }) } + "show" => self.get_status(config, target_type), + other => Err(anyhow!("Unknown audio action: '{}'", other)), } } } impl AudioModule { fn get_status(&self, config: &Config, target_type: &str) -> Result { - let target = if target_type == "sink" { "@DEFAULT_AUDIO_SINK@" } else { "@DEFAULT_AUDIO_SOURCE@" }; + let target = if target_type == "sink" { + "@DEFAULT_AUDIO_SINK@" + } else { + "@DEFAULT_AUDIO_SOURCE@" + }; - let output = Command::new("wpctl") - .args(["get-volume", target]) - .output()?; - let stdout = String::from_utf8_lossy(&output.stdout); - - let parts: Vec<&str> = stdout.trim().split_whitespace().collect(); + let stdout = run_command("wpctl", &["get-volume", target])?; + + let parts: Vec<&str> = stdout.split_whitespace().collect(); if parts.len() < 2 { return Err(anyhow!("Could not parse wpctl output: {}", stdout)); } @@ -58,31 +58,43 @@ impl AudioModule { let (text, class) = if muted { let icon = if target_type == "sink" { "" } else { "" }; - let format_str = if target_type == "sink" { &config.audio.format_sink_muted } else { &config.audio.format_source_muted }; + let format_str = if target_type == "sink" { + &config.audio.format_sink_muted + } else { + &config.audio.format_source_muted + }; let t = format_template( format_str, &[ ("name", TokenValue::String(&name)), ("icon", TokenValue::String(icon)), - ] + ], ); (t, "muted") } else { let icon = if target_type == "sink" { - if display_vol <= 30 { "" } - else if display_vol <= 60 { "" } - else { "" } + if display_vol <= 30 { + "" + } else if display_vol <= 60 { + "" + } else { + "" + } } else { "" }; - let format_str = if target_type == "sink" { &config.audio.format_sink_unmuted } else { &config.audio.format_source_unmuted }; + let format_str = if target_type == "sink" { + &config.audio.format_sink_unmuted + } else { + &config.audio.format_source_unmuted + }; let t = format_template( format_str, &[ ("name", TokenValue::String(&name)), ("icon", TokenValue::String(icon)), ("volume", TokenValue::Int(display_vol as i64)), - ] + ], ); (t, "unmuted") }; @@ -96,19 +108,26 @@ impl AudioModule { } fn get_description(&self, target_type: &str) -> Result { - let info_output = Command::new("pactl").arg("info").output()?; - let info_stdout = String::from_utf8_lossy(&info_output.stdout); - let search_key = if target_type == "sink" { "Default Sink:" } else { "Default Source:" }; - - let default_dev = info_stdout.lines() + let info_stdout = run_command("pactl", &["info"])?; + let search_key = if target_type == "sink" { + "Default Sink:" + } else { + "Default Source:" + }; + + let default_dev = info_stdout + .lines() .find(|l| l.contains(search_key)) .and_then(|l| l.split(':').nth(1)) .map(|s| s.trim()) .ok_or_else(|| anyhow!("Default {} not found", target_type))?; - let list_cmd = if target_type == "sink" { "sinks" } else { "sources" }; - let list_output = Command::new("pactl").args(["list", list_cmd]).output()?; - let list_stdout = String::from_utf8_lossy(&list_output.stdout); + let list_cmd = if target_type == "sink" { + "sinks" + } else { + "sources" + }; + let list_stdout = run_command("pactl", &["list", list_cmd])?; let mut current_name = String::new(); for line in list_stdout.lines() { @@ -124,11 +143,15 @@ impl AudioModule { } fn cycle_device(&self, target_type: &str) -> Result<()> { - let list_cmd = if target_type == "sink" { "sinks" } else { "sources" }; - let output = Command::new("pactl").args(["list", "short", list_cmd]).output()?; - let stdout = String::from_utf8_lossy(&output.stdout); + let list_cmd = if target_type == "sink" { + "sinks" + } else { + "sources" + }; + let stdout = run_command("pactl", &["list", "short", list_cmd])?; - let devices: Vec = stdout.lines() + let devices: Vec = stdout + .lines() .filter_map(|l| { let parts: Vec<&str> = l.split_whitespace().collect(); if parts.len() >= 2 { @@ -144,13 +167,19 @@ impl AudioModule { }) .collect(); - if devices.is_empty() { return Ok(()); } + if devices.is_empty() { + return Ok(()); + } - let info_output = Command::new("pactl").arg("info").output()?; - let info_stdout = String::from_utf8_lossy(&info_output.stdout); - let search_key = if target_type == "sink" { "Default Sink:" } else { "Default Source:" }; - - let current_dev = info_stdout.lines() + let info_stdout = run_command("pactl", &["info"])?; + let search_key = if target_type == "sink" { + "Default Sink:" + } else { + "Default Source:" + }; + + let current_dev = info_stdout + .lines() .find(|l| l.contains(search_key)) .and_then(|l| l.split(':').nth(1)) .map(|s| s.trim()) @@ -160,7 +189,11 @@ impl AudioModule { let next_index = (current_index + 1) % devices.len(); let next_dev = &devices[next_index]; - let set_cmd = if target_type == "sink" { "set-default-sink" } else { "set-default-source" }; + let set_cmd = if target_type == "sink" { + "set-default-sink" + } else { + "set-default-source" + }; Command::new("pactl").args([set_cmd, next_dev]).status()?; Ok(()) diff --git a/src/modules/bt.rs b/src/modules/bt.rs index bcbe733..8bd1ce4 100644 --- a/src/modules/bt.rs +++ b/src/modules/bt.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template, run_command}; use anyhow::Result; use std::process::Command; @@ -14,7 +14,9 @@ impl WaybarModule for BtModule { if *action == "disconnect" { if let Some(mac) = find_audio_device() { - let _ = Command::new("bluetoothctl").args(["disconnect", &mac]).output(); + let _ = Command::new("bluetoothctl") + .args(["disconnect", &mac]) + .output(); } return Ok(WaybarOutput { text: String::new(), @@ -24,8 +26,7 @@ impl WaybarModule for BtModule { }); } - if let Ok(output) = Command::new("bluetoothctl").arg("show").output() { - let stdout = String::from_utf8_lossy(&output.stdout); + if let Ok(stdout) = run_command("bluetoothctl", &["show"]) { if stdout.contains("Powered: no") { return Ok(WaybarOutput { text: config.bt.format_disabled.clone(), @@ -37,8 +38,7 @@ impl WaybarModule for BtModule { } if let Some(mac) = find_audio_device() { - let info_output = Command::new("bluetoothctl").args(["info", &mac]).output()?; - let info = String::from_utf8_lossy(&info_output.stdout); + let info = run_command("bluetoothctl", &["info", &mac])?; let mut alias = mac.clone(); let mut battery = None; @@ -48,7 +48,8 @@ impl WaybarModule for BtModule { if line.contains("Alias:") { alias = line.split("Alias:").nth(1).unwrap_or("").trim().to_string(); } else if line.contains("Battery Percentage:") { - if let Some(bat_str) = line.split('(').nth(1).and_then(|s| s.split(')').next()) { + if let Some(bat_str) = line.split('(').nth(1).and_then(|s| s.split(')').next()) + { battery = bat_str.parse::().ok(); } } else if line.contains("Trusted: yes") { @@ -61,12 +62,14 @@ impl WaybarModule for BtModule { alias, mac, trusted, - battery.map(|b| format!("{}%", b)).unwrap_or_else(|| "N/A".to_string()) + battery + .map(|b| format!("{}%", b)) + .unwrap_or_else(|| "N/A".to_string()) ); let text = format_template( &config.bt.format_connected, - &[("alias", TokenValue::String(&alias))] + &[("alias", TokenValue::String(&alias))], ); Ok(WaybarOutput { @@ -87,8 +90,7 @@ impl WaybarModule for BtModule { } fn find_audio_device() -> Option { - if let Ok(output) = Command::new("pactl").arg("get-default-sink").output() { - let sink = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if let Ok(sink) = run_command("pactl", &["get-default-sink"]) { if sink.starts_with("bluez_output.") { let parts: Vec<&str> = sink.split('.').collect(); if parts.len() >= 2 { @@ -97,15 +99,13 @@ fn find_audio_device() -> Option { } } - if let Ok(output) = Command::new("bluetoothctl").args(["devices", "Connected"]).output() { - let stdout = String::from_utf8_lossy(&output.stdout); + if let Ok(stdout) = run_command("bluetoothctl", &["devices", "Connected"]) { for line in stdout.lines() { if line.starts_with("Device ") { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 2 { let mac = parts[1]; - if let Ok(info) = Command::new("bluetoothctl").args(["info", mac]).output() { - let info_str = String::from_utf8_lossy(&info.stdout); + if let Ok(info_str) = run_command("bluetoothctl", &["info", mac]) { if info_str.contains("0000110b-0000-1000-8000-00805f9b34fb") { return Some(mac.to_string()); } diff --git a/src/modules/btrfs.rs b/src/modules/btrfs.rs index 0c36414..801b710 100644 --- a/src/modules/btrfs.rs +++ b/src/modules/btrfs.rs @@ -2,22 +2,26 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template}; use anyhow::Result; -use sysinfo::Disks; pub struct BtrfsModule; impl WaybarModule for BtrfsModule { - fn run(&self, config: &Config, _state: &SharedState, _args: &[&str]) -> Result { - let disks = Disks::new_with_refreshed_list(); + fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result { + let disks = if let Ok(s) = state.read() { + s.disks.clone() + } else { + return Err(anyhow::anyhow!("Failed to read state")); + }; + let mut total_used: f64 = 0.0; let mut total_size: f64 = 0.0; for disk in &disks { - if disk.file_system().to_string_lossy().to_lowercase().contains("btrfs") { - let size = disk.total_space() as f64; - let available = disk.available_space() as f64; + if disk.filesystem.contains("btrfs") { + let size = disk.total_bytes as f64; + let available = disk.available_bytes as f64; total_size += size; total_used += size - available; } @@ -49,7 +53,7 @@ impl WaybarModule for BtrfsModule { &[ ("used", TokenValue::Float(used_gb)), ("total", TokenValue::Float(size_gb)), - ] + ], ); Ok(WaybarOutput { diff --git a/src/modules/buds.rs b/src/modules/buds.rs index 118c281..00dc1ac 100644 --- a/src/modules/buds.rs +++ b/src/modules/buds.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template}; use anyhow::Result; use std::process::Command; @@ -17,39 +17,54 @@ impl WaybarModule for BudsModule { "cycle_anc" => { let output = Command::new("pbpctrl").args(["get", "anc"]).output()?; let current_mode = String::from_utf8_lossy(&output.stdout).trim().to_string(); - + let next_mode = match current_mode.as_str() { "active" => "aware", "aware" => "off", _ => "active", }; - - Command::new("pbpctrl").args(["set", "anc", next_mode]).status()?; + + Command::new("pbpctrl") + .args(["set", "anc", next_mode]) + .status()?; return Ok(WaybarOutput { text: String::new(), - tooltip: None, class: None, percentage: None, + tooltip: None, + class: None, + percentage: None, }); } "connect" => { - Command::new("bluetoothctl").args(["connect", mac]).status()?; + Command::new("bluetoothctl") + .args(["connect", mac]) + .status()?; return Ok(WaybarOutput { text: String::new(), - tooltip: None, class: None, percentage: None, + tooltip: None, + class: None, + percentage: None, }); } "disconnect" => { - Command::new("bluetoothctl").args(["disconnect", mac]).status()?; + Command::new("bluetoothctl") + .args(["disconnect", mac]) + .status()?; return Ok(WaybarOutput { text: String::new(), - tooltip: None, class: None, percentage: None, + tooltip: None, + class: None, + percentage: None, }); } - "show" | _ => {} + "show" => {} + other => { + return Err(anyhow::anyhow!("Unknown buds action: '{}'", other)); + } } let bt_info = Command::new("bluetoothctl").args(["info", mac]).output()?; let bt_str = String::from_utf8_lossy(&bt_info.stdout); - + if !bt_str.contains("Connected: yes") { return Ok(WaybarOutput { text: config.buds.format_disconnected.clone(), @@ -68,7 +83,7 @@ impl WaybarModule for BudsModule { percentage: None, }); } - + let bat_result = bat_cmd.unwrap(); let bat_output = String::from_utf8_lossy(&bat_result.stdout); let mut left_bud = "unknown"; @@ -85,12 +100,22 @@ impl WaybarModule for BudsModule { if left_bud == "unknown" && right_bud == "unknown" { return Ok(WaybarOutput { text: "{}".to_string(), - tooltip: None, class: None, percentage: None, + tooltip: None, + class: None, + percentage: None, }); } - let left_display = if left_bud == "unknown" { "---".to_string() } else { format!("{}%", left_bud) }; - let right_display = if right_bud == "unknown" { "---".to_string() } else { format!("{}%", right_bud) }; + let left_display = if left_bud == "unknown" { + "---".to_string() + } else { + format!("{}%", left_bud) + }; + let right_display = if right_bud == "unknown" { + "---".to_string() + } else { + format!("{}%", right_bud) + }; let anc_cmd = Command::new("pbpctrl").args(["get", "anc"]).output()?; let current_mode = String::from_utf8_lossy(&anc_cmd.stdout).trim().to_string(); @@ -108,7 +133,7 @@ impl WaybarModule for BudsModule { ("left", TokenValue::String(&left_display)), ("right", TokenValue::String(&right_display)), ("anc", TokenValue::String(anc_icon)), - ] + ], ); Ok(WaybarOutput { diff --git a/src/modules/cpu.rs b/src/modules/cpu.rs index 8f9c481..8c6e043 100644 --- a/src/modules/cpu.rs +++ b/src/modules/cpu.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template}; use anyhow::Result; pub struct CpuModule; @@ -26,9 +26,9 @@ impl WaybarModule for CpuModule { &[ ("usage", TokenValue::Float(usage)), ("temp", TokenValue::Float(temp)), - ] + ], ); - + let class = if usage > 95.0 { "max" } else if usage > 75.0 { diff --git a/src/modules/disk.rs b/src/modules/disk.rs index e440992..2aacc4b 100644 --- a/src/modules/disk.rs +++ b/src/modules/disk.rs @@ -2,28 +2,36 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template}; use anyhow::Result; -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(); + + let disks = if let Ok(s) = state.read() { + s.disks.clone() + } else { + return Err(anyhow::anyhow!("Failed to read state")); + }; + for disk in &disks { - if disk.mount_point().to_string_lossy() == *mountpoint { - let total = disk.total_space() as f64; - let available = disk.available_space() as f64; + if disk.mount_point == *mountpoint { + let total = disk.total_bytes as f64; + let available = disk.available_bytes as f64; let used = total - available; - + let used_gb = used / 1024.0 / 1024.0 / 1024.0; let total_gb = total / 1024.0 / 1024.0 / 1024.0; let free_gb = available / 1024.0 / 1024.0 / 1024.0; - - let percentage = if total > 0.0 { (used / total) * 100.0 } else { 0.0 }; + + let percentage = if total > 0.0 { + (used / total) * 100.0 + } else { + 0.0 + }; let class = if percentage > 95.0 { "max" @@ -39,12 +47,15 @@ impl WaybarModule for DiskModule { ("mount", TokenValue::String(mountpoint)), ("used", TokenValue::Float(used_gb)), ("total", TokenValue::Float(total_gb)), - ] + ], ); return Ok(WaybarOutput { text, - tooltip: Some(format!("Used: {:.1}G\nTotal: {:.1}G\nFree: {:.1}G", used_gb, total_gb, free_gb)), + 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/game.rs b/src/modules/game.rs index 034ea31..421997f 100644 --- a/src/modules/game.rs +++ b/src/modules/game.rs @@ -2,25 +2,16 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; +use crate::utils::run_command; use anyhow::Result; -use std::process::Command; pub struct GameModule; impl WaybarModule for GameModule { fn run(&self, config: &Config, _state: &SharedState, _args: &[&str]) -> Result { - let output = Command::new("hyprctl") - .args(["getoption", "animations:enabled", "-j"]) - .output(); - - let mut is_gamemode = false; - - if let Ok(out) = output { - let stdout = String::from_utf8_lossy(&out.stdout); - if stdout.contains("\"int\": 0") { - is_gamemode = true; - } - } + let is_gamemode = run_command("hyprctl", &["getoption", "animations:enabled", "-j"]) + .map(|stdout| stdout.contains("\"int\": 0")) + .unwrap_or(false); if is_gamemode { Ok(WaybarOutput { diff --git a/src/modules/gpu.rs b/src/modules/gpu.rs index 4052da0..d66709e 100644 --- a/src/modules/gpu.rs +++ b/src/modules/gpu.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template}; use anyhow::Result; pub struct GpuModule; @@ -21,7 +21,15 @@ impl WaybarModule for GpuModule { state_lock.gpu.model.clone(), ) } else { - (false, String::from("Unknown"), 0.0, 0.0, 0.0, 0.0, String::from("Unknown")) + ( + false, + String::from("Unknown"), + 0.0, + 0.0, + 0.0, + 0.0, + String::from("Unknown"), + ) } }; @@ -55,13 +63,16 @@ impl WaybarModule for GpuModule { ("vram_used", TokenValue::Float(vram_used)), ("vram_total", TokenValue::Float(vram_total)), ("temp", TokenValue::Float(temp)), - ] + ], ); let tooltip = if vendor == "Intel" { format!("Model: {}\nApprox Usage: {:.0}%", model, usage) } else { - format!("Model: {}\nUsage: {:.0}%\nVRAM: {:.1}/{:.1}GB\nTemp: {:.1}°C", model, usage, vram_used, vram_total, temp) + format!( + "Model: {}\nUsage: {:.0}%\nVRAM: {:.1}/{:.1}GB\nTemp: {:.1}°C", + model, usage, vram_used, vram_total, temp + ) }; Ok(WaybarOutput { diff --git a/src/modules/hardware.rs b/src/modules/hardware.rs index 4dc31cd..661dfa1 100644 --- a/src/modules/hardware.rs +++ b/src/modules/hardware.rs @@ -1,18 +1,27 @@ -use crate::state::SharedState; -use sysinfo::{Components, System}; +use crate::state::{DiskInfo, SharedState}; +use sysinfo::{Components, Disks, System}; pub struct HardwareDaemon { sys: System, components: Components, gpu_vendor: Option, + gpu_poll_counter: u8, + disk_poll_counter: u8, } impl HardwareDaemon { pub fn new() -> Self { - let mut sys = System::new_all(); - sys.refresh_all(); + let mut sys = System::new(); + sys.refresh_cpu_usage(); + sys.refresh_memory(); let components = Components::new_with_refreshed_list(); - Self { sys, components, gpu_vendor: None } + Self { + sys, + components, + gpu_vendor: None, + gpu_poll_counter: 0, + disk_poll_counter: 0, + } } pub fn poll(&mut self, state: SharedState) { @@ -21,15 +30,25 @@ impl HardwareDaemon { 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()); + let cpu_model = self + .sys + .cpus() + .first() + .map(|c| c.brand().to_string()) + .unwrap_or_else(|| "Unknown".to_string()); let mut cpu_temp = 0.0; for component in &self.components { let label = component.label().to_lowercase(); - if label.contains("tctl") || label.contains("cpu") || label.contains("package") || label.contains("temp1") { - if let Some(temp) = component.temperature() { - cpu_temp = temp as f64; - if cpu_temp > 0.0 { break; } + if (label.contains("tctl") + || label.contains("cpu") + || label.contains("package") + || label.contains("temp1")) + && let Some(temp) = component.temperature() + { + cpu_temp = temp as f64; + if cpu_temp > 0.0 { + break; } } } @@ -40,20 +59,20 @@ impl HardwareDaemon { let load_avg = System::load_average(); let uptime = System::uptime(); - + 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 parts.len() >= 4 + && 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; - state_lock.cpu.temp = cpu_temp as f64; + state_lock.cpu.temp = cpu_temp; state_lock.cpu.model = cpu_model; state_lock.memory.total_gb = total_mem; @@ -65,100 +84,143 @@ impl HardwareDaemon { state_lock.sys.uptime = uptime; state_lock.sys.process_count = process_count; - self.poll_gpu(&mut state_lock.gpu); + // Poll GPU every 5 seconds to avoid expensive nvidia-smi calls + self.gpu_poll_counter = (self.gpu_poll_counter + 1) % 5; + if self.gpu_poll_counter == 0 { + self.poll_gpu(&mut state_lock.gpu); + } + + // Poll disks every 10 seconds + self.disk_poll_counter = (self.disk_poll_counter + 1) % 10; + if self.disk_poll_counter == 0 { + state_lock.disks = Disks::new_with_refreshed_list() + .iter() + .map(|d| DiskInfo { + mount_point: d.mount_point().to_string_lossy().into_owned(), + filesystem: d.file_system().to_string_lossy().to_lowercase(), + total_bytes: d.total_space(), + available_bytes: d.available_space(), + }) + .collect(); + } } } fn poll_gpu(&mut self, gpu: &mut crate::state::GpuState) { gpu.active = false; - 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"]) + if (self.gpu_vendor.as_deref() == Some("NVIDIA") || self.gpu_vendor.is_none()) + && 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; - } - } + && 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; } } } - if self.gpu_vendor.as_deref() == Some("AMD") || self.gpu_vendor.as_deref() == Some("Intel") || self.gpu_vendor.is_none() { + 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 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; - } + + if (self.gpu_vendor.as_deref() == Some("AMD") || self.gpu_vendor.is_none()) + && 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; } + gpu.model = "AMD GPU".to_string(); + self.gpu_vendor = Some("AMD".to_string()); + return; } 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 - }; + 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); - } + if let Some(path) = freq_path + && let Ok(freq_str) = std::fs::read_to_string(&path) + { + gpu.active = true; + gpu.vendor = "Intel".to_string(); - 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; + 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; } } } diff --git a/src/modules/memory.rs b/src/modules/memory.rs index cb40e23..885f421 100644 --- a/src/modules/memory.rs +++ b/src/modules/memory.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template}; use anyhow::Result; pub struct MemoryModule; @@ -11,23 +11,24 @@ impl WaybarModule for MemoryModule { fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result { let (used_gb, total_gb) = { if let Ok(state_lock) = state.read() { - ( - state_lock.memory.used_gb, - state_lock.memory.total_gb, - ) + (state_lock.memory.used_gb, state_lock.memory.total_gb) } else { (0.0, 0.0) } }; - let ratio = if total_gb > 0.0 { (used_gb / total_gb) * 100.0 } else { 0.0 }; + let ratio = if total_gb > 0.0 { + (used_gb / total_gb) * 100.0 + } else { + 0.0 + }; let text = format_template( &config.memory.format, &[ ("used", TokenValue::Float(used_gb)), ("total", TokenValue::Float(total_gb)), - ] + ], ); let class = if ratio > 95.0 { diff --git a/src/modules/mod.rs b/src/modules/mod.rs index b0f809c..d41e049 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,16 +1,16 @@ -pub mod network; -pub mod cpu; -pub mod memory; -pub mod hardware; -pub mod disk; -pub mod btrfs; pub mod audio; -pub mod gpu; -pub mod sys; pub mod bt; +pub mod btrfs; pub mod buds; -pub mod power; +pub mod cpu; +pub mod disk; pub mod game; +pub mod gpu; +pub mod hardware; +pub mod memory; +pub mod network; +pub mod power; +pub mod sys; use crate::config::Config; use crate::output::WaybarOutput; @@ -20,4 +20,3 @@ use anyhow::Result; pub trait WaybarModule { fn run(&self, config: &Config, state: &SharedState, args: &[&str]) -> Result; } - diff --git a/src/modules/network.rs b/src/modules/network.rs index 75f78e5..ec03055 100644 --- a/src/modules/network.rs +++ b/src/modules/network.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template}; use anyhow::Result; use std::fs; use std::time::{SystemTime, UNIX_EPOCH}; @@ -30,21 +30,20 @@ impl NetworkDaemon { pub fn poll(&mut self, state: SharedState) { // 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 Some(ref iface) = self.cached_interface + && !std::path::Path::new(&format!("/sys/class/net/{}", iface)).exists() + { + self.cached_interface = None; + self.cached_ip = None; } // 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 self.cached_interface.is_none() + && let Ok(iface) = get_primary_interface() + && !iface.is_empty() + { + self.cached_ip = get_ip_address(&iface); + self.cached_interface = Some(iface); } if let Some(ref interface) = self.cached_interface { @@ -55,17 +54,26 @@ impl NetworkDaemon { 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; + let time_diff = (time_now - self.last_time) as f64; + let rx_mbps = (rx_bytes_now.saturating_sub(self.last_rx_bytes)) as f64 + / time_diff + / 1024.0 + / 1024.0; + let tx_mbps = (tx_bytes_now.saturating_sub(self.last_tx_bytes)) as f64 + / time_diff + / 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; + state_lock.network.interface = interface.clone(); + state_lock.network.ip = self.cached_ip.clone().unwrap_or_default(); } + } else if let Ok(mut state_lock) = state.write() { + // First poll: no speed data yet, but update interface/ip + state_lock.network.interface = interface.clone(); + state_lock.network.ip = self.cached_ip.clone().unwrap_or_default(); } self.last_time = time_now; @@ -75,13 +83,32 @@ impl NetworkDaemon { // Read failed, might be down self.cached_interface = None; } + } else if let Ok(mut state_lock) = state.write() { + // No interface detected + state_lock.network.interface.clear(); + state_lock.network.ip.clear(); } } } impl WaybarModule for NetworkModule { fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result { - let interface = get_primary_interface()?; + let (interface, ip, rx_mbps, tx_mbps) = if let Ok(s) = state.read() { + ( + s.network.interface.clone(), + s.network.ip.clone(), + s.network.rx_mbps, + s.network.tx_mbps, + ) + } else { + return Ok(WaybarOutput { + text: "No connection".to_string(), + tooltip: None, + class: None, + percentage: None, + }); + }; + if interface.is_empty() { return Ok(WaybarOutput { text: "No connection".to_string(), @@ -91,24 +118,16 @@ impl WaybarModule for NetworkModule { }); } - let ip = get_ip_address(&interface).unwrap_or_else(|| String::from("No IP")); - - let (rx_mbps, tx_mbps) = { - if let Ok(state_lock) = state.read() { - (state_lock.network.rx_mbps, state_lock.network.tx_mbps) - } else { - (0.0, 0.0) - } - }; + let ip_display = if ip.is_empty() { "No IP" } else { &ip }; let mut output_text = format_template( &config.network.format, &[ ("interface", TokenValue::String(&interface)), - ("ip", TokenValue::String(&ip)), + ("ip", TokenValue::String(ip_display)), ("rx", TokenValue::Float(rx_mbps)), ("tx", TokenValue::Float(tx_mbps)), - ] + ], ); if interface.starts_with("tun") @@ -116,12 +135,12 @@ impl WaybarModule for NetworkModule { || interface.starts_with("ppp") || interface.starts_with("pvpn") { - output_text = format!(" {}", output_text); + output_text = format!(" {}", output_text); } Ok(WaybarOutput { text: output_text, - tooltip: Some(format!("Interface: {}\nIP: {}", interface, ip)), + tooltip: Some(format!("Interface: {}\nIP: {}", interface, ip_display)), class: Some(interface), percentage: None, }) @@ -170,7 +189,7 @@ fn get_ip_address(interface: &str) -> Option { let stdout = String::from_utf8_lossy(&output.stdout); for line in stdout.lines() { if line.trim().starts_with("inet ") { - let parts: Vec<&str> = line.trim().split_whitespace().collect(); + let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() > 1 { let ip_cidr = parts[1]; let ip = ip_cidr.split('/').next().unwrap_or(ip_cidr); diff --git a/src/modules/power.rs b/src/modules/power.rs index 952f2ac..5bf7152 100644 --- a/src/modules/power.rs +++ b/src/modules/power.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template}; use anyhow::Result; use std::fs; @@ -32,11 +32,11 @@ impl WaybarModule for PowerModule { let name = entry.file_name().to_string_lossy().to_string(); if name.starts_with("AC") || name.starts_with("ADP") { let online_path = entry.path().join("online"); - if let Ok(online_str) = fs::read_to_string(online_path) { - if online_str.trim() == "1" { - ac_online = true; - break; - } + if let Ok(online_str) = fs::read_to_string(online_path) + && online_str.trim() == "1" + { + ac_online = true; + break; } } } @@ -60,9 +60,11 @@ impl WaybarModule for PowerModule { } }; - let capacity_str = fs::read_to_string(bat_path.join("capacity")).unwrap_or_else(|_| "0".to_string()); + 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()); + let status_str = + fs::read_to_string(bat_path.join("status")).unwrap_or_else(|_| "Unknown".to_string()); let state = status_str.trim().to_lowercase(); let (icon, class, tooltip) = if state == "charging" || ac_online { @@ -95,7 +97,7 @@ impl WaybarModule for PowerModule { &[ ("percentage", TokenValue::Int(percentage as i64)), ("icon", TokenValue::String(icon)), - ] + ], ); Ok(WaybarOutput { diff --git a/src/modules/sys.rs b/src/modules/sys.rs index f30fa95..9831d37 100644 --- a/src/modules/sys.rs +++ b/src/modules/sys.rs @@ -2,7 +2,7 @@ use crate::config::Config; use crate::modules::WaybarModule; use crate::output::WaybarOutput; use crate::state::SharedState; -use crate::utils::{format_template, TokenValue}; +use crate::utils::{TokenValue, format_template}; use anyhow::Result; pub struct SysModule; @@ -38,7 +38,7 @@ impl WaybarModule for SysModule { ("load1", TokenValue::Float(load1)), ("load5", TokenValue::Float(load5)), ("load15", TokenValue::Float(load15)), - ] + ], ); Ok(WaybarOutput { diff --git a/src/state.rs b/src/state.rs index 2d84b81..b520995 100644 --- a/src/state.rs +++ b/src/state.rs @@ -7,12 +7,23 @@ pub struct AppState { pub memory: MemoryState, pub sys: SysState, pub gpu: GpuState, + pub disks: Vec, +} + +#[derive(Default, Clone)] +pub struct DiskInfo { + pub mount_point: String, + pub filesystem: String, + pub total_bytes: u64, + pub available_bytes: u64, } #[derive(Default, Clone)] pub struct NetworkState { pub rx_mbps: f64, pub tx_mbps: f64, + pub interface: String, + pub ip: String, } #[derive(Clone)] diff --git a/src/utils.rs b/src/utils.rs index 9d73a55..fc57c9f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,21 @@ use anyhow::{Context, Result}; use std::io::Write; use std::process::{Command, Stdio}; +use tracing::warn; + +/// Run an external command and return its stdout as a trimmed String. +/// Provides clear error messages when the command is not found or fails. +pub fn run_command(cmd: &str, args: &[&str]) -> Result { + let output = Command::new(cmd) + .args(args) + .output() + .with_context(|| format!("'{}' not found or failed to execute. Is it installed?", cmd))?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + anyhow::bail!("'{}' exited with {}: {}", cmd, output.status, stderr.trim()); + } + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} pub fn show_menu(prompt: &str, items: &[String], menu_cmd: &str) -> Result { let cmd_str = menu_cmd.replace("{prompt}", prompt); @@ -14,11 +29,13 @@ pub fn show_menu(prompt: &str, items: &[String], menu_cmd: &str) -> Result String let name = &caps[1]; if let Some((_, val)) = values.iter().find(|(k, _)| *k == name) { let align = caps.get(2).map(|m| m.as_str()).unwrap_or(">"); - let width = caps.get(3).map(|m| m.as_str().parse::().unwrap_or(0)).unwrap_or(0); - let precision = caps.get(4).map(|m| m.as_str().parse::().unwrap_or(0)); + let width = caps + .get(3) + .map(|m| m.as_str().parse::().unwrap_or(0)) + .unwrap_or(0); + let precision = caps + .get(4) + .map(|m| m.as_str().parse::().unwrap_or(0)); match val { TokenValue::Float(f) => format_float(*f, align, width, precision), @@ -60,35 +82,36 @@ pub fn format_template(template: &str, values: &[(&str, TokenValue)]) -> String } else { caps[0].to_string() } - }).into_owned() + }) + .into_owned() } fn format_float(f: f64, align: &str, width: usize, precision: Option) -> String { match (align, precision) { - ("<", Some(p)) => format!("{: format!("{:^width$.p$}", f, width=width, p=p), - (">", Some(p)) => format!("{:>width$.p$}", f, width=width, p=p), - ("<", None) => format!("{: format!("{:^width$}", f, width=width), - (">", None) => format!("{:>width$}", f, width=width), + ("<", Some(p)) => format!("{: format!("{:^width$.p$}", f, width = width, p = p), + (">", Some(p)) => format!("{:>width$.p$}", f, width = width, p = p), + ("<", None) => format!("{: format!("{:^width$}", f, width = width), + (">", None) => format!("{:>width$}", f, width = width), _ => format!("{}", f), } } fn format_int(i: i64, align: &str, width: usize) -> String { match align { - "<" => format!("{: format!("{:^width$}", i, width=width), - ">" => format!("{:>width$}", i, width=width), + "<" => format!("{: format!("{:^width$}", i, width = width), + ">" => format!("{:>width$}", i, width = width), _ => format!("{}", i), } } fn format_str(s: &str, align: &str, width: usize) -> String { match align { - "<" => format!("{: format!("{:^width$}", s, width=width), - ">" => format!("{:>width$}", s, width=width), - _ => format!("{}", s), + "<" => format!("{: format!("{:^width$}", s, width = width), + ">" => format!("{:>width$}", s, width = width), + _ => s.to_string(), } }