rsftch 0.4.1

Lightning fast hardware fetch tool written in rust.
use libmacchina::{traits::MemoryReadout as _, MemoryReadout};
use std::{
    env,
    fs::{read_to_string, File},
    io::{BufRead, BufReader, Error, Read},
    process::{Command, Output}, 
    time::Duration,
};

pub fn help() {
    println!("Usage: rsftch [OPTION...] [OVERRIDE] [MARGIN]\n");
    println!("  -h, --help, --usage   Bring up this menu");
    println!("  --no-color, --no-formatting");
    println!("  -nc, -nf              Remove icons, colors and such.");
    println!("  -o, --override        Override distribution, changes ASCII.");
    println!("  -m, --margin          Add margin to the info sections, default 1.")
}

pub fn whoami() -> String {
    let output = Command::new("whoami").output().expect("whoami failed");
    String::from_utf8_lossy(&output.stdout).trim().to_string()
}

pub fn get_cpu_info() -> String {
    let cpuinfo = read_to_string("/proc/cpuinfo").expect("Failed to read /proc/cpuinfo");
    let mut cpu_info = String::new();
    let mut cpu = String::new();

    for line in cpuinfo.lines() {
        let parts: Vec<&str> = line.split(": ").map(|s| s.trim()).collect();
        if parts.len() == 2 {
            match parts[0] {
                "model name" | "Hardware" | "Processor" | "^cpu model" | "chip type"
                | "^cpu type" => {
                    cpu = parts[1].to_string();
                    break;
                }
                _ => {}
            }
        }
    }
    cpu_info.push_str(&cpu);
    cpu_info
}

pub fn get_gpu_info() -> Result<String, Error> {
    let output = Command::new("lspci").arg("-nnk").output()?;

    let stdout = String::from_utf8_lossy(&output.stdout);
    let reader = BufReader::new(stdout.as_bytes());

    for line in reader.lines() {
        let line = line?;
        if let Some(start_index) = line.find("NVIDIA").or_else(|| line.find("AMD")) {
            let (prefix, prefix_len) = if line.contains("NVIDIA") {
                ("NVIDIA", "NVIDIA".len())
            } else if line.contains("AMD") {
                ("AMD Radeon", "AMD Radeon".len())
            } else {
                let vendor_index = line.find("controller").unwrap_or(0);
                let vendor_str = &line[..vendor_index];
                (vendor_str, 0)
            };
            let start_index = start_index + prefix_len;
            let start_index = line[start_index..].find("[").ok_or(Error::new(
                std::io::ErrorKind::NotFound,
                "GPU name not found",
            ))? + start_index
                + 1;
            let end_index = line[start_index..].find("]").ok_or(Error::new(
                std::io::ErrorKind::NotFound,
                "GPU name not found",
            ))? + start_index;
            let gpu_name = &line[start_index..end_index];
            return Ok(format!("{} {}", prefix, gpu_name.trim()));
        }
    }

    Err(Error::new(std::io::ErrorKind::NotFound, "GPU not found"))
}

pub fn get_uptime() -> Result<String, Error> {
    let file = File::open("/proc/uptime").expect("Failed to open /proc/uptime");
    let mut reader = BufReader::new(file);
    let mut line = String::new();

    reader
        .read_line(&mut line)
        .expect("Failed to read from /proc/uptime");

    let uptime_secs: f64 = line
        .split_whitespace()
        .next()
        .expect("Failed to parse uptime from /proc/uptime")
        .parse()
        .expect("Failed to parse uptime as f64");

    Ok(format_duration(Duration::from_secs_f64(uptime_secs)))
}

fn format_duration(duration: Duration) -> String {
    let seconds = duration.as_secs();
    let days = seconds / (24 * 3600);
    let hours = (seconds / 3600) % 24;
    let minutes = (seconds / 60) % 60;
    let seconds = seconds % 60;

    let mut uptime_string = String::new();
    if days > 0 {
        uptime_string.push_str(&format!("{} days, ", days));
    }
    if hours > 0 {
        uptime_string.push_str(&format!("{} hours, ", hours));
    }
    if minutes > 0 {
        uptime_string.push_str(&format!("{} minutes, ", minutes));
    }
    uptime_string.push_str(&format!("{} seconds", seconds));

    uptime_string.trim_end_matches(", ").to_string()
}

pub fn get_os_release_pretty_name(overriden_ascii: Option<String>) -> Option<String> {
    if overriden_ascii != None {
        return overriden_ascii;
    }

    let output: Output = match Command::new("bash")
        .arg("-c")
        .arg("awk -F'=' '/^ID=/ {print tolower($2)}' /etc/*-release 2> /dev/null")
        .output()
    {
        Ok(output) => output,
        Err(_) => return None,
    };

    if output.status.success() {
        let stdout = String::from_utf8(output.stdout).ok()?;
        Some(stdout.trim().to_string())
    } else {
        None
    }
}

pub fn format_bytes(kbytes: u64) -> String {
    const MIB: u64 = 1048576;
    format!("{:.2} GiB", kbytes as f64 / MIB as f64)
}

pub fn get_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::new()
}

pub fn get_mem() -> String {
    let mem_readout = MemoryReadout::new();
    let total_mem = mem_readout.total().unwrap_or(0);
    let used_mem = mem_readout.used().unwrap_or(0);

    let total_mem_str = format_bytes(total_mem);
    let used_mem_str = format_bytes(used_mem);

    format!("{} / {}", used_mem_str, total_mem_str)
}

pub fn uname_r() -> String {
    let output = Command::new("uname")
        .arg("-r")
        .output()
        .expect("uname failed -r");
    String::from_utf8_lossy(&output.stdout).trim().to_string()
}

pub fn uname_s(overriden_ascii: Option<String>) -> String {
    if overriden_ascii != None {
        return match overriden_ascii {
            Some(str) => str,
            None => String::new(),
        };
    }
    let output = Command::new("uname")
        .arg("-s")
        .output()
        .expect("uname failed -s");
    String::from_utf8_lossy(&output.stdout).trim().to_string()
}

pub fn uname_n() -> String {
    let output = Command::new("uname")
        .arg("-n")
        .output()
        .expect("uname failed -n");
    String::from_utf8_lossy(&output.stdout).trim().to_string()
}

pub 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 fn get_terminal() -> String {
    let term = env::var("TERM").unwrap_or("".to_string());
    return term;
}