fixed some overhead + updated readme
This commit is contained in:
96
README.md
96
README.md
@@ -5,50 +5,57 @@ fluxo-rs is a high-performance system metrics daemon and client designed specifi
|
|||||||
## description
|
## description
|
||||||
|
|
||||||
the project follows a client-server architecture:
|
the project follows a client-server architecture:
|
||||||
- daemon: handles heavy lifting (polling cpu, memory, network) and stores state in memory.
|
- daemon: handles heavy lifting (polling cpu, memory, network, gpu) and stores state in memory.
|
||||||
- client: a thin cli wrapper that connects to the daemon's socket to retrieve formatted json for waybar.
|
- client: a thin cli wrapper that connects to the daemon's socket to retrieve formatted json for waybar.
|
||||||
|
|
||||||
this approach eliminates process spawning overhead and temporary file locking, resulting in near-zero cpu usage for custom modules.
|
this approach eliminates process spawning overhead and temporary file locking, resulting in near-zero cpu usage for custom modules.
|
||||||
|
|
||||||
|
## features
|
||||||
|
|
||||||
|
- ultra-lightweight: background polling is highly optimized (e.g., O(1) process counting).
|
||||||
|
- jitter-free: uses zero-width sentinels and figure spaces to prevent waybar from trimming padding.
|
||||||
|
- configurable: customizable output formats via toml config.
|
||||||
|
- live reload: configuration can be reloaded without restarting the daemon.
|
||||||
|
- multi-vendor gpu: native support for intel (igpu), amd, and nvidia.
|
||||||
|
|
||||||
## modules
|
## modules
|
||||||
|
|
||||||
- net: network interface speed (rx/tx mb/s)
|
| command | description | tokens |
|
||||||
- cpu: global usage percentage and package temperature
|
| :--- | :--- | :--- |
|
||||||
- mem: used/total ram in gigabytes
|
| `net` | network speed (rx/tx) | `{interface}`, `{ip}`, `{rx}`, `{tx}` |
|
||||||
- disk: disk usage for a specific mountpoint
|
| `cpu` | cpu usage and temp | `{usage}`, `{temp}` |
|
||||||
- pool: aggregate storage usage (e.g., btrfs)
|
| `mem` | memory usage | `{used}`, `{total}` |
|
||||||
- vol: audio output volume and device management
|
| `gpu` | gpu utilization | `{usage}`, `{vram_used}`, `{vram_total}`, `{temp}` |
|
||||||
- mic: audio input volume and device management
|
| `sys` | system load and uptime | `{uptime}`, `{load1}`, `{load5}`, `{load15}` |
|
||||||
|
| `disk` | disk usage (default: /) | `{mount}`, `{used}`, `{total}` |
|
||||||
## dependencies
|
| `pool` | aggregate storage (btrfs) | `{used}`, `{total}` |
|
||||||
|
| `vol` | audio output volume | `{percentage}`, `{icon}` |
|
||||||
### system
|
| `mic` | audio input volume | `{percentage}`, `{icon}` |
|
||||||
- iproute2 (for network interface discovery)
|
| `bt` | bluetooth status | device name and battery |
|
||||||
- wireplumber (for volume and mute status via wpctl)
|
| `buds` | pixel buds pro control | left/right battery and anc state |
|
||||||
- pulseaudio (for device description and cycling via pactl)
|
| `power` | battery and ac status | `{percentage}`, `{icon}` |
|
||||||
- lm-sensors (recommended for cpu temperature)
|
| `game` | hyprland gamemode status | active/inactive icon |
|
||||||
|
|
||||||
### rust
|
|
||||||
- cargo / rustc (edition 2024)
|
|
||||||
|
|
||||||
## setup
|
## setup
|
||||||
|
|
||||||
1. build the project:
|
1. build the project:
|
||||||
```
|
```bash
|
||||||
$ git clone https://git.narl.io/nvrl/fluxo-rs
|
cd fluxo-rs
|
||||||
$ cd fluxo-rs
|
cargo build --release
|
||||||
$ cargo build --release
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. start the daemon:
|
2. start the daemon:
|
||||||
```
|
```bash
|
||||||
$ ./target/release/fluxo-rs daemon &
|
./target/release/fluxo-rs daemon &
|
||||||
```
|
```
|
||||||
|
|
||||||
3. configure waybar (config.jsonc):
|
3. configuration:
|
||||||
```
|
create `~/.config/fluxo/config.toml` (see `example.config.toml` for all options).
|
||||||
|
|
||||||
|
4. waybar configuration (`config.jsonc`):
|
||||||
|
```json
|
||||||
"custom/cpu": {
|
"custom/cpu": {
|
||||||
"exec": "/path/to/fluxo-rs cpu",
|
"exec": "~/path/to/fluxo-rs cpu",
|
||||||
"return-type": "json"
|
"return-type": "json"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -56,25 +63,26 @@ this approach eliminates process spawning overhead and temporary file locking, r
|
|||||||
## development
|
## development
|
||||||
|
|
||||||
### architecture
|
### architecture
|
||||||
- src/main.rs: entry point and cli argument parsing
|
- `src/main.rs`: entry point, cli parsing, and client-side formatting logic.
|
||||||
- src/daemon.rs: uds listener and background thread orchestration
|
- `src/daemon.rs`:uds listener, configuration management, and polling orchestration.
|
||||||
- src/ipc.rs: thin client socket communication
|
- `src/ipc.rs`: unix domain socket communication logic.
|
||||||
- src/modules/: individual metric implementation logic
|
- `src/modules/`: individual metric implementations.
|
||||||
- src/state.rs: shared in-memory data structures
|
- `src/state.rs`: shared thread-safe data structures.
|
||||||
|
|
||||||
### adding a module
|
### adding a module
|
||||||
1. define the state structure in state.rs
|
1. add the required fields to `src/state.rs`.
|
||||||
2. implement the waybarmodule trait in src/modules/
|
2. implement the `WaybarModule` trait in a new file in `src/modules/`.
|
||||||
3. add the polling logic to the background thread in daemon.rs
|
3. add polling logic to `src/modules/hardware.rs` or `src/daemon.rs`.
|
||||||
4. register the subcommand in main.rs
|
4. register the new subcommand in `src/main.rs` and the router in `src/daemon.rs`.
|
||||||
|
|
||||||
### build and debug
|
### configuration reload
|
||||||
build for release:
|
the daemon can reload its configuration live:
|
||||||
```
|
```bash
|
||||||
$ cargo build --release
|
fluxo-rs reload
|
||||||
```
|
```
|
||||||
|
|
||||||
run with debug logs:
|
### logs
|
||||||
```
|
run the daemon with debug logs for troubleshooting:
|
||||||
$ RUST_LOG=debug ./target/release/fluxo-rs daemon
|
```bash
|
||||||
|
RUST_LOG=debug fluxo-rs daemon
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub struct Config {
|
|||||||
pub pool: PoolConfig,
|
pub pool: PoolConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub power: PowerConfig,
|
pub power: PowerConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub buds: BudsConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@@ -130,6 +132,19 @@ impl Default for PowerConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct BudsConfig {
|
||||||
|
pub mac: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BudsConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mac: "B4:23:A2:09:D3:53".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_config() -> Config {
|
pub fn load_config() -> Config {
|
||||||
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ pub fn run_daemon() -> Result<()> {
|
|||||||
|
|
||||||
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(SOCKET_PATH)?;
|
||||||
let config = crate::config::load_config();
|
let config = Arc::new(RwLock::new(crate::config::load_config()));
|
||||||
let config = Arc::new(config);
|
|
||||||
|
|
||||||
// Spawn the background polling thread
|
// Spawn the background polling thread
|
||||||
let poll_state = Arc::clone(&state);
|
let poll_state = Arc::clone(&state);
|
||||||
@@ -57,6 +56,16 @@ pub fn run_daemon() -> Result<()> {
|
|||||||
|
|
||||||
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() {
|
||||||
|
if *module_name == "reload" {
|
||||||
|
info!("Reloading configuration...");
|
||||||
|
let new_config = crate::config::load_config();
|
||||||
|
if let Ok(mut config_lock) = config_clone.write() {
|
||||||
|
*config_lock = new_config;
|
||||||
|
let _ = stream.write_all(b"{\"text\":\"ok\"}");
|
||||||
|
}
|
||||||
|
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()) {
|
||||||
@@ -72,23 +81,30 @@ pub fn run_daemon() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config: &Config) -> String {
|
fn handle_request(module_name: &str, args: &[&str], state: &SharedState, config_lock: &Arc<RwLock<Config>>) -> String {
|
||||||
debug!(module = module_name, args = ?args, "Handling request");
|
debug!(module = module_name, args = ?args, "Handling request");
|
||||||
|
|
||||||
|
let config = if let Ok(c) = config_lock.read() {
|
||||||
|
c
|
||||||
|
} else {
|
||||||
|
// Fallback to default if lock fails (should not happen normally)
|
||||||
|
return "{\"text\":\"error: config lock failed\"}".to_string();
|
||||||
|
};
|
||||||
|
|
||||||
let result = match module_name {
|
let result = match module_name {
|
||||||
"net" | "network" => crate::modules::network::NetworkModule.run(config, state, args),
|
"net" | "network" => crate::modules::network::NetworkModule.run(&config, state, args),
|
||||||
"cpu" => crate::modules::cpu::CpuModule.run(config, state, args),
|
"cpu" => crate::modules::cpu::CpuModule.run(&config, state, args),
|
||||||
"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(&config, state, &["sink", args.get(0).unwrap_or(&"show")]),
|
||||||
"mic" => crate::modules::audio::AudioModule.run(config, state, &["source", args.get(0).unwrap_or(&"show")]),
|
"mic" => crate::modules::audio::AudioModule.run(&config, state, &["source", args.get(0).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),
|
||||||
"buds" => crate::modules::buds::BudsModule.run(config, state, args),
|
"buds" => crate::modules::buds::BudsModule.run(&config, state, args),
|
||||||
"power" => crate::modules::power::PowerModule.run(config, state, args),
|
"power" => crate::modules::power::PowerModule.run(&config, state, args),
|
||||||
"game" => crate::modules::game::GameModule.run(config, state, args),
|
"game" => crate::modules::game::GameModule.run(&config, state, args),
|
||||||
_ => {
|
_ => {
|
||||||
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))
|
||||||
|
|||||||
13
src/main.rs
13
src/main.rs
@@ -22,6 +22,8 @@ struct Cli {
|
|||||||
enum Commands {
|
enum Commands {
|
||||||
/// Start the background polling daemon
|
/// Start the background polling daemon
|
||||||
Daemon,
|
Daemon,
|
||||||
|
/// Reload the daemon configuration
|
||||||
|
Reload,
|
||||||
/// Network speed module
|
/// Network speed module
|
||||||
#[command(alias = "network")]
|
#[command(alias = "network")]
|
||||||
Net,
|
Net,
|
||||||
@@ -90,6 +92,15 @@ fn main() {
|
|||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Commands::Reload => {
|
||||||
|
match ipc::request_data("reload", &[]) {
|
||||||
|
Ok(_) => info!("Reload signal sent to daemon"),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to send reload signal: {}", e);
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Commands::Net => handle_ipc_response(ipc::request_data("net", &[])),
|
Commands::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", &[])),
|
||||||
@@ -118,8 +129,6 @@ fn handle_ipc_response(response: anyhow::Result<String>) {
|
|||||||
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()) {
|
||||||
// 1. Replace regular spaces with Figure Spaces (\u2007) for perfect numeric alignment
|
|
||||||
// 2. Wrap the text in Zero-Width Spaces (\u200B) to prevent Waybar from trimming
|
|
||||||
let fixed_text = format!("\u{200B}{}\u{200B}", text.replace(' ', "\u{2007}"));
|
let fixed_text = format!("\u{200B}{}\u{200B}", text.replace(' ', "\u{2007}"));
|
||||||
val["text"] = serde_json::Value::String(fixed_text);
|
val["text"] = serde_json::Value::String(fixed_text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ use std::process::Command;
|
|||||||
|
|
||||||
pub struct BudsModule;
|
pub struct BudsModule;
|
||||||
|
|
||||||
const MAC_ADDRESS: &str = "B4:23:A2:09:D3:53";
|
|
||||||
|
|
||||||
impl WaybarModule for BudsModule {
|
impl WaybarModule for BudsModule {
|
||||||
fn run(&self, _config: &Config, _state: &SharedState, args: &[&str]) -> Result<WaybarOutput> {
|
fn run(&self, config: &Config, _state: &SharedState, args: &[&str]) -> Result<WaybarOutput> {
|
||||||
let action = args.first().unwrap_or(&"show");
|
let action = args.first().unwrap_or(&"show");
|
||||||
|
let mac = &config.buds.mac;
|
||||||
|
|
||||||
match *action {
|
match *action {
|
||||||
"cycle_anc" => {
|
"cycle_anc" => {
|
||||||
@@ -33,7 +32,7 @@ impl WaybarModule for BudsModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
"connect" => {
|
"connect" => {
|
||||||
Command::new("bluetoothctl").args(["connect", MAC_ADDRESS]).status()?;
|
Command::new("bluetoothctl").args(["connect", mac]).status()?;
|
||||||
return Ok(WaybarOutput {
|
return Ok(WaybarOutput {
|
||||||
text: String::new(),
|
text: String::new(),
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
@@ -42,7 +41,7 @@ impl WaybarModule for BudsModule {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
"disconnect" => {
|
"disconnect" => {
|
||||||
Command::new("bluetoothctl").args(["disconnect", MAC_ADDRESS]).status()?;
|
Command::new("bluetoothctl").args(["disconnect", mac]).status()?;
|
||||||
return Ok(WaybarOutput {
|
return Ok(WaybarOutput {
|
||||||
text: String::new(),
|
text: String::new(),
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
@@ -54,7 +53,7 @@ impl WaybarModule for BudsModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if connected
|
// Check if connected
|
||||||
let bt_info = Command::new("bluetoothctl").args(["info", MAC_ADDRESS]).output()?;
|
let bt_info = Command::new("bluetoothctl").args(["info", mac]).output()?;
|
||||||
let bt_str = String::from_utf8_lossy(&bt_info.stdout);
|
let bt_str = String::from_utf8_lossy(&bt_info.stdout);
|
||||||
|
|
||||||
if !bt_str.contains("Connected: yes") {
|
if !bt_str.contains("Connected: yes") {
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ impl HardwareDaemon {
|
|||||||
let load_avg = System::load_average();
|
let load_avg = System::load_average();
|
||||||
let uptime = System::uptime();
|
let uptime = System::uptime();
|
||||||
|
|
||||||
// Fast O(1) process count by reading loadavg instead of heavy sysinfo process refresh
|
|
||||||
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();
|
||||||
@@ -73,7 +72,6 @@ impl HardwareDaemon {
|
|||||||
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;
|
||||||
|
|
||||||
// Fast path: if we already detected NVIDIA, don't fallback to AMD/Intel scanning
|
|
||||||
if self.gpu_vendor.as_deref() == Some("NVIDIA") || self.gpu_vendor.is_none() {
|
if self.gpu_vendor.as_deref() == Some("NVIDIA") || self.gpu_vendor.is_none() {
|
||||||
if let Ok(output) = std::process::Command::new("nvidia-smi")
|
if let Ok(output) = std::process::Command::new("nvidia-smi")
|
||||||
.args(["--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu,name", "--format=csv,noheader,nounits"])
|
.args(["--query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu,name", "--format=csv,noheader,nounits"])
|
||||||
@@ -99,7 +97,6 @@ impl HardwareDaemon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: if we detected AMD or Intel, scan sysfs
|
|
||||||
if self.gpu_vendor.as_deref() == Some("AMD") || self.gpu_vendor.as_deref() == Some("Intel") || self.gpu_vendor.is_none() {
|
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);
|
||||||
|
|||||||
Reference in New Issue
Block a user