use std::path::{Path, PathBuf};
use std::time::Duration;
use console::style;
use super::delta::WatchDelta;
use crate::engine::AnalysisResult;
use crate::models::Severity;
pub fn display_initial(result: &AnalysisResult, elapsed: Duration, _no_emoji: bool, quiet: bool) {
if quiet { return; }
println!(" {} Initial analysis: {} findings, score {:.1} ({:.2}s)",
style("✓").green(), result.findings.len(), result.score.overall, elapsed.as_secs_f64());
println!();
}
pub fn display_error(message: &str, changed_files: &[PathBuf], repo_path: &Path, no_emoji: bool) {
let time = chrono::Local::now().format("%H:%M:%S");
let file_list: String = changed_files.iter()
.map(|p| p.strip_prefix(repo_path).unwrap_or(p).display().to_string())
.collect::<Vec<_>>().join(", ");
eprintln!("{} {} {} {}", style(format!("[{}]", time)).dim(),
if no_emoji { "ERR" } else { "❌" }, style(&file_list).dim(),
style(format!("Analysis error: {}", message)).red());
eprintln!(" {}", style("Watching for next change...").dim());
}
pub fn display_unchanged(changed_files: &[PathBuf], repo_path: &Path, total_findings: usize, score: Option<f64>, no_emoji: bool) {
let time = chrono::Local::now().format("%H:%M:%S");
let file_list: String = changed_files.iter()
.map(|p| p.strip_prefix(repo_path).unwrap_or(p).display().to_string())
.collect::<Vec<_>>().join(", ");
let score_str = score.map(|s| format!(", score {:.1}", s)).unwrap_or_default();
println!("{} {} {} ({} total findings{})", style(format!("[{}]", time)).dim(),
if no_emoji { "→" } else { "📝" }, style(&file_list).dim(), total_findings, score_str);
}
pub fn display_delta(
delta: &WatchDelta,
repo_path: &Path,
no_emoji: bool,
quiet: bool,
) {
let time = chrono::Local::now().format("%H:%M:%S");
let file_list: String = delta
.changed_files
.iter()
.map(|p| {
p.strip_prefix(repo_path)
.unwrap_or(p)
.display()
.to_string()
})
.collect::<Vec<_>>()
.join(", ");
if delta.new_findings.is_empty() && delta.fixed_findings.is_empty() {
if !quiet {
println!(
"{} {} {} ({:.0}ms, {} total findings{})",
style(format!("[{}]", time)).dim(),
if no_emoji { "→" } else { "📝" },
style(&file_list).dim(),
delta.elapsed.as_millis(),
delta.total_findings,
score_suffix(delta),
);
}
return;
}
println!(
"{} {} {} ({:.0}ms)",
style(format!("[{}]", time)).dim(),
if no_emoji { "→" } else { "📝" },
style(&file_list).cyan().bold(),
delta.elapsed.as_millis(),
);
for f in &delta.new_findings {
let sev_icon = severity_icon(f.severity, no_emoji);
let file_line = f
.affected_files
.first()
.map(|af| {
let rel = af.strip_prefix(repo_path).unwrap_or(af);
match f.line_start {
Some(line) => format!("{}:{}", rel.display(), line),
None => rel.display().to_string(),
}
})
.unwrap_or_default();
println!(
" {} {} {} {}",
sev_icon,
style(f.detector.replace("Detector", "")).yellow(),
style(&file_line).dim(),
f.title
);
if is_ai_detector(&f.detector) {
println!(
" {} {}",
style("⚡").dim(),
style("Possible AI-generated code issue").dim().italic()
);
}
}
for f in &delta.fixed_findings {
println!(
" {} {} {}",
if no_emoji { "FIX " } else { "✅" },
style(f.detector.replace("Detector", "")).green(),
style(&f.title).strikethrough()
);
}
{
let score = delta.score;
let delta_str = match delta.score_delta {
Some(d) if d > 0.5 => format!(" {}", style(format!("+{:.1}", d)).green()),
Some(d) if d < -0.5 => format!(" {}", style(format!("{:.1}", d)).red()),
_ => String::new(),
};
println!(" Score: {:.1}{}", score, delta_str);
}
println!();
}
pub fn score_suffix(delta: &WatchDelta) -> String {
let score = delta.score;
match delta.score_delta {
Some(d) if d.abs() > 0.05 => format!(", score {:.1} ({:+.1})", score, d),
_ => format!(", score {:.1}", score),
}
}
pub fn is_ai_detector(name: &str) -> bool {
matches!(
name,
"AIBoilerplate"
| "AIChurn"
| "AIComplexitySpike"
| "AIDuplicateBlock"
| "AIMissingTests"
| "AINamingPattern"
)
}
pub fn severity_icon(severity: Severity, no_emoji: bool) -> &'static str {
match (severity, no_emoji) {
(Severity::Critical, true) => "CRIT",
(Severity::Critical, false) => "🔴",
(Severity::High, true) => "HIGH",
(Severity::High, false) => "🟠",
(Severity::Medium, true) => "MED ",
(Severity::Medium, false) => "🟡",
(Severity::Low, true) => "LOW ",
(Severity::Low, false) => "🔵",
(Severity::Info, true) => "INFO",
(Severity::Info, false) => "⚪",
}
}
pub fn filter_delta_by_severity(delta: WatchDelta, min_severity: Severity) -> WatchDelta {
WatchDelta {
new_findings: delta.new_findings.into_iter()
.filter(|f| f.severity >= min_severity).collect(),
fixed_findings: delta.fixed_findings.into_iter()
.filter(|f| f.severity >= min_severity).collect(),
..delta
}
}