use std::io::Write;
use anyhow::Result;
use archon_monitor::BuildReport;
use archon_repo::LocalDb;
use archon_resolver::{InstallPlan, PlannedPackage};
use crate::color;
const DETAIL_THRESHOLD: usize = 12;
const SAMPLE_THRESHOLD: usize = 60;
const SAMPLE_SIZE: usize = 16;
pub fn print_plan(plan: &InstallPlan, local: Option<&LocalDb>) {
let total = plan.package_count();
println!(
"{} {} package{} {}\n",
color::bold("▶ Install plan —"),
total,
if total == 1 { "" } else { "s" },
color::dim(&format!("({} official, {} aur)", plan.official.len(), plan.aur.len())),
);
let is_satisfied =
|p: &&PlannedPackage| local.is_some_and(|db| db.satisfies(&p.meta.name, &p.meta.version));
if !plan.aur.is_empty() {
let (already, new): (Vec<&PlannedPackage>, Vec<&PlannedPackage>) =
plan.aur.iter().partition(is_satisfied);
for p in &new {
let mut line = format!(
" {} {} {}",
color::yellow("AUR"),
p.meta.name,
color::dim(&p.meta.version.to_string())
);
if !p.required_by.is_empty() {
line.push_str(&color::dim(&format!(" (needs: {})", p.required_by.join(", "))));
}
println!("{line}");
}
if !already.is_empty() {
let names: Vec<&str> = already.iter().map(|p| p.meta.name.as_str()).collect();
println!(" {}", color::dim(&format!("already installed: {}", names.join(", "))));
}
println!();
}
if !plan.official.is_empty() {
let (already, new): (Vec<&PlannedPackage>, Vec<&PlannedPackage>) =
plan.official.iter().partition(is_satisfied);
if new.is_empty() {
println!(
" {}",
color::dim(&format!("Official: all {} already up to date", plan.official.len()))
);
return;
}
let suffix = if already.is_empty() {
String::new()
} else {
format!(", {} already up to date", already.len())
};
println!(" {}", color::cyan(&format!("Official ({} new/updated{suffix}):", new.len())));
if new.len() <= DETAIL_THRESHOLD {
print_detailed(&new);
} else if new.len() <= SAMPLE_THRESHOLD {
let names: Vec<&str> = new.iter().map(|p| p.meta.name.as_str()).collect();
print_wrapped(&names);
} else {
let names: Vec<&str> = new.iter().take(SAMPLE_SIZE).map(|p| p.meta.name.as_str()).collect();
print_wrapped(&names);
println!(" {}", color::dim(&format!("... and {} more", new.len() - SAMPLE_SIZE)));
}
}
}
fn print_detailed(pkgs: &[&PlannedPackage]) {
let name_width = pkgs.iter().map(|p| p.meta.name.len()).max().unwrap_or(0);
for p in pkgs {
let mut line = format!(
" {:<name_width$} {}",
p.meta.name,
color::dim(&p.meta.version.to_string()),
name_width = name_width
);
if !p.required_by.is_empty() {
line.push_str(&color::dim(&format!(" (required by: {})", p.required_by.join(", "))));
}
println!("{line}");
}
}
fn print_wrapped(names: &[&str]) {
const WIDTH: usize = 92;
const INDENT: &str = " ";
let mut line = String::from(INDENT);
for (i, name) in names.iter().enumerate() {
let piece = if i + 1 == names.len() { name.to_string() } else { format!("{name}, ") };
if line.len() > INDENT.len() && line.len() + piece.len() > WIDTH {
println!("{}", color::dim(&line));
line = String::from(INDENT);
}
line.push_str(&piece);
}
if line.len() > INDENT.len() {
println!("{}", color::dim(&line));
}
}
pub fn print_report(report: &BuildReport) {
println!("\n{}", color::bold(&format!("Build report for {}", report.package)));
println!(" {} syscall event(s) observed", report.total_events);
if report.is_clean() {
println!(" {}", color::green("no policy violations — nothing flagged"));
return;
}
println!(" {}", color::bold_red(&format!("{} event(s) FLAGGED:", report.flagged.len())));
for flagged in &report.flagged {
println!(
" {} pid={} comm={} — {}",
color::red("[!]"),
flagged.event.pid,
flagged.event.comm_str(),
color::red(&flagged.reason.to_string())
);
}
}
pub fn confirm(prompt: &str) -> Result<bool> {
print!("{} ", color::bold(&format!("{prompt} [y/N]")));
std::io::stdout().flush()?;
let mut line = String::new();
std::io::stdin().read_line(&mut line)?;
Ok(matches!(line.trim().to_lowercase().as_str(), "y" | "yes"))
}