use rayon::prelude::*; use std::{ collections::HashSet, env, fs::{self, read_to_string, File}, io::{BufRead, BufReader, Read}, path::Path, process::{Command, Stdio}, sync::{Arc, Mutex}, time::Duration, }; #[cfg(target_os = "linux")] use {once_cell::sync::Lazy, regex::Regex}; pub(crate) fn help() { println!( r#"Usage: rsftch [-h / --help / --usage] [-v / --version] [-o / --override ] [-m / --margin ] [--ignore-config] [--config ] -h, --help, --usage Bring up this menu. -v, --version Print version number. -o, --override Overrides distribution, affects ASCII and "distro" info. Running without an argument prints all possible options. -m, --margin Add margin to the info sections, default 1. E.g. `rsftch --info distro` would output: "EndeavourOS". --config Specify another info config file to be used. --ignore-config Ignores configuration and uses the example one. Configuration file is located at: ~/.config/rsftch/info.json"# ); } pub(crate) fn whoami() -> String { let output = Command::new("whoami") .output() .expect("`whoami` failed, are you on a Unix-like operating system?"); String::from_utf8_lossy(&output.stdout).trim().to_string() } pub(crate) fn timezone() -> String { let timezone_path = Path::new("/etc/timezone"); if timezone_path.exists() { if let Ok(timezone) = fs::read_to_string(timezone_path) { return timezone.trim().to_string(); } } let localtime_path = Path::new("/etc/localtime"); if localtime_path.exists() { if let Ok(symlink_target) = fs::read_link(localtime_path) { if let Some(target_str) = symlink_target.to_str() { if target_str.contains("/zoneinfo/") { if let Some(tz) = target_str.split("/zoneinfo/").last() { return tz.to_string(); } } } } } String::new() } pub(crate) fn cpu_temp() -> String { #[cfg(target_os = "linux")] { static REGEX: Lazy = Lazy::new(|| Regex::new(r"Package id 0:\s+\+(\d+\.\d+)°C").unwrap()); let output = Command::new("sensors").output().unwrap(); let output_str = String::from_utf8_lossy(&output.stdout); REGEX .captures(&output_str) .map_or_else(|| "(N/A)".to_string(), |caps| format!("({}°C)", &caps[1])) } #[cfg(target_os = "netbsd")] { Command::new("envstat") .output() .ok() .and_then(|output| String::from_utf8(output.stdout).ok()) .and_then(|output_str| { output_str .lines() .skip_while(|line| *line != "[acpitz0]") .nth(1) .and_then(|line| line.split(':').nth(1)) .map(|s| s.split_whitespace().next().unwrap_or("")) .and_then(|temp_str| temp_str.parse::().ok()) .map(|temp| format!("({:.1}°C)", temp)) }) .unwrap_or(String::from("(N/A)")) } } fn gpu_temp() -> String { #[cfg(target_os = "linux")] { Command::new("nvidia-smi") .arg("--query-gpu=temperature.gpu") .arg("--format=csv,noheader") .output() .ok() .and_then(|output| String::from_utf8(output.stdout).ok()) .and_then(|temp_str| { temp_str .lines() .next() .and_then(|s| s.trim().parse::().ok()) }) .map(|temp| format!("({:.1}°C)", temp)) .unwrap_or_else(|| { Command::new("sensors") .output() .ok() .and_then(|output| String::from_utf8(output.stdout).ok()) .and_then(|output_str| { output_str .lines() .find(|line| { line.contains("temp1:") || line.contains("edge:") || line.contains("junction:") || line.contains("mem:") }) .and_then(|line| line.split_whitespace().nth(1)) .and_then(|temp_str| { temp_str.trim_end_matches("°C").parse::().ok() }) .map(|temp| format!("({:.1}°C)", temp)) }) .unwrap_or(String::from("(N/A)")) }) } #[cfg(target_os = "netbsd")] { Command::new("envstat") .output() .ok() .and_then(|output| String::from_utf8(output.stdout).ok()) .and_then(|output_str| { output_str .lines() .skip_while(|line| *line != "[acpitz2]") .nth(1) .and_then(|line| line.split(':').nth(1)) .map(|s| s.split_whitespace().next().unwrap_or("")) .and_then(|temp_str| temp_str.parse::().ok()) .map(|temp| format!("({:.1}°C)", temp)) }) .unwrap_or(String::from("(N/A)")) } } fn extract_gpu_name(line: &str) -> String { let parts: Vec<&str> = line.split('[').collect(); if parts.len() >= 3 { return parts[2].split(']').next().unwrap_or("Unknown GPU").trim().to_string(); } String::from("Unknown GPU") } pub(crate) fn gpu_info() -> String { #[cfg(target_os = "linux")] { let output = Command::new("lspci") .arg("-nnk") .output() .map(|output| String::from_utf8_lossy(&output.stdout).to_string()) .unwrap_or_else(|_| format!("N/A {}", gpu_temp())); let reader = BufReader::new(output.as_bytes()); for line in reader.lines().flatten() { if line.contains("NVIDIA") { let prefix = "NVIDIA"; let gpu_name = extract_gpu_name(&line); return format!("{} {} {}", prefix, gpu_name, gpu_temp()); } else if line.contains("AMD") { let prefix = if line.contains("Radeon") { "AMD" } else { "AMD Radeon" }; let gpu_name = extract_gpu_name(&line); return format!("{} {} {}", prefix, gpu_name, gpu_temp()); } else if line.contains("Intel") { if line.contains("VGA compatible controller") || line.contains("3D controller") { let prefix = "Intel Integrated"; let gpu_name = extract_gpu_name(&line); return format!("{} {} {}", prefix, gpu_name, gpu_temp()); } } } format!("N/A {}", gpu_temp()) } #[cfg(target_os = "netbsd")] { let formatted_str = Command::new("pcictl") .args(&["pci0", "list"]) .output() .map(|output| String::from_utf8_lossy(&output.stdout).to_string()) .unwrap_or_else(|_| format!("N/A {}", gpu_temp())) .lines() .find(|&l| l.contains("VGA display")) .and_then(|l| l.rsplitn(2, ':').next()) .map(|name| { name.trim() .split_at(name.find('(').unwrap_or(0)) .0 .trim() .to_string() }) .unwrap_or_else(|| format!("N/A {}", gpu_info())); format!("{} {}", formatted_str, gpu_temp()) } } pub(crate) fn disk_usage() -> String { let output_str = match Command::new("df").arg("-h").output() { Ok(output) if output.status.success() => { String::from_utf8(output.stdout).unwrap_or_default() } _ => return String::from("N/A"), }; let line = output_str.lines().find(|line| line.starts_with('/')); if let Some(line) = line { let parts: Vec<_> = line.split_whitespace().collect(); if parts.len() >= 5 { let filesystem = parts[0]; let used = parts[2]; let size = parts[1]; let capacity = parts[4]; return format!("({}) {} / {} ({})", filesystem, used, size, capacity); } } String::new() } pub(crate) fn cpu_info() -> String { let cpuinfo_file = match read_to_string("/proc/cpuinfo") { Ok(content) => content, Err(_) => return format!("N/A {}", cpu_temp()), }; let keys: HashSet<&str> = [ "model name", "Hardware", "Processor", "^cpu model", "chip type", "^cpu type", ] .iter() .cloned() .collect(); for line in cpuinfo_file.lines() { if let Some(pos) = line.find(": ") { let key = &line[..pos]; let value = &line[pos + 2..].trim(); if keys.contains(&key.trim()) { return format!("{}{}", value.split('@').next().unwrap_or(value), cpu_temp()); } } } format!("N/A {}", cpu_temp()) } fn package_managers() -> Vec { let possible_managers = vec![ "xbps-query", "dnf", "rpm", "apt", "pacman", "emerge", "yum", "zypper", "apk", "pkg_info", "pkg", ]; possible_managers .par_iter() .filter_map(|manager| { let version_command = match *manager { "pkg_info" => "-V", "emerge" => "--help", _ => "--version", }; if Command::new(manager) .arg(version_command) .output() .ok() .map_or(false, |result| result.status.success()) { Some(manager.to_string()) } else { None } }) .collect() } fn count_packages(command: &str, args: &[&str]) -> Option { let mut cmd = Command::new(command) .args(args) .stdout(Stdio::piped()) .spawn() .ok()?; let output = cmd.stdout.take()?; let reader = BufReader::new(output); let line_count = reader.lines().count() as i16; let _ = cmd.wait().ok()?; Some(line_count) } pub(crate) fn packages() -> String { let managers = package_managers(); let packs_numbers = Arc::new(Mutex::new(Vec::new())); managers.par_iter().for_each(|manager| { let count = match manager.as_str() { "xbps-query" => count_packages(manager, &["-l"]), "dnf" | "yum" => count_packages(manager, &["list", "installed"]), "rpm" => count_packages(manager, &["-qa", "--last"]), "apt" => count_packages("dpkg", &["--list"]), "pacman" => count_packages(manager, &["-Q"]), "zypper" => count_packages(manager, &["se"]), "apk" => count_packages(manager, &["list", "--installed"]), "pkg_info" => count_packages("ls", &["/usr/pkg/pkgdb/"]).map(|x| x - 1), "pkg" => count_packages(manager, &["info"]), "emerge" => { if os_pretty_name(None, "ID") .unwrap_or_default() .to_ascii_lowercase() .contains("funtoo") { count_packages("find", &["/var/db/pkg/", "-name", "PF"]) } else { count_packages(manager, &["-I"]) } } _ => None, }; if let Some(count) = count { packs_numbers.lock().unwrap().push(count); } }); let summed: i16 = packs_numbers.lock().unwrap().par_iter().sum(); match managers.is_empty() { false => format!("{} ({})", summed, managers.join(", ")), true => String::from("N/A"), } } pub(crate) fn res() -> String { let output = match Command::new("xrandr").arg("--query").output() { Ok(out) => out, Err(_) => return String::from("N/A"), }; String::from_utf8_lossy(&output.stdout) .lines() .filter_map(|line| { if let Some(index) = line.find(" connected") { let line = &line[index + 1..]; if let Some(resolution) = line.split_whitespace().find(|s| s.contains('x')) { Some(resolution.split('+').next().unwrap_or("").to_string()) } else { None } } else { None } }) .collect::>() .join(", ") } pub(crate) fn uptime() -> String { let mut line = String::new(); File::open("/proc/uptime") .map_err(|_| "NA".to_string()) .and_then(|file| { BufReader::new(file) .read_line(&mut line) .map_err(|_| "N/A".to_string()) }) .ok(); let uptime_secs: f64 = line .split_whitespace() .next() .and_then(|val| val.parse().ok()) .unwrap_or_default(); format_duration(Duration::from_secs_f64(uptime_secs)).to_string() } fn format_duration(duration: Duration) -> String { let seconds = duration.as_secs(); let mut values = vec![ (seconds / (24 * 3600), "days"), ((seconds % (24 * 3600)) / 3600, "hours"), ((seconds % 3600) / 60, "minutes"), (seconds % 60, "seconds"), ]; values.retain(|&(value, _)| value > 0); values .iter() .map(|&(value, unit)| format!("{} {}", value, unit)) .collect::>() .join(", ") } fn search_file(custom_paths: Vec<&'static str>, search_variable: &str) -> Option { for path in custom_paths.iter() { if let Ok(content) = fs::read_to_string(path) { for line in content.lines() { if line.starts_with(search_variable) { if let Some(name) = line.split('=').nth(1) { return Some(name.trim_matches('"').to_string()); } } } } } None } pub(crate) fn os_pretty_name(ascii_override: Option, identifier: &str) -> Option { if ascii_override.is_some() { return ascii_override; } search_file(vec!["/etc/os-release", "/etc/lsb-release"], identifier) } pub(crate) fn wm() -> String { if env::var("DISPLAY").is_err() { return String::new(); } for env_var in &[ "XDG_SESSION_DESKTOP", "XDG_CURRENT_DESKTOP", "DESKTOP_SESSION", ] { if let Ok(de) = env::var(env_var) { return de; } } let path = format!("{}/.xinitrc", env::var("HOME").unwrap_or_default()); if let Ok(mut file) = File::open(&path) { let mut buf = String::new(); if file.read_to_string(&mut buf).is_ok() { if let Some(last_line) = buf.lines().last() { let last_word = last_line.split(' ').last().unwrap_or(""); return last_word.to_string(); } } } String::from("N/A") } fn parse_memory_value(line: &str) -> u64 { line.split_whitespace() .nth(1) .unwrap_or("0") .parse::() .unwrap_or(0) } pub(crate) fn mem() -> String { let kb_to_gb = |kilobytes: u64| kilobytes as f64 / (1024.0 * 1024.0); if let Ok(file) = File::open("/proc/meminfo") { let reader = BufReader::new(file); let mut mem_total: u64 = 0; #[cfg(target_os = "linux")] { let mut mem_available: u64 = 0; for line in reader.lines() { if let Ok(line) = line { if line.starts_with("MemTotal:") { mem_total = parse_memory_value(&line); } else if line.starts_with("MemAvailable:") { mem_available = parse_memory_value(&line); } } } let used = mem_total - mem_available; return format!("{:.2} GiB / {:.2} GiB", kb_to_gb(used), kb_to_gb(mem_total)); } #[cfg(target_os = "netbsd")] { let mut mem_free: u64 = 0; for line in reader.lines() { if let Ok(line) = line { if line.starts_with("MemTotal:") { mem_total = parse_memory_value(&line); } else if line.starts_with("MemAvailable:") || line.starts_with("MemFree:") { mem_free = parse_memory_value(&line); } } } let used = mem_total - mem_free; return format!("{:.2} GiB / {:.2} GiB", kb_to_gb(used), kb_to_gb(mem_total)); } } String::from("N/A") } pub(crate) fn uname(arg: &str, ascii_override: Option) -> String { if ascii_override.is_some() { return ascii_override.unwrap(); } let output = Command::new("uname") .arg(arg) .output() .expect(format!("`uname` failed {arg}, this should not happen.").as_str()); String::from_utf8_lossy(&output.stdout).trim().to_string() } pub(crate) fn shell_name() -> String { let shell = env::var("SHELL").expect("SHELL not set"); let parts: Vec<&str> = shell.split('/').collect(); parts.last().unwrap().to_string() } pub(crate) fn terminal() -> String { env::var("TERM").unwrap_or("N/A".to_string()) }