diff --git a/src/main.rs b/src/main.rs index cd59777..8074f71 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,10 @@ struct Cli { #[arg(long)] full: bool, + /// counts lines if in non-git directory + #[arg(long, short)] + force_count_lines: bool, + #[command(subcommand)] command: Option, } @@ -45,8 +49,14 @@ fn main() -> Result<()> { if cli.full { let sys_telem = system::collect_system_telemetry(); - let proj_telem = telemetry::collect_project_telemetry(&cwd); - let output = render::render_full(&git_state, &sys_telem, &proj_telem, &palette); + let proj_telem = telemetry::collect_project_telemetry(cli.force_count_lines, &cwd); + let output = render::render_full( + cli.force_count_lines, + &git_state, + &sys_telem, + &proj_telem, + &palette, + ); print!("{}", output); } else { let top_lang = telemetry::detect_project_language(&cwd); diff --git a/src/render.rs b/src/render.rs index f63374c..6cf1638 100644 --- a/src/render.rs +++ b/src/render.rs @@ -567,6 +567,7 @@ fn sys_cell( // Full Mode (--full, 79 total width) pub fn render_full( + force_count_lines: bool, git: &GitState, sys: &SystemTelemetry, telem: &ProjectTelemetry, @@ -826,7 +827,12 @@ pub fn render_full( // grid math: left cell 38 (16 prefix + 22 value) + 2 gutter + // right cell 35 (16 prefix + 19 value) = 75. every variable value is // truncated into its slot, so long counts/names can never overflow. - let loc_val = pad_to_visible(&format!("{} lines", telem.total_lines), 22); + let loc_val = if git.is_repo || force_count_lines { + pad_to_visible(&format!("{} lines", telem.total_lines), 22) + } else { + pad_to_visible("line count unavailable", 22) + }; + let loc_cell = format!( " {}\u{f121}{} {} {}{}{}", p.gauge_fill, diff --git a/src/telemetry.rs b/src/telemetry.rs index 8ff2a3a..e30ce35 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -5,6 +5,8 @@ use std::path::Path; use ignore::WalkBuilder; +use crate::git; + #[derive(Debug, Clone, PartialEq, Default)] pub struct ProjectTelemetry { pub total_lines: usize, @@ -67,89 +69,92 @@ fn extension_to_lang(ext: &str) -> &'static str { const MAX_COUNT_BYTES: u64 = 1024 * 1024; // walk workspace respecting .gitignore using ignore crate, counting lines of code -pub fn count_lines_of_code(root: &Path) -> (usize, Option<(String, f32)>) { - let mut lang_counts: HashMap<&'static str, usize> = HashMap::new(); - let mut total_lines = 0; +pub fn count_lines_of_code(force_count_lines: bool, root: &Path) -> (usize, Option<(String, f32)>) { + let git_state = git::detect_git_state(root).unwrap_or_default(); - let walker = WalkBuilder::new(root) - .standard_filters(true) - .hidden(true) - .build(); + if git_state.is_repo || force_count_lines { + let mut lang_counts: HashMap<&'static str, usize> = HashMap::new(); + let mut total_lines = 0; - for entry in walker.filter_map(Result::ok) { - if !entry.file_type().is_some_and(|ft| ft.is_file()) { - continue; - } + let walker = WalkBuilder::new(root) + .standard_filters(true) + .hidden(true) + .build(); - let path = entry.path(); - let ext = path - .extension() - .and_then(|s| s.to_str()) - .unwrap_or("") - .to_lowercase(); + for entry in walker.filter_map(Result::ok) { + if !entry.file_type().is_some_and(|ft| ft.is_file()) { + continue; + } - // unclassified extensions (binaries, media, archives) are skipped - // without opening: counting their bytes as code would be dishonest, - // and reading them is what made big directories slow. - let lang = extension_to_lang(&ext); - if lang == "other" { - continue; - } + let path = entry.path(); + let ext = path + .extension() + .and_then(|s| s.to_str()) + .unwrap_or("") + .to_lowercase(); - // cheap size gate before paying for a full read - if entry - .metadata() - .map(|m| m.len() > MAX_COUNT_BYTES) - .unwrap_or(true) - { - continue; - } + // unclassified extensions (binaries, media, archives) are skipped + // without opening: counting their bytes as code would be dishonest, + // and reading them is what made big directories slow. + let lang = extension_to_lang(&ext); + if lang == "other" { + continue; + } - let lang = extension_to_lang(&ext); + // cheap size gate before paying for a full read + if entry + .metadata() + .map(|m| m.len() > MAX_COUNT_BYTES) + .unwrap_or(true) + { + continue; + } - if let Ok(file) = fs::File::open(path) { - let reader = BufReader::new(file); - let lines = reader.lines().count(); - if lines > 0 { - total_lines += lines; - *lang_counts.entry(lang).or_insert(0) += lines; + if let Ok(file) = fs::File::open(path) { + let reader = BufReader::new(file); + let lines = reader.lines().count(); + if lines > 0 { + total_lines += lines; + *lang_counts.entry(lang).or_insert(0) += lines; + } } } - } - if total_lines == 0 { - return (0, None); - } + if total_lines == 0 { + return (0, None); + } - // top primary code language (ignoring config and markdown if code exists) - let mut top_code_lang: Option<(&'static str, usize)> = None; + // top primary code language (ignoring config and markdown if code exists) + let mut top_code_lang: Option<(&'static str, usize)> = None; - for (&lang, &count) in &lang_counts { - if lang != "markdown" - && lang != "config" - && lang != "other" - && top_code_lang.is_none_or(|(_, max)| count > max) - { - top_code_lang = Some((lang, count)); + for (&lang, &count) in &lang_counts { + if lang != "markdown" + && lang != "config" + && lang != "other" + && top_code_lang.is_none_or(|(_, max)| count > max) + { + top_code_lang = Some((lang, count)); + } } - } - let top_lang = match top_code_lang { - Some((lang, count)) => { - let pct = (count as f32 / total_lines as f32) * 100.0; - Some((lang.to_string(), pct)) - } - _ => { - // fall back to overall top language - let overall = lang_counts.iter().max_by_key(|&(_, &count)| count); - overall.map(|(&lang, &count)| { + let top_lang = match top_code_lang { + Some((lang, count)) => { let pct = (count as f32 / total_lines as f32) * 100.0; - (lang.to_string(), pct) - }) - } - }; - - (total_lines, top_lang) + Some((lang.to_string(), pct)) + } + _ => { + // fall back to overall top language + let overall = lang_counts.iter().max_by_key(|&(_, &count)| count); + overall.map(|(&lang, &count)| { + let pct = (count as f32 / total_lines as f32) * 100.0; + (lang.to_string(), pct) + }) + } + }; + (total_lines, top_lang) + } else { + (0, None) + } } // detect real coverage report if present in standard locations @@ -319,8 +324,8 @@ pub fn detect_project_language(root: &Path) -> Option { None } -pub fn collect_project_telemetry(root: &Path) -> ProjectTelemetry { - let (total_lines, top_language) = count_lines_of_code(root); +pub fn collect_project_telemetry(force_count_lines: bool, root: &Path) -> ProjectTelemetry { + let (total_lines, top_language) = count_lines_of_code(force_count_lines, root); let coverage_info = detect_coverage(root); ProjectTelemetry { diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 3b3b8d6..f76c756 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -315,7 +315,7 @@ fn test_all_mascot_reactions_render() { } // full mode drives the same face from the same state - let full = render_full(git, &sys, &telem, &palette); + let full = render_full(true, git, &sys, &telem, &palette); assert!( full.contains(word), "full mode missing chip word '{}'",