use std::fmt::Write;
use std::net::IpAddr;
use std::path::Path;
use std::process::Command;
use std::sync::mpsc::channel;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use chrono::{DateTime, Local};
use clap::Parser;
use local_ip_address::list_afinet_netifas;
use nl80211::Socket;
use sysinfo::{ProcessesToUpdate, System, Users, get_current_pid};
const BAT0_PATH: &str = "/sys/class/power_supply/BAT0/capacity";
const BAT1_PATH: &str = "/sys/class/power_supply/BAT1/capacity";
const BAT0_POWER_PATH: &str = "/sys/class/power_supply/BAT0/power_now";
const BAT1_POWER_PATH: &str = "/sys/class/power_supply/BAT1/power_now";
const AC_ONLINE_PATH: &str = "/sys/class/power_supply/AC/online";
fn get_wifi_ssid(socket: &mut Socket, interface: &str) -> Option<String> {
socket
.get_interfaces_info()
.ok()
.and_then(|interfaces| {
interfaces.into_iter().find(|iface| {
let name = iface
.name
.as_ref()
.and_then(|name| String::from_utf8(name.clone()).ok())
.map(|s| s.trim_end_matches('\0').to_string());
name.map(|name| name == interface).unwrap_or(false)
})
})
.and_then(|iface| iface.ssid.and_then(|ssid| String::from_utf8(ssid).ok()))
}
fn read_battery_capacity(path: &str) -> Option<String> {
std::fs::read_to_string(path)
.ok()
.map(|s| s.trim().to_string())
}
fn read_power_uw(path: &str) -> Option<u64> {
std::fs::read_to_string(path)
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
}
fn get_memory_usage_percent() -> Option<u64> {
let meminfo = std::fs::read_to_string("/proc/meminfo").ok()?;
let mut mem_total = None;
let mut mem_available = None;
for line in meminfo.lines() {
let parts: Vec<&str> = line.split_ascii_whitespace().collect();
if parts.len() < 2 {
continue;
}
match parts[0] {
"MemTotal:" => mem_total = parts[1].parse::<u64>().ok(),
"MemAvailable:" => mem_available = parts[1].parse::<u64>().ok(),
_ => {}
}
if mem_total.is_some() && mem_available.is_some() {
break;
}
}
let total = mem_total?;
let available = mem_available?;
Some(((total - available) * 100) / total)
}
fn get_ac_online_status(path: &str) -> bool {
std::fs::read_to_string(path)
.ok()
.and_then(|s| s.trim().parse::<u8>().ok())
.map(|status| status == 1)
.unwrap_or(false)
}
fn format_ip_addresses(interfaces: &[String], socket: &mut Option<Socket>) -> String {
let network_interfaces = list_afinet_netifas().unwrap_or_default();
let mut ip_addresses: Vec<(String, String)> = vec![];
for (name, ip) in network_interfaces
.iter()
.filter(|(name, ip)| interfaces.contains(name) && matches!(ip, IpAddr::V4(_)))
{
let ip_str = ip.to_string();
if !ip_addresses
.iter()
.any(|(_, existing_ip)| *existing_ip == ip_str)
{
ip_addresses.push((name.clone(), ip_str));
}
}
let mut result = String::from("[");
for (i, (interface, address)) in ip_addresses.iter().enumerate() {
result.push_str(address);
if let Some(sock) = socket.as_mut() {
if let Some(ssid) = get_wifi_ssid(sock, interface) {
result.push_str(&format!("[{}]", ssid));
}
}
if i != ip_addresses.len() - 1 {
result.push_str(", ");
}
}
result.push(']');
result
}
fn format_battery_status(bat0: &str, bat1: &str) -> String {
let mut battery_levels = Vec::new();
if !bat0.is_empty() {
battery_levels.push(format!("{}%", bat0));
}
if !bat1.is_empty() {
battery_levels.push(format!("{}%", bat1));
}
if battery_levels.is_empty() {
String::new()
} else {
format!(" bat [{}],", battery_levels.join(", "))
}
}
#[derive(Debug, Parser)]
#[command(version)]
struct Cli {
#[arg(long)]
interface: Vec<String>,
#[arg(long)]
username: Option<String>,
}
fn main() {
let args = Cli::parse();
let battery_00_enable = Path::new(BAT0_PATH).exists();
let battery_01_enable = Path::new(BAT1_PATH).exists();
let bat0_power_enable = Path::new(BAT0_POWER_PATH).exists();
let bat1_power_enable = Path::new(BAT1_POWER_PATH).exists();
let ac_online_enable = Path::new(AC_ONLINE_PATH).exists();
let (ip_addresses_tx, ip_addresses_rx) = channel();
let (bat0_tx, bat0_rx) = channel();
let (bat1_tx, bat1_rx) = channel();
let (wattage_tx, wattage_rx) = channel();
let (ac_online_tx, ac_online_rx) = channel();
let (mem_tx, mem_rx) = channel();
let (cpu_tx, cpu_rx) = channel();
let m_sys = Arc::new(Mutex::new(System::new()));
let (sys_host_name, sys_user_name) = {
let mut sys = m_sys.lock().unwrap_or_else(|e| e.into_inner());
sys.refresh_processes(
ProcessesToUpdate::Some(&[get_current_pid().unwrap()]),
false,
);
let pid = loop {
match get_current_pid() {
Ok(pid) => break pid,
Err(_) => {
std::thread::sleep(Duration::from_millis(100));
continue;
}
}
};
let name = if let Some(username) = &args.username {
username.clone()
} else if let Some(process) = sys.process(pid) {
if let Some(user_id) = process.user_id() {
let users = Users::new_with_refreshed_list();
if let Some(user) = users.iter().find(|u| u.id() == user_id) {
user.name().to_string()
} else {
String::new()
}
} else {
String::new()
}
} else {
String::new()
};
let hostname = loop {
match System::host_name() {
Some(name) => break name,
None => {
std::thread::sleep(Duration::from_millis(100));
continue;
}
}
};
(hostname, name)
};
std::thread::scope(|x| {
let mut socket = Socket::connect().ok();
x.spawn(move || {
let ip_addresses_string = format_ip_addresses(&args.interface, &mut socket);
while ip_addresses_tx.send(ip_addresses_string.clone()).is_err() {
std::thread::sleep(Duration::from_millis(100));
}
let mut ip_fetch_counter = 0;
loop {
if battery_00_enable && let Some(bat0) = read_battery_capacity(BAT0_PATH) {
while bat0_tx.send(bat0.clone()).is_err() {
std::thread::sleep(Duration::from_millis(100));
}
}
if battery_01_enable && let Some(bat1) = read_battery_capacity(BAT1_PATH) {
while bat1_tx.send(bat1.clone()).is_err() {
std::thread::sleep(Duration::from_millis(100));
}
}
let mut total_wattage_uw: u64 = 0;
if bat0_power_enable && let Some(power_uw) = read_power_uw(BAT0_POWER_PATH) {
total_wattage_uw += power_uw;
}
if bat1_power_enable && let Some(power_uw) = read_power_uw(BAT1_POWER_PATH) {
total_wattage_uw += power_uw;
}
let total_wattage = total_wattage_uw as f64 / 1_000_000.0;
while wattage_tx.send(total_wattage).is_err() {
std::thread::sleep(Duration::from_millis(100));
}
let ac_online = if ac_online_enable {
get_ac_online_status(AC_ONLINE_PATH)
} else {
false
};
while ac_online_tx.send(ac_online).is_err() {
std::thread::sleep(Duration::from_millis(100));
}
if let Some(memory_usage_percent) = get_memory_usage_percent() {
while mem_tx.send(memory_usage_percent).is_err() {
std::thread::sleep(Duration::from_millis(100));
}
}
let mut sys = m_sys.lock().unwrap_or_else(|e| e.into_inner());
sys.refresh_cpu_usage();
let new_avg_cpu_usage: f32 =
((sys.cpus().iter().map(|cpu| cpu.cpu_usage()).sum::<f32>())
/ sys.cpus().len() as f32)
.ceil();
while cpu_tx.send(new_avg_cpu_usage).is_err() {
std::thread::sleep(Duration::from_millis(100));
}
drop(sys);
std::thread::sleep(Duration::from_secs(1));
ip_fetch_counter += 1;
if ip_fetch_counter >= 10 {
ip_fetch_counter = 0;
let ip_addresses_string = format_ip_addresses(&args.interface, &mut socket);
while ip_addresses_tx.send(ip_addresses_string.clone()).is_err() {
std::thread::sleep(Duration::from_millis(100));
}
}
}
});
x.spawn(move || {
let mut last_bat0 = String::new();
let mut last_bat1 = String::new();
let mut last_wattage = 0.0;
let mut last_ac_online = false;
let mut last_mem_usage = 0;
let mut last_cpu_usage = 0.0;
let mut last_addrs = String::new();
let mut status = String::new();
loop {
status.clear();
let local: DateTime<Local> = Local::now();
if let Ok(bat0) = bat0_rx.try_recv()
&& battery_00_enable
{
last_bat0 = bat0;
}
if let Ok(bat1) = bat1_rx.try_recv()
&& battery_01_enable
{
last_bat1 = bat1;
}
let battery_s = format_battery_status(&last_bat0, &last_bat1);
if let Ok(wattage) = wattage_rx.try_recv() {
last_wattage = wattage;
}
if let Ok(ac_online) = ac_online_rx.try_recv() {
last_ac_online = ac_online;
}
if let Ok(mem_usage) = mem_rx.try_recv() {
last_mem_usage = mem_usage;
}
if let Ok(cpu_usage) = cpu_rx.try_recv() {
last_cpu_usage = cpu_usage;
}
if let Ok(ip_addrs) = ip_addresses_rx.try_recv() {
last_addrs = ip_addrs;
}
let ac_indicator = if last_ac_online { " [AC]" } else { "" };
if write!(
status,
"[{sys_host_name}][{sys_user_name}] => cpu {last_cpu_usage:02}%, mem {last_mem_usage:02}%, net {last_addrs},{battery_s} pwr {last_wattage:.1}W{ac_indicator}, {}",
local.format("%F %T")
)
.is_err()
{
std::thread::sleep(Duration::from_millis(100));
continue;
}
let _ = Command::new("xsetroot")
.args(["-name", &status])
.spawn()
.and_then(|mut child| child.wait());
std::thread::sleep(Duration::from_secs(1));
}
});
});
}