230 lines
8.0 KiB
Rust
230 lines
8.0 KiB
Rust
mod config;
|
|
mod daemon;
|
|
mod error;
|
|
mod ipc;
|
|
mod modules;
|
|
mod output;
|
|
mod state;
|
|
mod utils;
|
|
|
|
use clap::{Parser, Subcommand};
|
|
use std::path::PathBuf;
|
|
use std::process;
|
|
use tracing::{error, info};
|
|
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
|
|
|
|
#[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 {
|
|
/// Optional custom path to config.toml
|
|
#[arg(short, long)]
|
|
config: Option<PathBuf>,
|
|
},
|
|
/// Reload the daemon configuration
|
|
Reload,
|
|
/// 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,
|
|
},
|
|
/// GPU usage, VRAM, and temp module
|
|
Gpu,
|
|
/// System load average and uptime
|
|
Sys,
|
|
/// Bluetooth audio device status
|
|
#[command(alias = "bluetooth")]
|
|
Bt {
|
|
#[arg(default_value = "show")]
|
|
action: String,
|
|
},
|
|
/// System power and battery status
|
|
Power,
|
|
/// Hyprland gamemode status
|
|
Game,
|
|
}
|
|
|
|
fn main() {
|
|
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 { config } => {
|
|
info!("Starting Fluxo daemon...");
|
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap();
|
|
|
|
if let Err(e) = rt.block_on(daemon::run_daemon(config.clone())) {
|
|
error!("Daemon failed: {}", e);
|
|
process::exit(1);
|
|
}
|
|
}
|
|
Commands::Reload => match ipc::request_data("reload", &[]) {
|
|
Ok(_) => info!("Reload signal sent to daemon"),
|
|
Err(e) => {
|
|
error!("Failed to send reload signal: {}", e);
|
|
process::exit(1);
|
|
}
|
|
},
|
|
Commands::Net => handle_ipc_response(ipc::request_data("net", &[])),
|
|
Commands::Cpu => handle_ipc_response(ipc::request_data("cpu", &[])),
|
|
Commands::Mem => handle_ipc_response(ipc::request_data("mem", &[])),
|
|
Commands::Disk { path } => handle_ipc_response(ipc::request_data("disk", &[path])),
|
|
Commands::Pool { kind } => handle_ipc_response(ipc::request_data("pool", &[kind])),
|
|
Commands::Vol { cycle } => {
|
|
let action = if *cycle { "cycle" } else { "show" };
|
|
handle_ipc_response(ipc::request_data("vol", &[action]));
|
|
}
|
|
Commands::Mic { cycle } => {
|
|
let action = if *cycle { "cycle" } else { "show" };
|
|
handle_ipc_response(ipc::request_data("mic", &[action]));
|
|
}
|
|
Commands::Gpu => handle_ipc_response(ipc::request_data("gpu", &[])),
|
|
Commands::Sys => handle_ipc_response(ipc::request_data("sys", &[])),
|
|
Commands::Bt { action } => {
|
|
if action == "menu" {
|
|
// Client-side execution of the menu
|
|
let config = config::load_config(None);
|
|
|
|
let mut items = Vec::new();
|
|
|
|
// If connected, show plugin modes and disconnect
|
|
if let Ok(json_str) = ipc::request_data("bt", &["get_modes"])
|
|
&& let Ok(val) = serde_json::from_str::<serde_json::Value>(&json_str)
|
|
&& let Some(modes_str) = val.get("text").and_then(|t| t.as_str())
|
|
&& !modes_str.is_empty()
|
|
{
|
|
for mode in modes_str.lines() {
|
|
items.push(format!("Mode: {}", mode));
|
|
}
|
|
}
|
|
|
|
if !items.is_empty() {
|
|
items.push("Disconnect".to_string());
|
|
}
|
|
|
|
items.push("--- Connect Device ---".to_string());
|
|
|
|
let devices_out = match std::process::Command::new("bluetoothctl")
|
|
.args(["devices"])
|
|
.output()
|
|
{
|
|
Ok(out) => out,
|
|
Err(e) => {
|
|
error!("bluetoothctl not found or failed: {}", e);
|
|
return;
|
|
}
|
|
};
|
|
let stdout = String::from_utf8_lossy(&devices_out.stdout);
|
|
|
|
for line in stdout.lines() {
|
|
if line.starts_with("Device ") {
|
|
let parts: Vec<&str> = line.splitn(3, ' ').collect();
|
|
if parts.len() == 3 {
|
|
items.push(format!("{} ({})", parts[2], parts[1]));
|
|
}
|
|
}
|
|
}
|
|
|
|
if !items.is_empty() {
|
|
if let Ok(selected) =
|
|
utils::show_menu("BT Menu: ", &items, &config.general.menu_command)
|
|
{
|
|
if let Some(mode) = selected.strip_prefix("Mode: ") {
|
|
handle_ipc_response(ipc::request_data("bt", &["set_mode", mode]));
|
|
} else if selected == "Disconnect" {
|
|
handle_ipc_response(ipc::request_data("bt", &["disconnect"]));
|
|
} else if selected == "--- Connect Device ---" {
|
|
// Do nothing
|
|
} else if let Some(mac_start) = selected.rfind('(')
|
|
&& let Some(mac_end) = selected.rfind(')')
|
|
{
|
|
let mac = &selected[mac_start + 1..mac_end];
|
|
let _ = std::process::Command::new("bluetoothctl")
|
|
.args(["connect", mac])
|
|
.status();
|
|
}
|
|
}
|
|
} else {
|
|
info!("No Bluetooth options found.");
|
|
}
|
|
return;
|
|
}
|
|
handle_ipc_response(ipc::request_data("bt", &[action]));
|
|
}
|
|
Commands::Power => handle_ipc_response(ipc::request_data("power", &[])),
|
|
Commands::Game => handle_ipc_response(ipc::request_data("game", &[])),
|
|
}
|
|
}
|
|
|
|
fn handle_ipc_response(response: anyhow::Result<String>) {
|
|
match response {
|
|
Ok(json_str) => match serde_json::from_str::<serde_json::Value>(&json_str) {
|
|
Ok(mut val) => {
|
|
if let Some(text) = val.get_mut("text").and_then(|t| t.as_str()) {
|
|
let processed_text = if text.contains('<') {
|
|
text.to_string()
|
|
} else {
|
|
text.replace(' ', "\u{2007}")
|
|
};
|
|
|
|
let fixed_text = format!("\u{200B}{}\u{200B}", processed_text);
|
|
val["text"] = serde_json::Value::String(fixed_text);
|
|
}
|
|
println!("{}", serde_json::to_string(&val).unwrap());
|
|
}
|
|
Err(_) => println!("{}", json_str),
|
|
},
|
|
Err(e) => {
|
|
let err_out = output::WaybarOutput {
|
|
text: format!("\u{200B}Daemon offline ({})\u{200B}", e),
|
|
tooltip: Some(e.to_string()),
|
|
class: Some("error".to_string()),
|
|
percentage: None,
|
|
};
|
|
println!("{}", serde_json::to_string(&err_out).unwrap());
|
|
process::exit(1);
|
|
}
|
|
}
|
|
}
|