use crate::models::{HealthReport, Severity};
use anyhow::Result;
fn grade_color(grade: &str) -> &'static str {
match grade {
"A" => "\x1b[32m", "B" => "\x1b[92m", "C" => "\x1b[33m", "D" => "\x1b[91m", "F" => "\x1b[31m", _ => "\x1b[0m",
}
}
fn severity_color(severity: &Severity) -> &'static str {
match severity {
Severity::Critical => "\x1b[31m", Severity::High => "\x1b[91m", Severity::Medium => "\x1b[33m", Severity::Low => "\x1b[34m", Severity::Info => "\x1b[90m", }
}
const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
const DIM: &str = "\x1b[2m";
fn severity_tag(severity: &Severity) -> &'static str {
match severity {
Severity::Critical => "[C]",
Severity::High => "[H]",
Severity::Medium => "[M]",
Severity::Low => "[L]",
Severity::Info => "[I]",
}
}
pub fn render(report: &HealthReport) -> Result<String> {
let mut out = String::new();
let grade_c = grade_color(&report.grade);
out.push_str(&format!("\n{BOLD}Repotoire Analysis{RESET}\n"));
out.push_str(&format!("{DIM}──────────────────────────────────────{RESET}\n"));
out.push_str(&format!(
"Score: {BOLD}{:.1}/100{RESET} Grade: {grade_c}{BOLD}{}{RESET} ",
report.overall_score, report.grade
));
out.push_str(&format!(
"Files: {} Functions: {} Classes: {}\n\n",
report.total_files, report.total_functions, report.total_classes
));
out.push_str(&format!("{BOLD}SCORES{RESET}\n"));
out.push_str(&format!(
" Structure: {} Quality: {}",
format_score(report.structure_score),
format_score(report.quality_score)
));
if let Some(arch) = report.architecture_score {
out.push_str(&format!(" Architecture: {}", format_score(arch)));
}
out.push_str("\n\n");
let fs = &report.findings_summary;
out.push_str(&format!("{BOLD}FINDINGS{RESET} ({} total)\n", fs.total));
let mut summary_parts = Vec::new();
if fs.critical > 0 {
summary_parts.push(format!("\x1b[31m{} critical{RESET}", fs.critical));
}
if fs.high > 0 {
summary_parts.push(format!("\x1b[91m{} high{RESET}", fs.high));
}
if fs.medium > 0 {
summary_parts.push(format!("\x1b[33m{} medium{RESET}", fs.medium));
}
if fs.low > 0 {
summary_parts.push(format!("\x1b[34m{} low{RESET}", fs.low));
}
if !summary_parts.is_empty() {
out.push_str(&format!(" {}\n\n", summary_parts.join(" | ")));
}
if !report.findings.is_empty() {
out.push_str(&format!("{DIM} # SEV TITLE FILE{RESET}\n"));
out.push_str(&format!("{DIM} ─────────────────────────────────────────────────────────────────{RESET}\n"));
for (i, finding) in report.findings.iter().take(10).enumerate() {
let sev_c = severity_color(&finding.severity);
let sev_tag = severity_tag(&finding.severity);
let title = if finding.title.len() > 38 {
format!("{}...", &finding.title[..35])
} else {
finding.title.clone()
};
let file_info = if let Some(file) = finding.affected_files.first() {
let file_str = file.display().to_string();
let short_file = if file_str.len() > 25 {
format!("...{}", &file_str[file_str.len()-22..])
} else {
file_str
};
if let Some(line) = finding.line_start {
format!("{}:{}", short_file, line)
} else {
short_file
}
} else {
String::new()
};
out.push_str(&format!(
" {DIM}{:>3}{RESET} {sev_c}{}{RESET} {:<40} {DIM}{}{RESET}\n",
i + 1,
sev_tag,
title,
file_info
));
}
let remaining = report.findings.len().saturating_sub(10);
if remaining > 0 {
out.push_str(&format!(
"\n {DIM}...and {} more (use --page 2 or findings -i){RESET}\n",
remaining
));
}
out.push('\n');
}
match report.grade.as_str() {
"A" => out.push_str(&format!("{DIM}Excellent! Keep up the good work.{RESET}\n")),
"B" => out.push_str(&format!("{DIM}Good shape. Address remaining issues for an A.{RESET}\n")),
"C" | "D" | "F" => {
out.push_str(&format!("{DIM}Run `repotoire findings -i` for interactive review.{RESET}\n"));
}
_ => {}
}
Ok(out)
}
fn format_score(score: f64) -> String {
let color = if score >= 80.0 {
"\x1b[32m"
} else if score >= 60.0 {
"\x1b[33m"
} else {
"\x1b[31m"
};
format!("{color}{:.0}{RESET}", score)
}