2 Commits

Author SHA1 Message Date
nvrl c1b3d9134e clippy fixes 2026-03-30 15:53:03 +02:00
nvrl 78c004bcb4 improved performance 2026-03-30 15:52:23 +02:00
22 changed files with 692 additions and 482 deletions
Generated
+70 -117
View File
@@ -47,7 +47,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [ dependencies = [
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@@ -58,7 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell_polyfill", "once_cell_polyfill",
"windows-sys 0.61.2", "windows-sys",
] ]
[[package]] [[package]]
@@ -73,12 +73,27 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.6.0" version = "4.6.0"
@@ -125,29 +140,42 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 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]] [[package]]
name = "fluxo-rs" name = "fluxo-rs"
version = "0.1.0" version = "0.2.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
"fs4", "ctrlc",
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",
@@ -157,16 +185,6 @@ dependencies = [
"tracing-subscriber", "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]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.16.1" version = "0.16.1"
@@ -213,12 +231,6 @@ version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.29"
@@ -240,6 +252,18 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" 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]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.4.3" version = "0.4.3"
@@ -255,7 +279,16 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [ 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]] [[package]]
@@ -267,6 +300,12 @@ dependencies = [
"bitflags", "bitflags",
] ]
[[package]]
name = "objc2-encode"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
[[package]] [[package]]
name = "objc2-io-kit" name = "objc2-io-kit"
version = "0.3.2" version = "0.3.2"
@@ -342,19 +381,6 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" 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]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@@ -703,15 +729,6 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@@ -721,22 +738,6 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "windows-threading" name = "windows-threading"
version = "0.2.1" version = "0.2.1"
@@ -746,54 +747,6 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "winnow" name = "winnow"
version = "0.7.15" version = "0.7.15"
+2 -2
View File
@@ -1,12 +1,12 @@
[package] [package]
name = "fluxo-rs" name = "fluxo-rs"
version = "0.1.0" version = "0.2.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
anyhow = "1.0.102" anyhow = "1.0.102"
clap = { version = "4.6.0", features = ["derive"] } clap = { version = "4.6.0", features = ["derive"] }
fs4 = "0.13.1" ctrlc = "3"
regex = "1.10" regex = "1.10"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
+8 -3
View File
@@ -95,9 +95,11 @@ pub struct GpuConfig {
impl Default for GpuConfig { impl Default for GpuConfig {
fn default() -> Self { fn default() -> Self {
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_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<PathBuf>) -> Config {
} }
} }
} else { } 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() Config::default()
} }
} }
+75 -22
View File
@@ -1,62 +1,95 @@
use crate::config::Config; use crate::config::Config;
use crate::ipc::SOCKET_PATH; use crate::ipc::socket_path;
use crate::modules::network::NetworkDaemon;
use crate::modules::hardware::HardwareDaemon;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::modules::hardware::HardwareDaemon;
use crate::modules::network::NetworkDaemon;
use crate::state::{AppState, SharedState}; use crate::state::{AppState, SharedState};
use anyhow::Result; use anyhow::Result;
use std::fs; use std::fs;
use std::io::{BufRead, BufReader, Write}; use std::io::{BufRead, BufReader, Write};
use std::net::Shutdown;
use std::os::unix::net::UnixListener; use std::os::unix::net::UnixListener;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
use tracing::{info, warn, error, debug}; use tracing::{debug, error, info, warn};
use std::path::PathBuf;
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<PathBuf>) -> Result<()> { pub fn run_daemon(config_path: Option<PathBuf>) -> Result<()> {
if fs::metadata(SOCKET_PATH).is_ok() { let sock_path = socket_path();
debug!("Removing stale socket file: {}", SOCKET_PATH);
fs::remove_file(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 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 // We store the original config_path to allow proper reloading later
let config_path_clone = config_path.clone(); let config_path_clone = config_path.clone();
let config = Arc::new(RwLock::new(crate::config::load_config(config_path))); let config = Arc::new(RwLock::new(crate::config::load_config(config_path)));
let poll_state = Arc::clone(&state); let poll_state = Arc::clone(&state);
let poll_running = Arc::clone(&running);
thread::spawn(move || { thread::spawn(move || {
info!("Starting background polling thread"); info!("Starting background polling thread");
let mut network_daemon = NetworkDaemon::new(); let mut network_daemon = NetworkDaemon::new();
let mut hardware_daemon = HardwareDaemon::new(); let mut hardware_daemon = HardwareDaemon::new();
loop { while poll_running.load(Ordering::SeqCst) {
network_daemon.poll(Arc::clone(&poll_state)); network_daemon.poll(Arc::clone(&poll_state));
hardware_daemon.poll(Arc::clone(&poll_state)); hardware_daemon.poll(Arc::clone(&poll_state));
thread::sleep(Duration::from_secs(1)); 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() { // Use non-blocking accept so we can check the running flag
match stream { listener.set_nonblocking(true)?;
Ok(mut stream) => {
while running.load(Ordering::SeqCst) {
match listener.accept() {
Ok((mut stream, _)) => {
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
let config_clone = Arc::clone(&config); let config_clone = Arc::clone(&config);
let cp_clone = config_path_clone.clone(); let cp_clone = config_path_clone.clone();
thread::spawn(move || { thread::spawn(move || {
let mut reader = BufReader::new(stream.try_clone().unwrap()); let mut reader = BufReader::new(&stream);
let mut request = String::new(); let mut request = String::new();
if let Err(e) = reader.read_line(&mut request) { if let Err(e) = reader.read_line(&mut request) {
error!("Failed to read from IPC stream: {}", e); error!("Failed to read from IPC stream: {}", e);
return; return;
} }
drop(reader);
let request = request.trim(); let request = request.trim();
if request.is_empty() { return; } if request.is_empty() {
return;
}
let parts: Vec<&str> = request.split_whitespace().collect(); let parts: Vec<&str> = request.split_whitespace().collect();
if let Some(module_name) = parts.first() { if let Some(module_name) = parts.first() {
@@ -70,25 +103,37 @@ pub fn run_daemon(config_path: Option<PathBuf>) -> Result<()> {
} else { } else {
error!("Failed to acquire write lock for configuration reload."); error!("Failed to acquire write lock for configuration reload.");
} }
let _ = stream.shutdown(Shutdown::Write);
return; return;
} }
debug!(module = module_name, args = ?&parts[1..], "Handling IPC request"); 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()) { if let Err(e) = stream.write_all(response.as_bytes()) {
error!("Failed to write IPC response: {}", e); 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), Err(e) => error!("Failed to accept incoming connection: {}", e),
} }
} }
info!("Daemon shutting down gracefully.");
Ok(()) Ok(())
} }
fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_lock: &Arc<RwLock<Config>>) -> String { fn handle_request(
module_name: &str,
args: &[&str],
state: &SharedState,
config_lock: &Arc<RwLock<Config>>,
) -> String {
let config = if let Ok(c) = config_lock.read() { let config = if let Ok(c) = config_lock.read() {
c c
} else { } 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), "mem" | "memory" => crate::modules::memory::MemoryModule.run(&config, state, args),
"disk" => crate::modules::disk::DiskModule.run(&config, state, args), "disk" => crate::modules::disk::DiskModule.run(&config, state, args),
"pool" | "btrfs" => crate::modules::btrfs::BtrfsModule.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")]), "vol" => crate::modules::audio::AudioModule.run(
"mic" => crate::modules::audio::AudioModule.run(&config, state, &["source", args.get(0).unwrap_or(&"show")]), &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), "gpu" => crate::modules::gpu::GpuModule.run(&config, state, args),
"sys" => crate::modules::sys::SysModule.run(&config, state, args), "sys" => crate::modules::sys::SysModule.run(&config, state, args),
"bt" | "bluetooth" => crate::modules::bt::BtModule.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); warn!("Received request for unknown module: '{}'", module_name);
Err(anyhow::anyhow!("Unknown module: {}", module_name)) Err(anyhow::anyhow!("Unknown module: {}", module_name))
}, }
}; };
match result { match result {
@@ -121,7 +174,7 @@ fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_
Err(e) => { Err(e) => {
error!(module = module_name, error = %e, "Module execution failed"); error!(module = module_name, error = %e, "Module execution failed");
let err_out = crate::output::WaybarOutput { 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()), tooltip: Some(e.to_string()),
class: Some("error".to_string()), class: Some("error".to_string()),
percentage: None, percentage: None,
+13 -4
View File
@@ -1,12 +1,21 @@
use std::io::{Read, Write}; use std::io::{Read, Write};
use std::os::unix::net::UnixStream; use std::os::unix::net::UnixStream;
use std::time::Duration;
use tracing::debug; 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: &[String]) -> anyhow::Result<String> { pub fn request_data(module: &str, args: &[&str]) -> anyhow::Result<String> {
debug!(module, ?args, "Connecting to daemon socket: {}", SOCKET_PATH); let sock = socket_path();
let mut stream = UnixStream::connect(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)))?;
// Send module and args // Send module and args
let mut request = module.to_string(); let mut request = module.to_string();
+24 -22
View File
@@ -10,7 +10,7 @@ use clap::{Parser, Subcommand};
use std::path::PathBuf; use std::path::PathBuf;
use std::process; use std::process;
use tracing::{error, info}; use tracing::{error, info};
use tracing_subscriber::{fmt, prelude::*, EnvFilter}; use tracing_subscriber::{EnvFilter, fmt, prelude::*};
#[derive(Parser)] #[derive(Parser)]
#[command(name = "fluxo")] #[command(name = "fluxo")]
@@ -98,27 +98,25 @@ fn main() {
process::exit(1); process::exit(1);
} }
} }
Commands::Reload => { Commands::Reload => match ipc::request_data("reload", &[]) {
match ipc::request_data("reload", &[]) {
Ok(_) => info!("Reload signal sent to daemon"), Ok(_) => info!("Reload signal sent to daemon"),
Err(e) => { Err(e) => {
error!("Failed to send reload signal: {}", e); error!("Failed to send reload signal: {}", e);
process::exit(1); process::exit(1);
} }
} },
}
Commands::Net => handle_ipc_response(ipc::request_data("net", &[])), Commands::Net => handle_ipc_response(ipc::request_data("net", &[])),
Commands::Cpu => handle_ipc_response(ipc::request_data("cpu", &[])), Commands::Cpu => handle_ipc_response(ipc::request_data("cpu", &[])),
Commands::Mem => handle_ipc_response(ipc::request_data("mem", &[])), Commands::Mem => handle_ipc_response(ipc::request_data("mem", &[])),
Commands::Disk { path } => handle_ipc_response(ipc::request_data("disk", &[path.clone()])), Commands::Disk { path } => handle_ipc_response(ipc::request_data("disk", &[path])),
Commands::Pool { kind } => handle_ipc_response(ipc::request_data("pool", &[kind.clone()])), Commands::Pool { kind } => handle_ipc_response(ipc::request_data("pool", &[kind])),
Commands::Vol { cycle } => { Commands::Vol { cycle } => {
let action = if *cycle { "cycle" } else { "show" }; 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 } => { Commands::Mic { cycle } => {
let action = if *cycle { "cycle" } else { "show" }; 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::Gpu => handle_ipc_response(ipc::request_data("gpu", &[])),
Commands::Sys => handle_ipc_response(ipc::request_data("sys", &[])), Commands::Sys => handle_ipc_response(ipc::request_data("sys", &[])),
@@ -127,10 +125,16 @@ fn main() {
// Client-side execution of the menu // Client-side execution of the menu
let config = config::load_config(None); 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"]) .args(["devices"])
.output() .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 stdout = String::from_utf8_lossy(&devices_out.stdout);
let mut items = Vec::new(); let mut items = Vec::new();
@@ -144,24 +148,24 @@ fn main() {
} }
if !items.is_empty() { if !items.is_empty() {
if let Ok(selected) = utils::show_menu("Connect BT: ", &items, &config.general.menu_command) { if let Ok(selected) =
if let Some(mac_start) = selected.rfind('(') { utils::show_menu("Connect BT: ", &items, &config.general.menu_command)
if let Some(mac_end) = selected.rfind(')') { && let Some(mac_start) = selected.rfind('(')
&& let Some(mac_end) = selected.rfind(')')
{
let mac = &selected[mac_start + 1..mac_end]; let mac = &selected[mac_start + 1..mac_end];
let _ = std::process::Command::new("bluetoothctl") let _ = std::process::Command::new("bluetoothctl")
.args(["connect", mac]) .args(["connect", mac])
.status(); .status();
} }
}
}
} else { } else {
info!("No paired Bluetooth devices found."); info!("No paired Bluetooth devices found.");
} }
return; 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::Power => handle_ipc_response(ipc::request_data("power", &[])),
Commands::Game => handle_ipc_response(ipc::request_data("game", &[])), Commands::Game => handle_ipc_response(ipc::request_data("game", &[])),
} }
@@ -169,8 +173,7 @@ fn main() {
fn handle_ipc_response(response: anyhow::Result<String>) { fn handle_ipc_response(response: anyhow::Result<String>) {
match response { match response {
Ok(json_str) => { Ok(json_str) => match serde_json::from_str::<serde_json::Value>(&json_str) {
match serde_json::from_str::<serde_json::Value>(&json_str) {
Ok(mut val) => { Ok(mut val) => {
if let Some(text) = val.get_mut("text").and_then(|t| t.as_str()) { if let Some(text) = val.get_mut("text").and_then(|t| t.as_str()) {
let processed_text = if text.contains('<') { let processed_text = if text.contains('<') {
@@ -185,8 +188,7 @@ fn handle_ipc_response(response: anyhow::Result<String>) {
println!("{}", serde_json::to_string(&val).unwrap()); println!("{}", serde_json::to_string(&val).unwrap());
} }
Err(_) => println!("{}", json_str), Err(_) => println!("{}", json_str),
} },
}
Err(e) => { Err(e) => {
let err_out = output::WaybarOutput { let err_out = output::WaybarOutput {
text: format!("\u{200B}Daemon offline ({})\u{200B}", e), text: format!("\u{200B}Daemon offline ({})\u{200B}", e),
+69 -36
View File
@@ -2,7 +2,7 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template, run_command};
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use std::process::Command; use std::process::Command;
@@ -16,30 +16,30 @@ impl WaybarModule for AudioModule {
match *action { match *action {
"cycle" => { "cycle" => {
self.cycle_device(target_type)?; self.cycle_device(target_type)?;
return Ok(WaybarOutput { Ok(WaybarOutput {
text: String::new(), text: String::new(),
tooltip: None, tooltip: None,
class: None, class: None,
percentage: 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 { impl AudioModule {
fn get_status(&self, config: &Config, target_type: &str) -> Result<WaybarOutput> { fn get_status(&self, config: &Config, target_type: &str) -> Result<WaybarOutput> {
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") let stdout = run_command("wpctl", &["get-volume", target])?;
.args(["get-volume", target])
.output()?;
let stdout = String::from_utf8_lossy(&output.stdout);
let parts: Vec<&str> = stdout.trim().split_whitespace().collect(); let parts: Vec<&str> = stdout.split_whitespace().collect();
if parts.len() < 2 { if parts.len() < 2 {
return Err(anyhow!("Could not parse wpctl output: {}", stdout)); return Err(anyhow!("Could not parse wpctl output: {}", stdout));
} }
@@ -58,31 +58,43 @@ impl AudioModule {
let (text, class) = if muted { let (text, class) = if muted {
let icon = if target_type == "sink" { "" } else { "" }; 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( let t = format_template(
format_str, format_str,
&[ &[
("name", TokenValue::String(&name)), ("name", TokenValue::String(&name)),
("icon", TokenValue::String(icon)), ("icon", TokenValue::String(icon)),
] ],
); );
(t, "muted") (t, "muted")
} else { } else {
let icon = if target_type == "sink" { let icon = if target_type == "sink" {
if display_vol <= 30 { "" } if display_vol <= 30 {
else if display_vol <= 60 { "" } ""
else { "" } } else if display_vol <= 60 {
""
} else {
""
}
} 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( let t = format_template(
format_str, format_str,
&[ &[
("name", TokenValue::String(&name)), ("name", TokenValue::String(&name)),
("icon", TokenValue::String(icon)), ("icon", TokenValue::String(icon)),
("volume", TokenValue::Int(display_vol as i64)), ("volume", TokenValue::Int(display_vol as i64)),
] ],
); );
(t, "unmuted") (t, "unmuted")
}; };
@@ -96,19 +108,26 @@ impl AudioModule {
} }
fn get_description(&self, target_type: &str) -> Result<String> { fn get_description(&self, target_type: &str) -> Result<String> {
let info_output = Command::new("pactl").arg("info").output()?; let info_stdout = run_command("pactl", &["info"])?;
let info_stdout = String::from_utf8_lossy(&info_output.stdout); let search_key = if target_type == "sink" {
let search_key = if target_type == "sink" { "Default Sink:" } else { "Default Source:" }; "Default Sink:"
} else {
"Default Source:"
};
let default_dev = info_stdout.lines() let default_dev = info_stdout
.lines()
.find(|l| l.contains(search_key)) .find(|l| l.contains(search_key))
.and_then(|l| l.split(':').nth(1)) .and_then(|l| l.split(':').nth(1))
.map(|s| s.trim()) .map(|s| s.trim())
.ok_or_else(|| anyhow!("Default {} not found", target_type))?; .ok_or_else(|| anyhow!("Default {} not found", target_type))?;
let list_cmd = if target_type == "sink" { "sinks" } else { "sources" }; let list_cmd = if target_type == "sink" {
let list_output = Command::new("pactl").args(["list", list_cmd]).output()?; "sinks"
let list_stdout = String::from_utf8_lossy(&list_output.stdout); } else {
"sources"
};
let list_stdout = run_command("pactl", &["list", list_cmd])?;
let mut current_name = String::new(); let mut current_name = String::new();
for line in list_stdout.lines() { for line in list_stdout.lines() {
@@ -124,11 +143,15 @@ impl AudioModule {
} }
fn cycle_device(&self, target_type: &str) -> Result<()> { fn cycle_device(&self, target_type: &str) -> Result<()> {
let list_cmd = if target_type == "sink" { "sinks" } else { "sources" }; let list_cmd = if target_type == "sink" {
let output = Command::new("pactl").args(["list", "short", list_cmd]).output()?; "sinks"
let stdout = String::from_utf8_lossy(&output.stdout); } else {
"sources"
};
let stdout = run_command("pactl", &["list", "short", list_cmd])?;
let devices: Vec<String> = stdout.lines() let devices: Vec<String> = stdout
.lines()
.filter_map(|l| { .filter_map(|l| {
let parts: Vec<&str> = l.split_whitespace().collect(); let parts: Vec<&str> = l.split_whitespace().collect();
if parts.len() >= 2 { if parts.len() >= 2 {
@@ -144,13 +167,19 @@ impl AudioModule {
}) })
.collect(); .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 = run_command("pactl", &["info"])?;
let info_stdout = String::from_utf8_lossy(&info_output.stdout); let search_key = if target_type == "sink" {
let search_key = if target_type == "sink" { "Default Sink:" } else { "Default Source:" }; "Default Sink:"
} else {
"Default Source:"
};
let current_dev = info_stdout.lines() let current_dev = info_stdout
.lines()
.find(|l| l.contains(search_key)) .find(|l| l.contains(search_key))
.and_then(|l| l.split(':').nth(1)) .and_then(|l| l.split(':').nth(1))
.map(|s| s.trim()) .map(|s| s.trim())
@@ -160,7 +189,11 @@ impl AudioModule {
let next_index = (current_index + 1) % devices.len(); let next_index = (current_index + 1) % devices.len();
let next_dev = &devices[next_index]; 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()?; Command::new("pactl").args([set_cmd, next_dev]).status()?;
Ok(()) Ok(())
+18 -21
View File
@@ -2,7 +2,7 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template, run_command};
use anyhow::Result; use anyhow::Result;
use std::process::Command; use std::process::Command;
@@ -14,7 +14,9 @@ impl WaybarModule for BtModule {
if *action == "disconnect" { if *action == "disconnect" {
if let Some(mac) = find_audio_device() { 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 { return Ok(WaybarOutput {
text: String::new(), text: String::new(),
@@ -24,9 +26,8 @@ impl WaybarModule for BtModule {
}); });
} }
if let Ok(output) = Command::new("bluetoothctl").arg("show").output() { if let Ok(stdout) = run_command("bluetoothctl", &["show"])
let stdout = String::from_utf8_lossy(&output.stdout); && stdout.contains("Powered: no") {
if stdout.contains("Powered: no") {
return Ok(WaybarOutput { return Ok(WaybarOutput {
text: config.bt.format_disabled.clone(), text: config.bt.format_disabled.clone(),
tooltip: Some("Bluetooth Disabled".to_string()), tooltip: Some("Bluetooth Disabled".to_string()),
@@ -34,11 +35,9 @@ impl WaybarModule for BtModule {
percentage: None, percentage: None,
}); });
} }
}
if let Some(mac) = find_audio_device() { if let Some(mac) = find_audio_device() {
let info_output = Command::new("bluetoothctl").args(["info", &mac]).output()?; let info = run_command("bluetoothctl", &["info", &mac])?;
let info = String::from_utf8_lossy(&info_output.stdout);
let mut alias = mac.clone(); let mut alias = mac.clone();
let mut battery = None; let mut battery = None;
@@ -48,7 +47,8 @@ impl WaybarModule for BtModule {
if line.contains("Alias:") { if line.contains("Alias:") {
alias = line.split("Alias:").nth(1).unwrap_or("").trim().to_string(); alias = line.split("Alias:").nth(1).unwrap_or("").trim().to_string();
} else if line.contains("Battery Percentage:") { } 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::<u8>().ok(); battery = bat_str.parse::<u8>().ok();
} }
} else if line.contains("Trusted: yes") { } else if line.contains("Trusted: yes") {
@@ -61,12 +61,14 @@ impl WaybarModule for BtModule {
alias, alias,
mac, mac,
trusted, 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( let text = format_template(
&config.bt.format_connected, &config.bt.format_connected,
&[("alias", TokenValue::String(&alias))] &[("alias", TokenValue::String(&alias))],
); );
Ok(WaybarOutput { Ok(WaybarOutput {
@@ -87,33 +89,28 @@ impl WaybarModule for BtModule {
} }
fn find_audio_device() -> Option<String> { fn find_audio_device() -> Option<String> {
if let Ok(output) = Command::new("pactl").arg("get-default-sink").output() { if let Ok(sink) = run_command("pactl", &["get-default-sink"])
let sink = String::from_utf8_lossy(&output.stdout).trim().to_string(); && sink.starts_with("bluez_output.") {
if sink.starts_with("bluez_output.") {
let parts: Vec<&str> = sink.split('.').collect(); let parts: Vec<&str> = sink.split('.').collect();
if parts.len() >= 2 { if parts.len() >= 2 {
return Some(parts[1].replace('_', ":")); return Some(parts[1].replace('_', ":"));
} }
} }
}
if let Ok(output) = Command::new("bluetoothctl").args(["devices", "Connected"]).output() { if let Ok(stdout) = run_command("bluetoothctl", &["devices", "Connected"]) {
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() { for line in stdout.lines() {
if line.starts_with("Device ") { if line.starts_with("Device ") {
let parts: Vec<&str> = line.split_whitespace().collect(); let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 { if parts.len() >= 2 {
let mac = parts[1]; let mac = parts[1];
if let Ok(info) = Command::new("bluetoothctl").args(["info", mac]).output() { if let Ok(info_str) = run_command("bluetoothctl", &["info", mac])
let info_str = String::from_utf8_lossy(&info.stdout); && info_str.contains("0000110b-0000-1000-8000-00805f9b34fb") {
if info_str.contains("0000110b-0000-1000-8000-00805f9b34fb") {
return Some(mac.to_string()); return Some(mac.to_string());
} }
} }
} }
} }
} }
}
None None
} }
+12 -8
View File
@@ -2,22 +2,26 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template};
use anyhow::Result; use anyhow::Result;
use sysinfo::Disks;
pub struct BtrfsModule; pub struct BtrfsModule;
impl WaybarModule for BtrfsModule { impl WaybarModule for BtrfsModule {
fn run(&self, config: &Config, _state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> { fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
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"));
};
let mut total_used: f64 = 0.0; let mut total_used: f64 = 0.0;
let mut total_size: f64 = 0.0; let mut total_size: f64 = 0.0;
for disk in &disks { for disk in &disks {
if disk.file_system().to_string_lossy().to_lowercase().contains("btrfs") { if disk.filesystem.contains("btrfs") {
let size = disk.total_space() as f64; let size = disk.total_bytes as f64;
let available = disk.available_space() as f64; let available = disk.available_bytes as f64;
total_size += size; total_size += size;
total_used += size - available; total_used += size - available;
} }
@@ -49,7 +53,7 @@ impl WaybarModule for BtrfsModule {
&[ &[
("used", TokenValue::Float(used_gb)), ("used", TokenValue::Float(used_gb)),
("total", TokenValue::Float(size_gb)), ("total", TokenValue::Float(size_gb)),
] ],
); );
Ok(WaybarOutput { Ok(WaybarOutput {
+37 -12
View File
@@ -2,7 +2,7 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template};
use anyhow::Result; use anyhow::Result;
use std::process::Command; use std::process::Command;
@@ -24,27 +24,42 @@ impl WaybarModule for BudsModule {
_ => "active", _ => "active",
}; };
Command::new("pbpctrl").args(["set", "anc", next_mode]).status()?; Command::new("pbpctrl")
.args(["set", "anc", next_mode])
.status()?;
return Ok(WaybarOutput { return Ok(WaybarOutput {
text: String::new(), text: String::new(),
tooltip: None, class: None, percentage: None, tooltip: None,
class: None,
percentage: None,
}); });
} }
"connect" => { "connect" => {
Command::new("bluetoothctl").args(["connect", mac]).status()?; Command::new("bluetoothctl")
.args(["connect", mac])
.status()?;
return Ok(WaybarOutput { return Ok(WaybarOutput {
text: String::new(), text: String::new(),
tooltip: None, class: None, percentage: None, tooltip: None,
class: None,
percentage: None,
}); });
} }
"disconnect" => { "disconnect" => {
Command::new("bluetoothctl").args(["disconnect", mac]).status()?; Command::new("bluetoothctl")
.args(["disconnect", mac])
.status()?;
return Ok(WaybarOutput { return Ok(WaybarOutput {
text: String::new(), 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_info = Command::new("bluetoothctl").args(["info", mac]).output()?;
@@ -85,12 +100,22 @@ impl WaybarModule for BudsModule {
if left_bud == "unknown" && right_bud == "unknown" { if left_bud == "unknown" && right_bud == "unknown" {
return Ok(WaybarOutput { return Ok(WaybarOutput {
text: "{}".to_string(), 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 left_display = if left_bud == "unknown" {
let right_display = if right_bud == "unknown" { "---".to_string() } else { format!("{}%", right_bud) }; "---".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 anc_cmd = Command::new("pbpctrl").args(["get", "anc"]).output()?;
let current_mode = String::from_utf8_lossy(&anc_cmd.stdout).trim().to_string(); 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)), ("left", TokenValue::String(&left_display)),
("right", TokenValue::String(&right_display)), ("right", TokenValue::String(&right_display)),
("anc", TokenValue::String(anc_icon)), ("anc", TokenValue::String(anc_icon)),
] ],
); );
Ok(WaybarOutput { Ok(WaybarOutput {
+2 -2
View File
@@ -2,7 +2,7 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template};
use anyhow::Result; use anyhow::Result;
pub struct CpuModule; pub struct CpuModule;
@@ -26,7 +26,7 @@ impl WaybarModule for CpuModule {
&[ &[
("usage", TokenValue::Float(usage)), ("usage", TokenValue::Float(usage)),
("temp", TokenValue::Float(temp)), ("temp", TokenValue::Float(temp)),
] ],
); );
let class = if usage > 95.0 { let class = if usage > 95.0 {
+21 -10
View File
@@ -2,28 +2,36 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template};
use anyhow::Result; use anyhow::Result;
use sysinfo::Disks;
pub struct DiskModule; pub struct DiskModule;
impl WaybarModule for DiskModule { impl WaybarModule for DiskModule {
fn run(&self, config: &Config, _state: &SharedState, args: &[&str]) -> Result<WaybarOutput> { fn run(&self, config: &Config, state: &SharedState, args: &[&str]) -> Result<WaybarOutput> {
let mountpoint = args.first().unwrap_or(&"/"); 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 { for disk in &disks {
if disk.mount_point().to_string_lossy() == *mountpoint { if disk.mount_point == *mountpoint {
let total = disk.total_space() as f64; let total = disk.total_bytes as f64;
let available = disk.available_space() as f64; let available = disk.available_bytes as f64;
let used = total - available; let used = total - available;
let used_gb = used / 1024.0 / 1024.0 / 1024.0; let used_gb = used / 1024.0 / 1024.0 / 1024.0;
let total_gb = total / 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 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 { let class = if percentage > 95.0 {
"max" "max"
@@ -39,12 +47,15 @@ impl WaybarModule for DiskModule {
("mount", TokenValue::String(mountpoint)), ("mount", TokenValue::String(mountpoint)),
("used", TokenValue::Float(used_gb)), ("used", TokenValue::Float(used_gb)),
("total", TokenValue::Float(total_gb)), ("total", TokenValue::Float(total_gb)),
] ],
); );
return Ok(WaybarOutput { return Ok(WaybarOutput {
text, 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()), class: Some(class.to_string()),
percentage: Some(percentage as u8), percentage: Some(percentage as u8),
}); });
+4 -13
View File
@@ -2,25 +2,16 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::run_command;
use anyhow::Result; use anyhow::Result;
use std::process::Command;
pub struct GameModule; pub struct GameModule;
impl WaybarModule for GameModule { impl WaybarModule for GameModule {
fn run(&self, config: &Config, _state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> { fn run(&self, config: &Config, _state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
let output = Command::new("hyprctl") let is_gamemode = run_command("hyprctl", &["getoption", "animations:enabled", "-j"])
.args(["getoption", "animations:enabled", "-j"]) .map(|stdout| stdout.contains("\"int\": 0"))
.output(); .unwrap_or(false);
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;
}
}
if is_gamemode { if is_gamemode {
Ok(WaybarOutput { Ok(WaybarOutput {
+15 -4
View File
@@ -2,7 +2,7 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template};
use anyhow::Result; use anyhow::Result;
pub struct GpuModule; pub struct GpuModule;
@@ -21,7 +21,15 @@ impl WaybarModule for GpuModule {
state_lock.gpu.model.clone(), state_lock.gpu.model.clone(),
) )
} else { } 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_used", TokenValue::Float(vram_used)),
("vram_total", TokenValue::Float(vram_total)), ("vram_total", TokenValue::Float(vram_total)),
("temp", TokenValue::Float(temp)), ("temp", TokenValue::Float(temp)),
] ],
); );
let tooltip = if vendor == "Intel" { let tooltip = if vendor == "Intel" {
format!("Model: {}\nApprox Usage: {:.0}%", model, usage) format!("Model: {}\nApprox Usage: {:.0}%", model, usage)
} else { } 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 { Ok(WaybarOutput {
+95 -33
View File
@@ -1,18 +1,27 @@
use crate::state::SharedState; use crate::state::{DiskInfo, SharedState};
use sysinfo::{Components, System}; use sysinfo::{Components, Disks, System};
pub struct HardwareDaemon { pub struct HardwareDaemon {
sys: System, sys: System,
components: Components, components: Components,
gpu_vendor: Option<String>, gpu_vendor: Option<String>,
gpu_poll_counter: u8,
disk_poll_counter: u8,
} }
impl HardwareDaemon { impl HardwareDaemon {
pub fn new() -> Self { pub fn new() -> Self {
let mut sys = System::new_all(); let mut sys = System::new();
sys.refresh_all(); sys.refresh_cpu_usage();
sys.refresh_memory();
let components = Components::new_with_refreshed_list(); 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) { pub fn poll(&mut self, state: SharedState) {
@@ -21,15 +30,25 @@ impl HardwareDaemon {
self.components.refresh(true); self.components.refresh(true);
let cpu_usage = self.sys.global_cpu_usage(); 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; let mut cpu_temp = 0.0;
for component in &self.components { for component in &self.components {
let label = component.label().to_lowercase(); let label = component.label().to_lowercase();
if label.contains("tctl") || label.contains("cpu") || label.contains("package") || label.contains("temp1") { if (label.contains("tctl")
if let Some(temp) = component.temperature() { || label.contains("cpu")
|| label.contains("package")
|| label.contains("temp1"))
&& let Some(temp) = component.temperature()
{
cpu_temp = temp as f64; cpu_temp = temp as f64;
if cpu_temp > 0.0 { break; } if cpu_temp > 0.0 {
break;
} }
} }
} }
@@ -44,16 +63,16 @@ impl HardwareDaemon {
let mut process_count = 0; let mut process_count = 0;
if let Ok(loadavg_str) = std::fs::read_to_string("/proc/loadavg") { if let Ok(loadavg_str) = std::fs::read_to_string("/proc/loadavg") {
let parts: Vec<&str> = loadavg_str.split_whitespace().collect(); let parts: Vec<&str> = loadavg_str.split_whitespace().collect();
if parts.len() >= 4 { if parts.len() >= 4
if let Some(total_procs) = parts[3].split('/').nth(1) { && let Some(total_procs) = parts[3].split('/').nth(1)
{
process_count = total_procs.parse().unwrap_or(0); process_count = total_procs.parse().unwrap_or(0);
} }
} }
}
if let Ok(mut state_lock) = state.write() { if let Ok(mut state_lock) = state.write() {
state_lock.cpu.usage = cpu_usage as f64; 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.cpu.model = cpu_model;
state_lock.memory.total_gb = total_mem; state_lock.memory.total_gb = total_mem;
@@ -65,19 +84,40 @@ impl HardwareDaemon {
state_lock.sys.uptime = uptime; state_lock.sys.uptime = uptime;
state_lock.sys.process_count = process_count; state_lock.sys.process_count = process_count;
// 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); 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) { fn poll_gpu(&mut self, gpu: &mut crate::state::GpuState) {
gpu.active = false; gpu.active = false;
if self.gpu_vendor.as_deref() == Some("NVIDIA") || self.gpu_vendor.is_none() { if (self.gpu_vendor.as_deref() == Some("NVIDIA") || self.gpu_vendor.is_none())
if let Ok(output) = std::process::Command::new("nvidia-smi") && 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"]) .args([
"--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu,name",
"--format=csv,noheader,nounits",
])
.output() .output()
&& output.status.success()
{ {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
if let Some(line) = stdout.lines().next() { if let Some(line) = stdout.lines().next() {
let parts: Vec<&str> = line.split(',').collect(); let parts: Vec<&str> = line.split(',').collect();
@@ -94,24 +134,37 @@ impl HardwareDaemon {
} }
} }
} }
}
}
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 { for i in 0..=3 {
let base = format!("/sys/class/drm/card{}/device", i); let base = format!("/sys/class/drm/card{}/device", i);
if self.gpu_vendor.as_deref() == Some("AMD") || self.gpu_vendor.is_none() { 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)) { && let Ok(usage_str) =
std::fs::read_to_string(format!("{}/gpu_busy_percent", base))
{
gpu.active = true; gpu.active = true;
gpu.vendor = "AMD".to_string(); gpu.vendor = "AMD".to_string();
gpu.usage = usage_str.trim().parse().unwrap_or(0.0); 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)) { if let Ok(mem_used) =
gpu.vram_used = mem_used.trim().parse::<f64>().unwrap_or(0.0) / 1024.0 / 1024.0 / 1024.0; std::fs::read_to_string(format!("{}/mem_info_vram_used", base))
{
gpu.vram_used = mem_used.trim().parse::<f64>().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)) { if let Ok(mem_total) =
gpu.vram_total = mem_total.trim().parse::<f64>().unwrap_or(0.0) / 1024.0 / 1024.0 / 1024.0; std::fs::read_to_string(format!("{}/mem_info_vram_total", base))
{
gpu.vram_total = mem_total.trim().parse::<f64>().unwrap_or(0.0)
/ 1024.0
/ 1024.0
/ 1024.0;
} }
if let Ok(entries) = std::fs::read_dir(format!("{}/hwmon", base)) { if let Ok(entries) = std::fs::read_dir(format!("{}/hwmon", base)) {
@@ -127,19 +180,25 @@ impl HardwareDaemon {
self.gpu_vendor = Some("AMD".to_string()); self.gpu_vendor = Some("AMD".to_string());
return; return;
} }
}
if self.gpu_vendor.as_deref() == Some("Intel") || self.gpu_vendor.is_none() { 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() { let freq_path =
if std::path::Path::new(&format!("{}/gt_cur_freq_mhz", base)).exists() {
Some(format!("{}/gt_cur_freq_mhz", base)) Some(format!("{}/gt_cur_freq_mhz", base))
} else if std::path::Path::new(&format!("/sys/class/drm/card{}/gt_cur_freq_mhz", i)).exists() { } 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)) Some(format!("/sys/class/drm/card{}/gt_cur_freq_mhz", i))
} else { } else {
None None
}; };
if let Some(path) = freq_path { if let Some(path) = freq_path
if let Ok(freq_str) = std::fs::read_to_string(&path) { && let Ok(freq_str) = std::fs::read_to_string(&path)
{
gpu.active = true; gpu.active = true;
gpu.vendor = "Intel".to_string(); gpu.vendor = "Intel".to_string();
@@ -151,7 +210,11 @@ impl HardwareDaemon {
max_freq = max_str.trim().parse::<f64>().unwrap_or(0.0); max_freq = max_str.trim().parse::<f64>().unwrap_or(0.0);
} }
gpu.usage = if max_freq > 0.0 { (cur_freq / max_freq) * 100.0 } else { 0.0 }; gpu.usage = if max_freq > 0.0 {
(cur_freq / max_freq) * 100.0
} else {
0.0
};
gpu.temp = 0.0; gpu.temp = 0.0;
gpu.vram_used = 0.0; gpu.vram_used = 0.0;
gpu.vram_total = 0.0; gpu.vram_total = 0.0;
@@ -164,4 +227,3 @@ impl HardwareDaemon {
} }
} }
} }
}
+8 -7
View File
@@ -2,7 +2,7 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template};
use anyhow::Result; use anyhow::Result;
pub struct MemoryModule; pub struct MemoryModule;
@@ -11,23 +11,24 @@ impl WaybarModule for MemoryModule {
fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> { fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
let (used_gb, total_gb) = { let (used_gb, total_gb) = {
if let Ok(state_lock) = state.read() { 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 { } else {
(0.0, 0.0) (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( let text = format_template(
&config.memory.format, &config.memory.format,
&[ &[
("used", TokenValue::Float(used_gb)), ("used", TokenValue::Float(used_gb)),
("total", TokenValue::Float(total_gb)), ("total", TokenValue::Float(total_gb)),
] ],
); );
let class = if ratio > 95.0 { let class = if ratio > 95.0 {
+9 -10
View File
@@ -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 audio;
pub mod gpu;
pub mod sys;
pub mod bt; pub mod bt;
pub mod btrfs;
pub mod buds; pub mod buds;
pub mod power; pub mod cpu;
pub mod disk;
pub mod game; 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::config::Config;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
@@ -20,4 +20,3 @@ use anyhow::Result;
pub trait WaybarModule { pub trait WaybarModule {
fn run(&self, config: &Config, state: &SharedState, args: &[&str]) -> Result<WaybarOutput>; fn run(&self, config: &Config, state: &SharedState, args: &[&str]) -> Result<WaybarOutput>;
} }
+50 -31
View File
@@ -2,7 +2,7 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template};
use anyhow::Result; use anyhow::Result;
use std::fs; use std::fs;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
@@ -30,21 +30,20 @@ impl NetworkDaemon {
pub fn poll(&mut self, state: SharedState) { pub fn poll(&mut self, state: SharedState) {
// Cache invalidation: if the interface directory doesn't exist, clear cache // Cache invalidation: if the interface directory doesn't exist, clear cache
if let Some(ref iface) = self.cached_interface { if let Some(ref iface) = self.cached_interface
if !std::path::Path::new(&format!("/sys/class/net/{}", iface)).exists() { && !std::path::Path::new(&format!("/sys/class/net/{}", iface)).exists()
{
self.cached_interface = None; self.cached_interface = None;
self.cached_ip = None; self.cached_ip = None;
} }
}
// Re-detect interface if needed // Re-detect interface if needed
if self.cached_interface.is_none() { if self.cached_interface.is_none()
if let Ok(iface) = get_primary_interface() { && let Ok(iface) = get_primary_interface()
if !iface.is_empty() { && !iface.is_empty()
self.cached_interface = Some(iface.clone()); {
self.cached_ip = get_ip_address(&iface); self.cached_ip = get_ip_address(&iface);
} self.cached_interface = Some(iface);
}
} }
if let Some(ref interface) = self.cached_interface { 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 let Ok((rx_bytes_now, tx_bytes_now)) = get_bytes(interface) {
if self.last_time > 0 && time_now > self.last_time { if self.last_time > 0 && time_now > self.last_time {
let time_diff = time_now - self.last_time; let time_diff = (time_now - self.last_time) as f64;
let rx_bps = (rx_bytes_now.saturating_sub(self.last_rx_bytes)) / time_diff; let rx_mbps = (rx_bytes_now.saturating_sub(self.last_rx_bytes)) as f64
let tx_bps = (tx_bytes_now.saturating_sub(self.last_tx_bytes)) / time_diff; / time_diff
/ 1024.0
let rx_mbps = (rx_bps as f64) / 1024.0 / 1024.0; / 1024.0;
let tx_mbps = (tx_bps as f64) / 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() { if let Ok(mut state_lock) = state.write() {
state_lock.network.rx_mbps = rx_mbps; state_lock.network.rx_mbps = rx_mbps;
state_lock.network.tx_mbps = tx_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; self.last_time = time_now;
@@ -75,13 +83,32 @@ impl NetworkDaemon {
// Read failed, might be down // Read failed, might be down
self.cached_interface = None; 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 { impl WaybarModule for NetworkModule {
fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> { fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
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() { if interface.is_empty() {
return Ok(WaybarOutput { return Ok(WaybarOutput {
text: "No connection".to_string(), 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 ip_display = if ip.is_empty() { "No IP" } else { &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 mut output_text = format_template( let mut output_text = format_template(
&config.network.format, &config.network.format,
&[ &[
("interface", TokenValue::String(&interface)), ("interface", TokenValue::String(&interface)),
("ip", TokenValue::String(&ip)), ("ip", TokenValue::String(ip_display)),
("rx", TokenValue::Float(rx_mbps)), ("rx", TokenValue::Float(rx_mbps)),
("tx", TokenValue::Float(tx_mbps)), ("tx", TokenValue::Float(tx_mbps)),
] ],
); );
if interface.starts_with("tun") if interface.starts_with("tun")
@@ -116,12 +135,12 @@ impl WaybarModule for NetworkModule {
|| interface.starts_with("ppp") || interface.starts_with("ppp")
|| interface.starts_with("pvpn") || interface.starts_with("pvpn")
{ {
output_text = format!(" {}", output_text); output_text = format!(" {}", output_text);
} }
Ok(WaybarOutput { Ok(WaybarOutput {
text: output_text, text: output_text,
tooltip: Some(format!("Interface: {}\nIP: {}", interface, ip)), tooltip: Some(format!("Interface: {}\nIP: {}", interface, ip_display)),
class: Some(interface), class: Some(interface),
percentage: None, percentage: None,
}) })
@@ -170,7 +189,7 @@ fn get_ip_address(interface: &str) -> Option<String> {
let stdout = String::from_utf8_lossy(&output.stdout); let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() { for line in stdout.lines() {
if line.trim().starts_with("inet ") { 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 { if parts.len() > 1 {
let ip_cidr = parts[1]; let ip_cidr = parts[1];
let ip = ip_cidr.split('/').next().unwrap_or(ip_cidr); let ip = ip_cidr.split('/').next().unwrap_or(ip_cidr);
+9 -7
View File
@@ -2,7 +2,7 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template};
use anyhow::Result; use anyhow::Result;
use std::fs; use std::fs;
@@ -32,15 +32,15 @@ impl WaybarModule for PowerModule {
let name = entry.file_name().to_string_lossy().to_string(); let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with("AC") || name.starts_with("ADP") { if name.starts_with("AC") || name.starts_with("ADP") {
let online_path = entry.path().join("online"); let online_path = entry.path().join("online");
if let Ok(online_str) = fs::read_to_string(online_path) { if let Ok(online_str) = fs::read_to_string(online_path)
if online_str.trim() == "1" { && online_str.trim() == "1"
{
ac_online = true; ac_online = true;
break; break;
} }
} }
} }
} }
}
let Some(bat_path) = battery_path else { let Some(bat_path) = battery_path else {
if ac_online { if ac_online {
@@ -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 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 state = status_str.trim().to_lowercase();
let (icon, class, tooltip) = if state == "charging" || ac_online { let (icon, class, tooltip) = if state == "charging" || ac_online {
@@ -95,7 +97,7 @@ impl WaybarModule for PowerModule {
&[ &[
("percentage", TokenValue::Int(percentage as i64)), ("percentage", TokenValue::Int(percentage as i64)),
("icon", TokenValue::String(icon)), ("icon", TokenValue::String(icon)),
] ],
); );
Ok(WaybarOutput { Ok(WaybarOutput {
+2 -2
View File
@@ -2,7 +2,7 @@ use crate::config::Config;
use crate::modules::WaybarModule; use crate::modules::WaybarModule;
use crate::output::WaybarOutput; use crate::output::WaybarOutput;
use crate::state::SharedState; use crate::state::SharedState;
use crate::utils::{format_template, TokenValue}; use crate::utils::{TokenValue, format_template};
use anyhow::Result; use anyhow::Result;
pub struct SysModule; pub struct SysModule;
@@ -38,7 +38,7 @@ impl WaybarModule for SysModule {
("load1", TokenValue::Float(load1)), ("load1", TokenValue::Float(load1)),
("load5", TokenValue::Float(load5)), ("load5", TokenValue::Float(load5)),
("load15", TokenValue::Float(load15)), ("load15", TokenValue::Float(load15)),
] ],
); );
Ok(WaybarOutput { Ok(WaybarOutput {
+11
View File
@@ -7,12 +7,23 @@ pub struct AppState {
pub memory: MemoryState, pub memory: MemoryState,
pub sys: SysState, pub sys: SysState,
pub gpu: GpuState, pub gpu: GpuState,
pub disks: Vec<DiskInfo>,
}
#[derive(Default, Clone)]
pub struct DiskInfo {
pub mount_point: String,
pub filesystem: String,
pub total_bytes: u64,
pub available_bytes: u64,
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct NetworkState { pub struct NetworkState {
pub rx_mbps: f64, pub rx_mbps: f64,
pub tx_mbps: f64, pub tx_mbps: f64,
pub interface: String,
pub ip: String,
} }
#[derive(Clone)] #[derive(Clone)]
+27 -5
View File
@@ -2,6 +2,20 @@ use anyhow::{Context, Result};
use std::io::Write; use std::io::Write;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
/// 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<String> {
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<String> { pub fn show_menu(prompt: &str, items: &[String], menu_cmd: &str) -> Result<String> {
let cmd_str = menu_cmd.replace("{prompt}", prompt); let cmd_str = menu_cmd.replace("{prompt}", prompt);
let mut child = Command::new("sh") let mut child = Command::new("sh")
@@ -14,7 +28,9 @@ pub fn show_menu(prompt: &str, items: &[String], menu_cmd: &str) -> Result<Strin
if let Some(mut stdin) = child.stdin.take() { if let Some(mut stdin) = child.stdin.take() {
let input = items.join("\n"); let input = items.join("\n");
stdin.write_all(input.as_bytes()).context("Failed to write to menu stdin")?; stdin
.write_all(input.as_bytes())
.context("Failed to write to menu stdin")?;
} }
let output = child.wait_with_output().context("Failed to wait on menu")?; let output = child.wait_with_output().context("Failed to wait on menu")?;
@@ -49,8 +65,13 @@ pub fn format_template(template: &str, values: &[(&str, TokenValue)]) -> String
let name = &caps[1]; let name = &caps[1];
if let Some((_, val)) = values.iter().find(|(k, _)| *k == name) { if let Some((_, val)) = values.iter().find(|(k, _)| *k == name) {
let align = caps.get(2).map(|m| m.as_str()).unwrap_or(">"); let align = caps.get(2).map(|m| m.as_str()).unwrap_or(">");
let width = caps.get(3).map(|m| m.as_str().parse::<usize>().unwrap_or(0)).unwrap_or(0); let width = caps
let precision = caps.get(4).map(|m| m.as_str().parse::<usize>().unwrap_or(0)); .get(3)
.map(|m| m.as_str().parse::<usize>().unwrap_or(0))
.unwrap_or(0);
let precision = caps
.get(4)
.map(|m| m.as_str().parse::<usize>().unwrap_or(0));
match val { match val {
TokenValue::Float(f) => format_float(*f, align, width, precision), TokenValue::Float(f) => format_float(*f, align, width, precision),
@@ -60,7 +81,8 @@ pub fn format_template(template: &str, values: &[(&str, TokenValue)]) -> String
} else { } else {
caps[0].to_string() caps[0].to_string()
} }
}).into_owned() })
.into_owned()
} }
fn format_float(f: f64, align: &str, width: usize, precision: Option<usize>) -> String { fn format_float(f: f64, align: &str, width: usize, precision: Option<usize>) -> String {
@@ -89,6 +111,6 @@ fn format_str(s: &str, align: &str, width: usize) -> String {
"<" => format!("{:<width$}", s, width = width), "<" => format!("{:<width$}", s, width = width),
"^" => format!("{:^width$}", s, width = width), "^" => format!("{:^width$}", s, width = width),
">" => format!("{:>width$}", s, width = width), ">" => format!("{:>width$}", s, width = width),
_ => format!("{}", s), _ => s.to_string(),
} }
} }