pfetch 2.11.1

A rewrite of the pfetch system information tool
Documentation
use libmacchina::{
    traits::GeneralReadout as _, traits::KernelReadout as _, traits::MemoryReadout as _,
    traits::PackageReadout as _, GeneralReadout, KernelReadout, MemoryReadout, PackageReadout,
};
use pfetch_logo_parser::{Color, Logo, LogoPart};
use std::{env, fmt::Display, str::FromStr};
use unicode_width::UnicodeWidthStr;

#[derive(Debug, PartialEq)]
enum PfetchInfo {
    Ascii,
    Title,
    Os,
    Host,
    Kernel,
    Uptime,
    Pkgs,
    Cpu,
    Memory,
    Shell,
    Editor,
    Wm,
    De,
    Palette,
    BlankLine,
}

impl Display for PfetchInfo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", format!("{self:?}").to_lowercase())
    }
}

impl FromStr for PfetchInfo {
    type Err = String;

    fn from_str(info: &str) -> Result<Self, Self::Err> {
        match info {
            "ascii" => Ok(PfetchInfo::Ascii),
            "title" => Ok(PfetchInfo::Title),
            "os" => Ok(PfetchInfo::Os),
            "host" => Ok(PfetchInfo::Host),
            "kernel" => Ok(PfetchInfo::Kernel),
            "uptime" => Ok(PfetchInfo::Uptime),
            "pkgs" => Ok(PfetchInfo::Pkgs),
            "cpu" => Ok(PfetchInfo::Cpu),
            "memory" => Ok(PfetchInfo::Memory),
            "shell" => Ok(PfetchInfo::Shell),
            "editor" => Ok(PfetchInfo::Editor),
            "wm" => Ok(PfetchInfo::Wm),
            "de" => Ok(PfetchInfo::De),
            "palette" => Ok(PfetchInfo::Palette),
            unknown_info => Err(format!("Unknown pfetch info: {unknown_info}")),
        }
    }
}

fn pfetch(info: Vec<(Color, String, String)>, logo: Logo, logo_enabled: bool) {
    let raw_logo = if logo_enabled {
        logo.logo_parts
            .iter()
            .map(|LogoPart { content, .. }| content.as_ref())
            .collect::<String>()
    } else {
        "".to_string()
    };
    let color_enabled = dotenvy::var("PF_COLOR").unwrap_or_default() != "0";
    let logo = if color_enabled {
        logo.to_string()
    } else {
        format!("{:#}", logo)
    };
    let mut logo_lines = logo.lines();
    let raw_logo_lines: Vec<_> = raw_logo.lines().collect();
    let logo_width = raw_logo_lines
        .iter()
        .map(|line| line.width())
        .max()
        .unwrap_or(0);
    let line_amount = usize::max(raw_logo_lines.len(), info.len());

    let info1_width = info
        .iter()
        .skip(1)
        .map(|(_, line, _)| {
            if line.starts_with("\x1b[4") {
                // exclude palette from info1 width
                0
            } else {
                line.len()
            }
        })
        .max()
        .unwrap_or(0);

    let padding1 = match dotenvy::var("PF_PAD1") {
        Ok(padding0) => padding0.parse::<usize>().unwrap_or(0),
        Err(_) => 0,
    };
    let padding2 = match dotenvy::var("PF_PAD2") {
        Ok(padding1) => padding1.parse::<usize>().unwrap_or(0),
        Err(_) => 3,
    };
    let padding3 = match dotenvy::var("PF_PAD3") {
        Ok(padding2) => padding2.parse::<usize>().unwrap_or(0),
        Err(_) => 1,
    };

    let mut pfetch_str = String::new();

    for l in 0..line_amount {
        pfetch_str += &format!(
            "{padding1}{bold}{logo}{padding2}{color}{info1}{nobold}{separator}{padding3}{color2}{info2}\n",
            padding1 = " ".repeat(padding1),
            bold = if color_enabled {"\x1b[1m"} else {""},
            logo = if logo_enabled {
                logo_lines.next().unwrap_or("")
            } else {
                ""
            },
            padding2 = " ".repeat(
                logo_width - raw_logo_lines.get(l).map_or(0, |line| line.width())
                    + if logo_enabled { padding2 } else { 0 }
            ),
            color = if color_enabled {info.get(l).map_or("".to_owned(), |line| line.0.to_string())} else {"".to_string()},
            info1 = info.get(l).map_or("", |line| &line.1),
            nobold = if color_enabled {"\x1b[0m"} else {""},
            separator = info.get(l).map_or("".to_string(), |line|
                if ! &line.2.is_empty() {
                    dotenvy::var("PF_SEP").unwrap_or_default()
                } else { "".to_string() }
            ),
            padding3 = " ".repeat(
                info1_width.saturating_sub(info.get(l).map_or(0, |(_, line, _)| line.len()))
                    + padding3
            ),
            color2 = if color_enabled {match dotenvy::var("PF_COL2") {
                Ok(newcolor) => {
                    match Color::from_str(&newcolor) {
                        Ok(newcolor) => format!("{newcolor}"),
                        Err(_) => "".to_string(),
                    }
                },
                Err(_) => "".to_string()
            }} else {"".to_string()},
            info2 = info.get(l).map_or("", |line| &line.2)
        )
    }

    // if colors are disabled, remove them from string
    // if dotenvy::var("PF_COLOR").unwrap_or_default() == "0" {
    //     pfetch_str = pfetch_str
    //         .split("\x1b[")
    //         .map(|chunk| chunk.chars().skip(3).collect::<String>())
    //         .collect();
    // }

    // disable line wrap
    crossterm::execute!(std::io::stdout(), crossterm::terminal::DisableLineWrap)
        .unwrap_or_default();

    println!("{pfetch_str}");

    // enable line wrap
    crossterm::execute!(std::io::stdout(), crossterm::terminal::EnableLineWrap).unwrap_or_default();
}

