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() {
144 output.push_str("\nBy Language:\n");
145
146 let max_lang = report
147 .by_language
148 .iter()
149 .map(|e| e.language.len())
150 .max()
151 .unwrap_or(8)
152 .max(8);
153 output.push_str(&format!(
154 " {:<width$} {:>5} {:>6} {:>6} {:>5} {:>6}\n",
155 "Language",
156 "Files",
157 "Code",
158 "Comment",
159 "Blank",
160 "Total",
161 width = max_lang,
162 ));
163
164 for entry in &report.by_language {
165 output.push_str(&format!(
166 " {:<width$} {:>5} {:>6} {:>6} {:>5} {:>6}\n",
167 entry.language,
168 entry.files,
169 entry.code_lines,
170 entry.comment_lines,
171 entry.blank_lines,
172 entry.total_lines,
173 width = max_lang,
174 ));
175 }
176 }
177
178 if let Some(by_file) = &report.by_file {
180 if !by_file.is_empty() {
181 output.push_str("\nBy File:\n");
182
183 let paths: Vec<&Path> = by_file.iter().map(|e| e.path.as_path()).collect();
185 let prefix = common_path_prefix(&paths);
186
187 let display_count = by_file.len().min(50);
188 let max_path = by_file
189 .iter()
190 .take(display_count)
191 .map(|e| strip_prefix_display(&e.path, &prefix).len())
192 .max()
193 .unwrap_or(4)
194 .clamp(4, 50);
195
196 output.push_str(&format!(
197 " {:<width$} {:>4} {:>6} {:>6} {:>5} {:>6}\n",
198 "File",
199 "Lang",
200 "Code",
201 "Comment",
202 "Blank",
203 "Total",
204 width = max_path,
205 ));
206
207 for entry in by_file.iter().take(display_count) {
208 let rel = strip_prefix_display(&entry.path, &prefix);
209 let display_path = if rel.len() > 50 {
210 format!("...{}", &rel[rel.len() - 47..])
211 } else {
212 rel
213 };
214 output.push_str(&format!(
215 " {:<width$} {:>4} {:>6} {:>6} {:>5} {:>6}\n",
216 display_path,
217 entry.language,
218 entry.code_lines,
219 entry.comment_lines,
220 entry.blank_lines,
221 entry.total_lines,
222 width = max_path,
223 ));
224 }
225
226 if by_file.len() > display_count {
227 output.push_str(&format!(
228 " ... and {} more files\n",
229 by_file.len() - display_count
230 ));
231 }
232 }
233 }
234
235 if let Some(by_dir) = &report.by_directory {
237 if !by_dir.is_empty() {
238 output.push_str("\nBy Directory:\n");
239
240 let paths: Vec<&Path> = by_dir.iter().map(|e| e.path.as_path()).collect();
241 let prefix = common_path_prefix(&paths);
242
243 let max_dir = by_dir
244 .iter()
245 .take(30)
246 .map(|e| strip_prefix_display(&e.path, &prefix).len())
247 .max()
248 .unwrap_or(4)
249 .max(4);
250
251 output.push_str(&format!(
252 " {:<width$} {:>6} {:>6} {:>5} {:>6}\n",
253 "Directory",
254 "Code",
255 "Comment",
256 "Blank",
257 "Total",
258 width = max_dir,
259 ));
260
261 for entry in by_dir.iter().take(30) {
262 let rel = strip_prefix_display(&entry.path, &prefix);
263 output.push_str(&format!(
264 " {:<width$} {:>6} {:>6} {:>5} {:>6}\n",
265 rel,
266 entry.code_lines,
267 entry.comment_lines,
268 entry.blank_lines,
269 entry.total_lines,
270 width = max_dir,
271 ));
272 }
273 }
274 }
275
276 if !report.warnings.is_empty() {
278 output.push_str(&"\nWarnings:\n".yellow().to_string());
279 for warning in &report.warnings {
280 output.push_str(&format!(" - {}\n", warning));
281 }
282 }
283
284 output
285}