1use std::path::PathBuf;
35
36use anyhow::Result;
37use clap::Args;
38
39use tldr_core::metrics::loc::{analyze_loc, LocOptions, LocReport};
40use tldr_core::Language;
41
42use crate::output::{OutputFormat, OutputWriter};
43
44#[derive(Debug, Args)]
46pub struct LocArgs {
47 #[arg(default_value = ".")]
49 pub path: PathBuf,
50
51 #[arg(long, short = 'l')]
53 pub lang: Option<Language>,
54
55 #[arg(long)]
57 pub by_file: bool,
58
59 #[arg(long)]
61 pub by_dir: bool,
62
63 #[arg(long, short = 'e')]
65 pub exclude: Vec<String>,
66
67 #[arg(long)]
69 pub include_hidden: bool,
70
71 #[arg(long)]
73 pub no_gitignore: bool,
74
75 #[arg(long, default_value = "0")]
77 pub max_files: usize,
78}
79
80impl LocArgs {
81 pub fn run(&self, format: OutputFormat, quiet: bool) -> Result<()> {
83 let writer = OutputWriter::new(format, quiet);
84
85 writer.progress(&format!("Counting lines in {}...", self.path.display()));
86
87 let options = LocOptions {
89 lang: self.lang,
90 by_file: self.by_file,
91 by_dir: self.by_dir,
92 exclude: self.exclude.clone(),
93 include_hidden: self.include_hidden,
94 gitignore: !self.no_gitignore,
95 max_files: self.max_files,
96 max_file_size_mb: 10, };
98
99 let report = analyze_loc(&self.path, &options)?;
101
102 if writer.is_text() {
104 let text = format_loc_text(&report);
105 writer.write_text(&text)?;
106 } else {
107 writer.write(&report)?;
108 }
109
110 Ok(())
111 }
112}
113
114fn format_loc_text(report: &LocReport) -> String {
117 use crate::output::{common_path_prefix, strip_prefix_display};
118 use colored::Colorize;
119 use std::path::Path;
120
121 let mut output = String::new();
122
123 let summary = &report.summary;
125 output.push_str(&format!(
126 "Lines of Code ({} files, {} total)\n\n",
127 summary.total_files, summary.total_lines,
128 ));
129 output.push_str(&format!(
130 " Code: {:>6} ({:.1}%)\n",
131 summary.code_lines, summary.code_percent
132 ));
133 output.push_str(&format!(
134 " Comments: {:>6} ({:.1}%)\n",
135 summary.comment_lines, summary.comment_percent
136 ));
137 output.push_str(&format!(
138 " Blank: {:>6} ({:.1}%)\n",
139 summary.blank_lines, summary.blank_percent
140 ));
141
142 if !report.by_language.is_empty() {
146 output.push_str("\nBy Language:\n");
147
148 let mut entries: Vec<&tldr_core::metrics::loc::LanguageLocEntry> =
149 report.by_language.values().collect();
150 entries.sort_by(|a, b| b.total_lines.cmp(&a.total_lines));
151
152 let max_lang = entries
153 .iter()
154 .map(|e| e.language.len())
155 .max()
156 .unwrap_or(8)
157 .max(8);
158 output.push_str(&format!(
159 " {:<width$} {:>5} {:>6} {:>6} {:>5} {:>6}\n",
160 "Language",
161 "Files",
162 "Code",
163 "Comment",
164 "Blank",
165 "Total",
166 width = max_lang,
167 ));
168
169 for entry in &entries {
170 output.push_str(&format!(
171 " {:<width$} {:>5} {:>6} {:>6} {:>5} {:>6}\n",
172 entry.language,
173 entry.files,
174 entry.code_lines,
175 entry.comment_lines,
176 entry.blank_lines,
177 entry.total_lines,
178 width = max_lang,
179 ));
180 }
181 }
182
183 if let Some(by_file) = &report.by_file {
185 if !by_file.is_empty() {
186 output.push_str("\nBy File:\n");
187
188 let paths: Vec<&Path> = by_file.iter().map(|e| e.path.as_path()).collect();
190 let prefix = common_path_prefix(&paths);
191
192 let display_count = by_file.len().min(50);
193 let max_path = by_file
194 .iter()
195 .take(display_count)
196 .map(|e| strip_prefix_display(&e.path, &prefix).len())
197 .max()
198 .unwrap_or(4)
199 .clamp(4, 50);
200
201 output.push_str(&format!(
202 " {:<width$} {:>4} {:>6} {:>6} {:>5} {:>6}\n",
203 "File",
204 "Lang",
205 "Code",
206 "Comment",
207 "Blank",
208 "Total",
209 width = max_path,
210 ));
211
212 for entry in by_file.iter().take(display_count) {
213 let rel = strip_prefix_display(&entry.path, &prefix);
214 let display_path = if rel.len() > 50 {
215 format!("...{}", &rel[rel.len() - 47..])
216 } else {
217 rel
218 };
219 output.push_str(&format!(
220 " {:<width$} {:>4} {:>6} {:>6} {:>5} {:>6}\n",
221 display_path,
222 entry.language,
223 entry.code_lines,
224 entry.comment_lines,
225 entry.blank_lines,
226 entry.total_lines,
227 width = max_path,
228 ));
229 }
230
231 if by_file.len() > display_count {
232 output.push_str(&format!(
233 " ... and {} more files\n",
234 by_file.len() - display_count
235 ));
236 }
237 }
238 }
239
240 if let Some(by_dir) = &report.by_directory {
242 if !by_dir.is_empty() {
243 output.push_str("\nBy Directory:\n");
244
245 let paths: Vec<&Path> = by_dir.iter().map(|e| e.path.as_path()).collect();
246 let prefix = common_path_prefix(&paths);
247
248 let max_dir = by_dir
249 .iter()
250 .take(30)
251 .map(|e| strip_prefix_display(&e.path, &prefix).len())
252 .max()
253 .unwrap_or(4)
254 .max(4);
255
256 output.push_str(&format!(
257 " {:<width$} {:>6} {:>6} {:>5} {:>6}\n",
258 "Directory",
259 "Code",
260 "Comment",
261 "Blank",
262 "Total",
263 width = max_dir,
264 ));
265
266 for entry in by_dir.iter().take(30) {
267 let rel = strip_prefix_display(&entry.path, &prefix);
268 output.push_str(&format!(
269 " {:<width$} {:>6} {:>6} {:>5} {:>6}\n",
270 rel,
271 entry.code_lines,
272 entry.comment_lines,
273 entry.blank_lines,
274 entry.total_lines,
275 width = max_dir,
276 ));
277 }
278 }
279 }
280
281 if !report.warnings.is_empty() {
283 output.push_str(&"\nWarnings:\n".yellow().to_string());
284 for warning in &report.warnings {
285 output.push_str(&format!(" - {}\n", warning));
286 }
287 }
288
289 output
290}