init
This commit is contained in:
38
src/config.rs
Normal file
38
src/config.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub network: NetworkConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct NetworkConfig {
|
||||
pub format: String,
|
||||
}
|
||||
|
||||
impl Default for NetworkConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
format: "{interface} ({ip}): {rx} MB/s {tx} MB/s".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_config() -> Config {
|
||||
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| String::from("/"));
|
||||
PathBuf::from(home).join(".config")
|
||||
});
|
||||
let config_path = config_dir.join("fluxo/config.toml");
|
||||
|
||||
if let Ok(content) = fs::read_to_string(config_path) {
|
||||
toml::from_str(&content).unwrap_or_default()
|
||||
} else {
|
||||
Config::default()
|
||||
}
|
||||
}
|
||||
104
src/daemon.rs
Normal file
104
src/daemon.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use crate::config::Config;
|
||||
use crate::ipc::SOCKET_PATH;
|
||||
use crate::modules::network::NetworkDaemon;
|
||||
use crate::modules::hardware::HardwareDaemon;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::state::{AppState, SharedState};
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::os::unix::net::UnixListener;
|
||||
use std::sync::{Arc, RwLock};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tracing::{info, warn, error, debug};
|
||||
|
||||
pub fn run_daemon() -> Result<()> {
|
||||
if fs::metadata(SOCKET_PATH).is_ok() {
|
||||
debug!("Removing stale socket file: {}", SOCKET_PATH);
|
||||
fs::remove_file(SOCKET_PATH)?;
|
||||
}
|
||||
|
||||
let state: SharedState = Arc::new(RwLock::new(AppState::default()));
|
||||
let listener = UnixListener::bind(SOCKET_PATH)?;
|
||||
let config = crate::config::load_config();
|
||||
let config = Arc::new(config);
|
||||
|
||||
// Spawn the background polling thread
|
||||
let poll_state = Arc::clone(&state);
|
||||
thread::spawn(move || {
|
||||
info!("Starting background polling thread");
|
||||
let mut network_daemon = NetworkDaemon::new();
|
||||
let mut hardware_daemon = HardwareDaemon::new();
|
||||
loop {
|
||||
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);
|
||||
|
||||
for stream in listener.incoming() {
|
||||
match stream {
|
||||
Ok(mut stream) => {
|
||||
let state_clone = Arc::clone(&state);
|
||||
let config_clone = Arc::clone(&config);
|
||||
thread::spawn(move || {
|
||||
let mut reader = BufReader::new(stream.try_clone().unwrap());
|
||||
let mut request = String::new();
|
||||
if let Err(e) = reader.read_line(&mut request) {
|
||||
error!("Failed to read from IPC stream: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let request = request.trim();
|
||||
if request.is_empty() { return; }
|
||||
|
||||
let parts: Vec<&str> = request.split_whitespace().collect();
|
||||
if let Some(module_name) = parts.first() {
|
||||
debug!(module = module_name, args = ?&parts[1..], "Handling IPC request");
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => error!("Failed to accept incoming connection: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config: &Config) -> String {
|
||||
debug!(module = module_name, args = ?args, "Handling request");
|
||||
|
||||
let result = match module_name {
|
||||
"net" | "network" => crate::modules::network::NetworkModule.run(config, state, args),
|
||||
"cpu" => crate::modules::cpu::CpuModule.run(config, state, args),
|
||||
"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")]),
|
||||
_ => {
|
||||
warn!("Received request for unknown module: '{}'", module_name);
|
||||
Err(anyhow::anyhow!("Unknown module: {}", module_name))
|
||||
},
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(output) => serde_json::to_string(&output).unwrap_or_else(|_| "{}".to_string()),
|
||||
Err(e) => {
|
||||
let err_out = crate::output::WaybarOutput {
|
||||
text: "Error".to_string(),
|
||||
tooltip: Some(e.to_string()),
|
||||
class: Some("error".to_string()),
|
||||
percentage: None,
|
||||
};
|
||||
serde_json::to_string(&err_out).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/ipc.rs
Normal file
27
src/ipc.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use tracing::debug;
|
||||
|
||||
pub const SOCKET_PATH: &str = "/tmp/fluxo.sock";
|
||||
|
||||
pub fn request_data(module: &str, args: &[String]) -> anyhow::Result<String> {
|
||||
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 {
|
||||
request.push(' ');
|
||||
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)
|
||||
}
|
||||
118
src/main.rs
Normal file
118
src/main.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
mod config;
|
||||
mod daemon;
|
||||
mod ipc;
|
||||
mod modules;
|
||||
mod output;
|
||||
mod state;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::process;
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "fluxo")]
|
||||
#[command(about = "A high-performance daemon/client for Waybar custom modules", long_about = None)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Start the background polling daemon
|
||||
Daemon,
|
||||
/// Network speed module
|
||||
#[command(alias = "network")]
|
||||
Net,
|
||||
/// CPU usage and temp module
|
||||
Cpu,
|
||||
/// Memory usage module
|
||||
#[command(alias = "memory")]
|
||||
Mem,
|
||||
/// Disk usage module (path defaults to /)
|
||||
Disk {
|
||||
#[arg(default_value = "/")]
|
||||
path: String,
|
||||
},
|
||||
/// Storage pool aggregate module (e.g., btrfs)
|
||||
#[command(alias = "btrfs")]
|
||||
Pool {
|
||||
#[arg(default_value = "btrfs")]
|
||||
kind: String,
|
||||
},
|
||||
/// Audio volume (sink) control
|
||||
Vol {
|
||||
/// Cycle to the next available output device
|
||||
#[arg(short, long)]
|
||||
cycle: bool,
|
||||
},
|
||||
/// Microphone (source) control
|
||||
Mic {
|
||||
/// Cycle to the next available input device
|
||||
#[arg(short, long)]
|
||||
cycle: bool,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Initialize professional logging
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt::layer().with_target(false).pretty())
|
||||
.with(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into()))
|
||||
.init();
|
||||
|
||||
let cli = Cli::parse();
|
||||
|
||||
match &cli.command {
|
||||
Commands::Daemon => {
|
||||
info!("Starting Fluxo daemon...");
|
||||
if let Err(e) = daemon::run_daemon() {
|
||||
error!("Daemon failed: {}", 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::Vol { cycle } => {
|
||||
let action = if *cycle { "cycle" } else { "show" };
|
||||
handle_ipc_response(ipc::request_data("vol", &[action.to_string()]));
|
||||
}
|
||||
Commands::Mic { cycle } => {
|
||||
let action = if *cycle { "cycle" } else { "show" };
|
||||
handle_ipc_response(ipc::request_data("mic", &[action.to_string()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_ipc_response(response: anyhow::Result<String>) {
|
||||
match response {
|
||||
Ok(json_str) => {
|
||||
println!("{}", json_str);
|
||||
}
|
||||
Err(e) => {
|
||||
// Provide a graceful fallback JSON if the daemon isn't running
|
||||
let err_out = output::WaybarOutput {
|
||||
text: "Daemon offline".to_string(),
|
||||
tooltip: Some(e.to_string()),
|
||||
class: Some("error".to_string()),
|
||||
percentage: None,
|
||||
};
|
||||
println!("{}", serde_json::to_string(&err_out).unwrap());
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
src/modules/audio.rs
Normal file
154
src/modules/audio.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use anyhow::{Result, anyhow};
|
||||
use std::process::Command;
|
||||
|
||||
pub struct AudioModule;
|
||||
|
||||
impl WaybarModule for AudioModule {
|
||||
fn run(&self, _config: &Config, _state: &SharedState, args: &[&str]) -> Result<WaybarOutput> {
|
||||
let target_type = args.first().unwrap_or(&"sink");
|
||||
let action = args.get(1).unwrap_or(&"show");
|
||||
|
||||
match *action {
|
||||
"cycle" => {
|
||||
self.cycle_device(target_type)?;
|
||||
return Ok(WaybarOutput {
|
||||
text: String::new(),
|
||||
tooltip: None,
|
||||
class: None,
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
"show" | _ => {
|
||||
self.get_status(target_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioModule {
|
||||
fn get_status(&self, target_type: &str) -> Result<WaybarOutput> {
|
||||
let target = if target_type == "sink" { "@DEFAULT_AUDIO_SINK@" } else { "@DEFAULT_AUDIO_SOURCE@" };
|
||||
|
||||
// Get volume and mute status via wpctl (faster than pactl for this)
|
||||
let output = Command::new("wpctl")
|
||||
.args(["get-volume", target])
|
||||
.output()?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Output format: "Volume: 0.50" or "Volume: 0.50 [MUTED]"
|
||||
let parts: Vec<&str> = stdout.trim().split_whitespace().collect();
|
||||
if parts.len() < 2 {
|
||||
return Err(anyhow!("Could not parse wpctl output: {}", stdout));
|
||||
}
|
||||
|
||||
let vol_val: f64 = parts[1].parse().unwrap_or(0.0);
|
||||
let vol = (vol_val * 100.0).round() as u8;
|
||||
let display_vol = std::cmp::min(vol, 100);
|
||||
let muted = stdout.contains("[MUTED]");
|
||||
|
||||
let description = self.get_description(target_type)?;
|
||||
let name = if description.len() > 20 {
|
||||
format!("{}...", &description[..17])
|
||||
} else {
|
||||
description.clone()
|
||||
};
|
||||
|
||||
let (text, class) = if muted {
|
||||
let icon = if target_type == "sink" { "" } else { "" };
|
||||
(format!("{} {}", name, icon), "muted")
|
||||
} else {
|
||||
let icon = if target_type == "sink" {
|
||||
if display_vol <= 30 { "" }
|
||||
else if display_vol <= 60 { "" }
|
||||
else { "" }
|
||||
} else {
|
||||
""
|
||||
};
|
||||
(format!("{} {}% {}", name, display_vol, icon), "unmuted")
|
||||
};
|
||||
|
||||
Ok(WaybarOutput {
|
||||
text,
|
||||
tooltip: Some(description),
|
||||
class: Some(class.to_string()),
|
||||
percentage: Some(display_vol),
|
||||
})
|
||||
}
|
||||
|
||||
fn get_description(&self, target_type: &str) -> Result<String> {
|
||||
// Get the default device name
|
||||
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()
|
||||
.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))?;
|
||||
|
||||
// Get the description of that device
|
||||
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 mut current_name = String::new();
|
||||
for line in list_stdout.lines() {
|
||||
if line.trim().starts_with("Name: ") {
|
||||
current_name = line.split(':').nth(1).unwrap_or("").trim().to_string();
|
||||
}
|
||||
if current_name == default_dev && line.trim().starts_with("Description: ") {
|
||||
return Ok(line.split(':').nth(1).unwrap_or("").trim().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(default_dev.to_string())
|
||||
}
|
||||
|
||||
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 mut devices: Vec<String> = stdout.lines()
|
||||
.filter_map(|l| {
|
||||
let parts: Vec<&str> = l.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let name = parts[1].to_string();
|
||||
if target_type == "source" && name.contains(".monitor") {
|
||||
None
|
||||
} else {
|
||||
Some(name)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
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()
|
||||
.find(|l| l.contains(search_key))
|
||||
.and_then(|l| l.split(':').nth(1))
|
||||
.map(|s| s.trim())
|
||||
.unwrap_or("");
|
||||
|
||||
let current_index = devices.iter().position(|d| d == current_dev).unwrap_or(0);
|
||||
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" };
|
||||
Command::new("pactl").args([set_cmd, next_dev]).status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
53
src/modules/btrfs.rs
Normal file
53
src/modules/btrfs.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use anyhow::Result;
|
||||
use sysinfo::Disks;
|
||||
|
||||
pub struct BtrfsModule;
|
||||
|
||||
impl WaybarModule for BtrfsModule {
|
||||
fn run(&self, _config: &Config, _state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
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;
|
||||
total_size += size;
|
||||
total_used += size - available;
|
||||
}
|
||||
}
|
||||
|
||||
if total_size == 0.0 {
|
||||
return Ok(WaybarOutput {
|
||||
text: "No BTRFS".to_string(),
|
||||
tooltip: None,
|
||||
class: Some("normal".to_string()),
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
|
||||
let used_gb = total_used / 1024.0 / 1024.0 / 1024.0;
|
||||
let size_gb = total_size / 1024.0 / 1024.0 / 1024.0;
|
||||
let percentage = (total_used / total_size) * 100.0;
|
||||
|
||||
let class = if percentage > 95.0 {
|
||||
"max"
|
||||
} else if percentage > 80.0 {
|
||||
"high"
|
||||
} else {
|
||||
"normal"
|
||||
};
|
||||
|
||||
Ok(WaybarOutput {
|
||||
text: format!("{:.0}G / {:.0}G", used_gb, size_gb),
|
||||
tooltip: Some(format!("BTRFS Usage: {:.1}%", percentage)),
|
||||
class: Some(class.to_string()),
|
||||
percentage: Some(percentage as u8),
|
||||
})
|
||||
}
|
||||
}
|
||||
40
src/modules/cpu.rs
Normal file
40
src/modules/cpu.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct CpuModule;
|
||||
|
||||
impl WaybarModule for CpuModule {
|
||||
fn run(&self, _config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
|
||||
let (usage, temp, model) = {
|
||||
if let Ok(state_lock) = state.read() {
|
||||
(
|
||||
state_lock.cpu.usage,
|
||||
state_lock.cpu.temp,
|
||||
state_lock.cpu.model.clone(),
|
||||
)
|
||||
} else {
|
||||
(0.0, 0.0, String::from("Unknown"))
|
||||
}
|
||||
};
|
||||
|
||||
let text = format!("{:.1}% {:.1}C", usage, temp);
|
||||
|
||||
let class = if usage > 95.0 {
|
||||
"max"
|
||||
} else if usage > 75.0 {
|
||||
"high"
|
||||
} else {
|
||||
"normal"
|
||||
};
|
||||
|
||||
Ok(WaybarOutput {
|
||||
text: format!("CPU: {}", text),
|
||||
tooltip: Some(model),
|
||||
class: Some(class.to_string()),
|
||||
percentage: Some(usage as u8),
|
||||
})
|
||||
}
|
||||
}
|
||||
46
src/modules/disk.rs
Normal file
46
src/modules/disk.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use anyhow::Result;
|
||||
use sysinfo::Disks;
|
||||
|
||||
pub struct DiskModule;
|
||||
|
||||
impl WaybarModule for DiskModule {
|
||||
fn run(&self, _config: &Config, _state: &SharedState, args: &[&str]) -> Result<WaybarOutput> {
|
||||
let mountpoint = args.first().unwrap_or(&"/");
|
||||
|
||||
let disks = Disks::new_with_refreshed_list();
|
||||
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;
|
||||
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 class = if percentage > 95.0 {
|
||||
"max"
|
||||
} else if percentage > 80.0 {
|
||||
"high"
|
||||
} else {
|
||||
"normal"
|
||||
};
|
||||
|
||||
return Ok(WaybarOutput {
|
||||
text: format!("{} {:.1}G/{:.1}G", mountpoint, used_gb, total_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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!("Mountpoint {} not found", mountpoint))
|
||||
}
|
||||
}
|
||||
52
src/modules/hardware.rs
Normal file
52
src/modules/hardware.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use crate::state::SharedState;
|
||||
use sysinfo::{Components, System};
|
||||
|
||||
pub struct HardwareDaemon {
|
||||
sys: System,
|
||||
components: Components,
|
||||
}
|
||||
|
||||
impl HardwareDaemon {
|
||||
pub fn new() -> Self {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
let components = Components::new_with_refreshed_list();
|
||||
Self { sys, components }
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, state: SharedState) {
|
||||
self.sys.refresh_cpu_usage();
|
||||
self.sys.refresh_memory();
|
||||
self.components.refresh(true);
|
||||
|
||||
let cpu_usage = self.sys.global_cpu_usage();
|
||||
let cpu_model = self.sys.cpus().first().map(|c| c.brand().to_string()).unwrap_or_else(|| "Unknown".to_string());
|
||||
|
||||
// Try to find a reasonable CPU temperature
|
||||
// Often 'coretemp' or 'k10temp' depending on AMD/Intel
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total_mem = self.sys.total_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
|
||||
// Accurate used memory matching htop/free (Total - Available)
|
||||
let available_mem = self.sys.available_memory() as f64 / 1024.0 / 1024.0 / 1024.0;
|
||||
let used_mem = total_mem - available_mem;
|
||||
|
||||
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.model = cpu_model;
|
||||
|
||||
state_lock.memory.total_gb = total_mem;
|
||||
state_lock.memory.used_gb = used_mem;
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/modules/memory.rs
Normal file
39
src/modules/memory.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct MemoryModule;
|
||||
|
||||
impl WaybarModule for MemoryModule {
|
||||
fn run(&self, _config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
|
||||
let (used_gb, total_gb) = {
|
||||
if let Ok(state_lock) = state.read() {
|
||||
(
|
||||
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 class = if ratio > 95.0 {
|
||||
"max"
|
||||
} else if ratio > 75.0 {
|
||||
"high"
|
||||
} else {
|
||||
"normal"
|
||||
};
|
||||
|
||||
Ok(WaybarOutput {
|
||||
text: format!("{:.2}/{:.2}GB", used_gb, total_gb),
|
||||
tooltip: None,
|
||||
class: Some(class.to_string()),
|
||||
percentage: Some(ratio as u8),
|
||||
})
|
||||
}
|
||||
}
|
||||
17
src/modules/mod.rs
Normal file
17
src/modules/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
pub mod network;
|
||||
pub mod cpu;
|
||||
pub mod memory;
|
||||
pub mod hardware;
|
||||
pub mod disk;
|
||||
pub mod btrfs;
|
||||
pub mod audio;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use anyhow::Result;
|
||||
|
||||
pub trait WaybarModule {
|
||||
fn run(&self, config: &Config, state: &SharedState, args: &[&str]) -> Result<WaybarOutput>;
|
||||
}
|
||||
|
||||
179
src/modules/network.rs
Normal file
179
src/modules/network.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tracing::{debug, warn};
|
||||
|
||||
pub struct NetworkModule;
|
||||
|
||||
pub struct NetworkDaemon {
|
||||
last_time: u64,
|
||||
last_rx_bytes: u64,
|
||||
last_tx_bytes: u64,
|
||||
}
|
||||
|
||||
impl NetworkDaemon {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
last_time: 0,
|
||||
last_rx_bytes: 0,
|
||||
last_tx_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, state: SharedState) {
|
||||
if let Ok(interface) = get_primary_interface() {
|
||||
if !interface.is_empty() {
|
||||
let time_now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
|
||||
if let Ok((rx_bytes_now, tx_bytes_now)) = get_bytes(&interface) {
|
||||
if self.last_time > 0 && time_now > self.last_time {
|
||||
let time_diff = time_now - self.last_time;
|
||||
let rx_bps = (rx_bytes_now.saturating_sub(self.last_rx_bytes)) / time_diff;
|
||||
let tx_bps = (tx_bytes_now.saturating_sub(self.last_tx_bytes)) / time_diff;
|
||||
|
||||
let rx_mbps = (rx_bps as f64) / 1024.0 / 1024.0;
|
||||
let tx_mbps = (tx_bps as f64) / 1024.0 / 1024.0;
|
||||
|
||||
debug!(interface, rx = rx_mbps, tx = tx_mbps, "Network stats updated");
|
||||
|
||||
if let Ok(mut state_lock) = state.write() {
|
||||
state_lock.network.rx_mbps = rx_mbps;
|
||||
state_lock.network.tx_mbps = tx_mbps;
|
||||
}
|
||||
}
|
||||
|
||||
self.last_time = time_now;
|
||||
self.last_rx_bytes = rx_bytes_now;
|
||||
self.last_tx_bytes = tx_bytes_now;
|
||||
}
|
||||
} else {
|
||||
warn!("No primary network interface found during poll");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WaybarModule for NetworkModule {
|
||||
fn run(&self, config: &Config, state: &SharedState, _args: &[&str]) -> Result<WaybarOutput> {
|
||||
let interface = get_primary_interface()?;
|
||||
if interface.is_empty() {
|
||||
return Ok(WaybarOutput {
|
||||
text: "No connection".to_string(),
|
||||
tooltip: None,
|
||||
class: None,
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
|
||||
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 mut output_text = config
|
||||
.network
|
||||
.format
|
||||
.replace("{interface}", &interface)
|
||||
.replace("{ip}", &ip)
|
||||
.replace("{rx}", &format!("{:.2}", rx_mbps))
|
||||
.replace("{tx}", &format!("{:.2}", tx_mbps));
|
||||
|
||||
if interface.starts_with("tun")
|
||||
|| interface.starts_with("wg")
|
||||
|| interface.starts_with("ppp")
|
||||
|| interface.starts_with("pvpn")
|
||||
{
|
||||
output_text = format!(" {}", output_text);
|
||||
}
|
||||
|
||||
Ok(WaybarOutput {
|
||||
text: output_text,
|
||||
tooltip: Some(format!("Interface: {}\nIP: {}", interface, ip)),
|
||||
class: Some(interface),
|
||||
percentage: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn get_primary_interface() -> Result<String> {
|
||||
let output = std::process::Command::new("ip")
|
||||
.args(["route", "list"])
|
||||
.output()?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
let mut defaults = Vec::new();
|
||||
for line in stdout.lines() {
|
||||
if line.starts_with("default") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
let mut dev = "";
|
||||
let mut metric = 0;
|
||||
for i in 0..parts.len() {
|
||||
if parts[i] == "dev" && i + 1 < parts.len() {
|
||||
dev = parts[i + 1];
|
||||
}
|
||||
if parts[i] == "metric" && i + 1 < parts.len() {
|
||||
metric = parts[i + 1].parse::<i32>().unwrap_or(0);
|
||||
}
|
||||
}
|
||||
if !dev.is_empty() {
|
||||
defaults.push((metric, dev.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defaults.sort_by_key(|k| k.0);
|
||||
if let Some((_, dev)) = defaults.first() {
|
||||
Ok(dev.clone())
|
||||
} else {
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ip_address(interface: &str) -> Option<String> {
|
||||
let output = std::process::Command::new("ip")
|
||||
.args(["-4", "addr", "show", interface])
|
||||
.output()
|
||||
.ok()?;
|
||||
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();
|
||||
if parts.len() > 1 {
|
||||
let ip_cidr = parts[1];
|
||||
let ip = ip_cidr.split('/').next().unwrap_or(ip_cidr);
|
||||
return Some(ip.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn get_bytes(interface: &str) -> Result<(u64, u64)> {
|
||||
let rx_path = format!("/sys/class/net/{}/statistics/rx_bytes", interface);
|
||||
let tx_path = format!("/sys/class/net/{}/statistics/tx_bytes", interface);
|
||||
|
||||
let rx = fs::read_to_string(&rx_path)
|
||||
.unwrap_or_else(|_| "0".to_string())
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap_or(0);
|
||||
let tx = fs::read_to_string(&tx_path)
|
||||
.unwrap_or_else(|_| "0".to_string())
|
||||
.trim()
|
||||
.parse()
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok((rx, tx))
|
||||
}
|
||||
12
src/output.rs
Normal file
12
src/output.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct WaybarOutput {
|
||||
pub text: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tooltip: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub class: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub percentage: Option<u8>,
|
||||
}
|
||||
39
src/state.rs
Normal file
39
src/state.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct AppState {
|
||||
pub network: NetworkState,
|
||||
pub cpu: CpuState,
|
||||
pub memory: MemoryState,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct NetworkState {
|
||||
pub rx_mbps: f64,
|
||||
pub tx_mbps: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CpuState {
|
||||
pub usage: f64,
|
||||
pub temp: f64,
|
||||
pub model: String,
|
||||
}
|
||||
|
||||
impl Default for CpuState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
usage: 0.0,
|
||||
temp: 0.0,
|
||||
model: String::from("Unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
pub struct MemoryState {
|
||||
pub used_gb: f64,
|
||||
pub total_gb: f64,
|
||||
}
|
||||
|
||||
pub type SharedState = Arc<RwLock<AppState>>;
|
||||
Reference in New Issue
Block a user