use colored::Colorize;
use std::path::Path;
use super::preview::{
ChangedFile, DiffHunk, DiffLine, FilePreview, LineType, PreviewConfig, TransformationReport,
};
#[allow(dead_code)]
const CONTEXT_LINES: usize = 3;
pub struct DiffRenderer {
config: PreviewConfig,
}
impl DiffRenderer {
pub fn new(config: PreviewConfig) -> Self {
Self { config }
}
pub fn render_file_preview(&self, preview: &FilePreview) {
println!();
self.render_file_header(&preview.path);
println!();
if preview.is_binary {
self.render_binary_notice();
return;
}
if preview.was_truncated {
self.render_truncation_notice(preview.line_count);
}
if self.config.summary_only {
return;
}
self.render_hunks(&preview.hunks);
}
fn render_file_header(&self, path: &Path) {
let display = path.display().to_string();
println!("{} {}", "diff".bold().cyan(), display.bold());
}
fn render_binary_notice(&self) {
println!(" {} binary file - skipped", "skip".bold().dimmed());
}
fn render_truncation_notice(&self, line_count: usize) {
let max_display = self.config.max_lines * 2;
println!(
" {} output truncated from {} lines",
"note".bold().yellow(),
line_count
);
println!(
" {} use --max-preview-lines={} to adjust",
"hint".bold().dimmed(),
max_display.saturating_add(100)
);
}
fn render_hunks(&self, hunks: &[DiffHunk]) {
for hunk in hunks {
self.render_hunk(hunk);
}
}
fn render_hunk(&self, hunk: &DiffHunk) {
println!(
"@@ -{},{} +{},{} @@",
hunk.old_start, hunk.old_count, hunk.new_start, hunk.new_count
);
let visible_lines: Vec<&DiffLine> = hunk.lines.iter().take(self.config.max_lines).collect();
let mut has_more = false;
if self.config.max_lines > 0 && hunk.lines.len() > self.config.max_lines {
has_more = true;
}
for line in visible_lines {
self.render_line(line);
}
if has_more {
println!(
" {} {} more lines",
"note".bold().yellow(),
hunk.lines.len() - self.config.max_lines
);
}
}
fn render_line(&self, line: &DiffLine) {
let content = &line.content;
match line.line_type {
LineType::Addition => {
println!("{}", content.green());
}
LineType::Deletion => {
println!("{}", content.red());
}
LineType::Context => {
println!("{}", content.normal());
}
LineType::Header => {
println!("{}", content.cyan().bold());
}
}
}
pub fn render_changed_file(&self, file: &ChangedFile) {
if let Some(preview) = &file.preview {
self.render_file_preview(preview);
} else {
println!();
println!("{} {}", "changed".bold().green(), file.path.display());
}
}
#[allow(dead_code)]
pub fn render_changed_list(&self, report: &TransformationReport) {
for file in &report.changed_files {
if self.config.verbose || self.config.summary_only {
println!(
"{} {} (+{} -{})",
terminal::success_prefix(),
file.path.display(),
file.lines_added,
file.lines_removed
);
}
}
}
pub fn render_skipped_file(&self, path: &Path, reason: &str) {
println!(
" {} {} ({})",
"skip".bold().dimmed(),
path.display(),
reason
);
}
pub fn render_report(&self, report: &TransformationReport) {
println!();
println!("{}", "=".repeat(60).cyan());
println!("{}", terminal::label("Transformation Report"));
println!("{}", "=".repeat(60).cyan());
if !report.changed_files.is_empty() {
println!();
println!("{}", "Changed Files:".bold().green());
for file in &report.changed_files {
self.render_changed_file(file);
}
}
if !report.skipped_files.is_empty() {
println!();
println!("{}", "⚡ Skipped Files (Grouped by Reason):".bold().yellow());
let mut grouped_skipped: std::collections::HashMap<String, Vec<&super::preview::SkippedFile>> = std::collections::HashMap::new();
for skipped in &report.skipped_files {
grouped_skipped.entry(skipped.reason.to_string()).or_default().push(skipped);
}
for (reason, files) in grouped_skipped {
println!(" └─ {} ({} files):", reason.bold().cyan(), files.len());
for f in files.iter().take(5) {
println!(" • {}", f.path.display());
}
if files.len() > 5 {
println!(" • ... and {} more files", files.len() - 5);
}
}
}
println!();
println!("{}", terminal::label("Statistics"));
println!(" total changed: {}", report.total_changed());
println!(" lines added: {}", report.total_lines_added());
println!(" lines removed: {}", report.total_lines_removed());
println!(" files skipped: {}", report.total_files_skipped());
println!(" execution: {}ms", report.execution_time_ms);
}
}
mod terminal {
use colored::Colorize;
pub fn label(text: &str) -> colored::ColoredString {
text.bold().cyan()
}
#[allow(dead_code)]
pub fn success_prefix() -> colored::ColoredString {
"done".bold().green()
}
}