struct Readouts {
    general_readout: GeneralReadout,
    package_readout: PackageReadout,
    memory_readout: MemoryReadout,
    kernel_readout: KernelReadout,
}

fn get_info(
    info: &PfetchInfo,
    readouts: &Readouts,
    skip_slow_package_managers: bool,
) -> Option<String> {
    match info {
        PfetchInfo::Ascii => None,
        PfetchInfo::Title => pfetch::user_at_hostname(
            &readouts.general_readout,
            &dotenvy::var("USER").ok(),
            &dotenvy::var("HOSTNAME").ok(),
        ),
        PfetchInfo::Os => pfetch::os(&readouts.general_readout),
        PfetchInfo::Host => pfetch::host(&readouts.general_readout),
        PfetchInfo::Kernel => pfetch::kernel(&readouts.kernel_readout),
        PfetchInfo::Uptime => pfetch::uptime(&readouts.general_readout),
        PfetchInfo::Pkgs => Some(
            pfetch::total_packages(&readouts.package_readout, skip_slow_package_managers)
                .to_string(),
        ),
        PfetchInfo::Cpu => pfetch::cpu(&readouts.general_readout),
        PfetchInfo::Memory => pfetch::memory(&readouts.memory_readout),
        PfetchInfo::Shell => pfetch::shell(&readouts.general_readout),
        PfetchInfo::Editor => pfetch::editor(),
        PfetchInfo::Wm => pfetch::wm(&readouts.general_readout),
        PfetchInfo::De => pfetch::de(&readouts.general_readout),
        PfetchInfo::Palette => Some(pfetch::palette()),
        PfetchInfo::BlankLine => Some("".to_string()),
    }
}

fn main() {
    // parse arguements
    if std::env::args().any(|arg| arg.starts_with("-v") || arg.starts_with("--v")) {
        println!("pfetch-rs {}", env!("CARGO_PKG_VERSION"));
        std::process::exit(0);
    } else if std::env::args().len() > 1 {
        println!("pfetch     show system information");
        println!("pfetch -v  show version");
        std::process::exit(0);
    }

    // source file specified by env: PF_SOURCE
    if let Ok(filepath) = dotenvy::var("PF_SOURCE") {
        let _ = dotenvy::from_path(filepath);
    }
    // Check if SKIP_SLOW is enabled
    let skip_slow_package_managers = dotenvy::var("PF_FAST_PKG_COUNT").is_ok();

    let enabled_pf_info_base: Vec<PfetchInfo> = match dotenvy::var("PF_INFO") {
        Ok(pfetch_infos) => pfetch_infos
            .trim()
            .split(' ')
            .map(PfetchInfo::from_str)
            .filter_map(|i| i.ok())
            .collect(),
        Err(_) => vec![
            PfetchInfo::Ascii,
            PfetchInfo::Title,
            PfetchInfo::Os,
            PfetchInfo::Host,
            PfetchInfo::Kernel,
            PfetchInfo::Uptime,
            PfetchInfo::Pkgs,
            PfetchInfo::Memory,
        ],
    };

    // insert blank lines before and after palettes
    let mut enabled_pf_info: Vec<PfetchInfo> = vec![];
    let mut ascii_enabled: bool = false;
    for info in enabled_pf_info_base {
        match info {
            PfetchInfo::Palette => {
                enabled_pf_info.push(PfetchInfo::BlankLine);
                enabled_pf_info.push(PfetchInfo::Palette);
                enabled_pf_info.push(PfetchInfo::BlankLine);
            }
            PfetchInfo::Ascii => {
                ascii_enabled = true;
            }
            i => enabled_pf_info.push(i),
        }
    }

    let readouts = Readouts {
        general_readout: GeneralReadout::new(),
        package_readout: PackageReadout::new(),
        memory_readout: MemoryReadout::new(),
        kernel_readout: KernelReadout::new(),
    };

    let os = pfetch::os(&GeneralReadout::new()).unwrap_or_default();

    let logo_override = env::var("PF_ASCII");

    let logo_name = logo_override.unwrap_or(match env::consts::OS {
        "linux" => os.clone(),
        other => other.to_owned(),
    });

    let mut logo = pfetch::logo(&logo_name);

    // color overrides
    if let Ok(newcolor) = dotenvy::var("PF_COL1") {
        if let Ok(newcolor) = Color::from_str(&newcolor) {
            logo.primary_color = newcolor;
        }
    }

    if let Ok(newcolor) = dotenvy::var("PF_COL3") {
        if newcolor == "COL1" {
            logo.secondary_color = logo.primary_color;
        } else if let Ok(newcolor) = Color::from_str(&newcolor) {
            logo.secondary_color = newcolor;
        }
    }

    let gathered_pfetch_info: Vec<(Color, String, String)> = enabled_pf_info
        .iter()
        .filter_map(|info| match info {
            PfetchInfo::Os => Some((logo.primary_color, info.to_string(), os.clone())),
            _ => get_info(info, &readouts, skip_slow_package_managers).map(|info_str| match info {
                PfetchInfo::Title => (logo.secondary_color, info_str, "".to_string()),
                PfetchInfo::BlankLine => (logo.primary_color, "".to_string(), "".to_string()),
                PfetchInfo::Palette => (logo.primary_color, info_str, "".to_string()),
                _ => (logo.primary_color, info.to_string(), info_str),
            }),
        })
        .collect();

    pfetch(gathered_pfetch_info, logo, ascii_enabled);
}