rustpm 0.2.6

A fast, friendly APT frontend with kernel, desktop, and sources management
use anyhow::Result;
use std::process::Command;

#[derive(Debug, Clone, Default)]
pub struct PackageInfo {
    pub name: String,
    pub installed_version: Option<String>,
    pub candidate_version: Option<String>,
    pub installed_size_kb: Option<u64>,
    pub download_size_bytes: Option<u64>,
    pub description: Option<String>,
    pub section: Option<String>,
    pub homepage: Option<String>,
    pub depends: Vec<String>,
}

fn parse_apt_cache_show(output: &str, name: &str) -> PackageInfo {
    let mut info = PackageInfo { name: name.to_string(), ..Default::default() };
    let mut in_desc = false;
    let mut desc_parts: Vec<String> = Vec::new();

    for line in output.lines() {
        if in_desc {
            if line.starts_with(' ') || line.starts_with('\t') {
                let part = line.trim();
                desc_parts.push(if part == "." { String::new() } else { part.to_string() });
                continue;
            }
            in_desc = false;
        }

        if let Some(pos) = line.find(": ") {
            let key = &line[..pos];
            let val = &line[pos + 2..];
            match key {
                "Description" | "Description-en" => {
                    desc_parts.clear();
                    desc_parts.push(val.to_string());
                    in_desc = true;
                }
                "Section"  => { info.section  = Some(val.to_string()); }
                "Homepage" => { info.homepage = Some(val.to_string()); }
                "Depends"  => {
                    info.depends = val.split(',').map(|s| s.trim().to_string()).collect();
                }
                "Installed-Size" => {
                    info.installed_size_kb = val.split_ascii_whitespace().next()
                        .and_then(|n| n.parse().ok());
                }
                "Size" => {
                    info.download_size_bytes = val.split_ascii_whitespace().next()
                        .and_then(|n| n.parse().ok());
                }
                _ => {}
            }
        }
    }

    if !desc_parts.is_empty() {
        info.description = Some(desc_parts.join("\n"));
    }

    info
}

fn parse_apt_cache_policy(output: &str) -> (Option<String>, Option<String>) {
    let mut installed = None;
    let mut candidate = None;
    for line in output.lines() {
        let t = line.trim();
        if let Some(v) = t.strip_prefix("Installed: ") {
            if v != "(none)" { installed = Some(v.to_string()); }
        } else if let Some(v) = t.strip_prefix("Candidate: ") {
            if v != "(none)" { candidate = Some(v.to_string()); }
        }
    }
    (installed, candidate)
}

pub fn show_package(name: &str) -> Result<PackageInfo> {
    let show_out   = Command::new("apt-cache").args(["show", name]).output()?;
    let policy_out = Command::new("apt-cache").args(["policy", name]).output()?;
    let mut info = parse_apt_cache_show(&String::from_utf8_lossy(&show_out.stdout), name);
    let (inst, cand) = parse_apt_cache_policy(&String::from_utf8_lossy(&policy_out.stdout));
    info.installed_version = inst;
    info.candidate_version = cand;
    Ok(info)
}

pub fn search_packages(term: &str, names_only: bool) -> Result<Vec<PackageInfo>> {
    let mut cmd = Command::new("apt-cache");
    cmd.arg("search");
    if names_only { cmd.arg("--names-only"); }
    cmd.arg(term);

    let out = cmd.output()?;
    let stdout = String::from_utf8_lossy(&out.stdout);

    Ok(stdout.lines().filter_map(|line| {
        let mut parts = line.splitn(2, " - ");
        let name = parts.next()?.trim().to_string();
        if name.is_empty() { return None; }
        Some(PackageInfo { name, description: parts.next().map(|s| s.to_string()), ..Default::default() })
    }).collect())
}

pub fn list_packages(installed: bool, upgradable: bool) -> Result<Vec<PackageInfo>> {
    let mut args = vec!["list"];
    if installed  { args.push("--installed"); }
    if upgradable { args.push("--upgradable"); }

    let out = Command::new("apt").args(&args).output()?;
    let stdout = String::from_utf8_lossy(&out.stdout);

    // apt list format: "name/suite[,suite] ver arch [status]"
    let mut packages = Vec::new();
    for line in stdout.lines().skip(1) {
        if line.starts_with("WARNING") || line.is_empty() { continue; }

        let (name, rest) = match line.split_once('/') {
            Some(p) => p,
            None => continue,
        };

        // Extract version (2nd whitespace token after the slash)
        let mut tokens = rest.split_ascii_whitespace();
        tokens.next(); // suite
        let ver = match tokens.next() {
            Some(v) => v.to_string(),
            None => continue,
        };

        // Extract bracket status if present
        let status = if let (Some(s), Some(e)) = (line.find('['), line.rfind(']')) {
            Some(&line[s + 1..e])
        } else {
            None
        };

        let (installed_ver, candidate_ver) = match status {
            Some(s) if s.starts_with("installed") => {
                if let Some(upg) = s.strip_prefix("installed,upgradable to ") {
                    (Some(ver), Some(upg.to_string()))
                } else {
                    (Some(ver), None)
                }
            }
            _ => (None, Some(ver)),
        };

        packages.push(PackageInfo {
            name: name.to_string(),
            installed_version: installed_ver,
            candidate_version: candidate_ver,
            ..Default::default()
        });
    }

    Ok(packages)
}