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);
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,
};
let mut tokens = rest.split_ascii_whitespace();
tokens.next(); let ver = match tokens.next() {
Some(v) => v.to_string(),
None => continue,
};
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)
}