Skip to main content

codelens_core/output/
console.rs

1//! Console output with colors and formatting.
2
3use std::io::Write;
4
5use colored::Colorize;
6use comfy_table::{presets::UTF8_FULL, Attribute, Cell, Color, ContentArrangement, Table};
7
8use crate::analyzer::stats::AnalysisResult;
9use crate::error::Result;
10
11use super::format::{OutputFormat, OutputOptions};
12
13/// Console output formatter.
14pub struct ConsoleOutput;
15
16impl ConsoleOutput {
17    /// Create a new console output formatter.
18    pub fn new() -> Self {
19        Self
20    }
21
22    fn format_size(bytes: u64) -> String {
23        const KB: u64 = 1024;
24        const MB: u64 = KB * 1024;
25        const GB: u64 = MB * 1024;
26
27        if bytes >= GB {
28            format!("{:.2} GB", bytes as f64 / GB as f64)
29        } else if bytes >= MB {
30            format!("{:.2} MB", bytes as f64 / MB as f64)
31        } else if bytes >= KB {
32            format!("{:.2} KB", bytes as f64 / KB as f64)
33        } else {
34            format!("{} B", bytes)
35        }
36    }
37
38    fn format_number(n: usize) -> String {
39        let s = n.to_string();
40        let mut result = String::new();
41        for (i, c) in s.chars().rev().enumerate() {
42            if i > 0 && i % 3 == 0 {
43                result.push(',');
44            }
45            result.push(c);
46        }
47        result.chars().rev().collect()
48    }
49}
50
51impl Default for ConsoleOutput {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl OutputFormat for ConsoleOutput {
58    fn name(&self) -> &'static str {
59        "console"
60    }
61
62    fn extension(&self) -> &'static str {
63        "txt"
64    }
65
66    fn write(
67        &self,
68        result: &AnalysisResult,
69        options: &OutputOptions,
70        writer: &mut dyn Write,
71    ) -> Result<()> {
72        let summary = &result.summary;
73
74        // Header
75        writeln!(writer)?;
76        writeln!(writer, "{}", "═".repeat(60).dimmed())?;
77        writeln!(
78            writer,
79            "{}",
80            " CODELENS - Code Statistics Report ".bold().cyan()
81        )?;
82        writeln!(writer, "{}", "═".repeat(60).dimmed())?;
83        writeln!(writer)?;
84
85        // Summary table
86        let mut table = Table::new();
87        table
88            .load_preset(UTF8_FULL)
89            .set_content_arrangement(ContentArrangement::Dynamic);
90
91        table.set_header(vec![
92            Cell::new("Metric").add_attribute(Attribute::Bold),
93            Cell::new("Value").add_attribute(Attribute::Bold),
94        ]);
95
96        table.add_row(vec![
97            Cell::new("Total Files"),
98            Cell::new(Self::format_number(summary.total_files)).fg(Color::Green),
99        ]);
100        table.add_row(vec![
101            Cell::new("Code Lines"),
102            Cell::new(Self::format_number(summary.lines.code)).fg(Color::Cyan),
103        ]);
104        table.add_row(vec![
105            Cell::new("Comment Lines"),
106            Cell::new(Self::format_number(summary.lines.comment)).fg(Color::Yellow),
107        ]);
108        table.add_row(vec![
109            Cell::new("Blank Lines"),
110            Cell::new(Self::format_number(summary.lines.blank)).fg(Color::DarkGrey),
111        ]);
112        table.add_row(vec![
113            Cell::new("Total Lines"),
114            Cell::new(Self::format_number(summary.lines.total)).add_attribute(Attribute::Bold),
115        ]);
116        table.add_row(vec![
117            Cell::new("Total Size"),
118            Cell::new(Self::format_size(summary.total_size)),
119        ]);
120        table.add_row(vec![
121            Cell::new("Languages"),
122            Cell::new(summary.by_language.len().to_string()),
123        ]);
124        table.add_row(vec![
125            Cell::new("Functions"),
126            Cell::new(Self::format_number(summary.complexity.functions)),
127        ]);
128
129        writeln!(writer, "{table}")?;
130        writeln!(writer)?;
131
132        // Language breakdown
133        if !options.summary_only && !summary.by_language.is_empty() {
134            writeln!(writer, "{}", "By Language".bold())?;
135            writeln!(writer)?;
136
137            let mut lang_table = Table::new();
138            lang_table
139                .load_preset(UTF8_FULL)
140                .set_content_arrangement(ContentArrangement::Dynamic);
141
142            lang_table.set_header(vec![
143                Cell::new("Language").add_attribute(Attribute::Bold),
144                Cell::new("Files").add_attribute(Attribute::Bold),
145                Cell::new("Code").add_attribute(Attribute::Bold),
146                Cell::new("Comment").add_attribute(Attribute::Bold),
147                Cell::new("Blank").add_attribute(Attribute::Bold),
148                Cell::new("Total").add_attribute(Attribute::Bold),
149            ]);
150
151            let mut langs: Vec<_> = summary.by_language.iter().collect();
152
153            // Apply top_n limit
154            if let Some(n) = options.top_n {
155                langs.truncate(n);
156            }
157
158            for (name, stats) in langs {
159                lang_table.add_row(vec![
160                    Cell::new(name).fg(Color::Cyan),
161                    Cell::new(Self::format_number(stats.files)),
162                    Cell::new(Self::format_number(stats.lines.code)).fg(Color::Green),
163                    Cell::new(Self::format_number(stats.lines.comment)).fg(Color::Yellow),
164                    Cell::new(Self::format_number(stats.lines.blank)).fg(Color::DarkGrey),
165                    Cell::new(Self::format_number(stats.lines.total)),
166                ]);
167            }
168
169            writeln!(writer, "{lang_table}")?;
170            writeln!(writer)?;
171        }
172
173        // Footer
174        writeln!(writer, "{}", "─".repeat(60).dimmed())?;
175        writeln!(
176            writer,
177            "Scanned {} files in {:.2}s",
178            result.scanned_files.to_string().green(),
179            result.elapsed.as_secs_f64()
180        )?;
181
182        Ok(())
183    }
184}