Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Commands>,
}
Expand Down Expand Up @@ -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);
Expand Down
8 changes: 7 additions & 1 deletion src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
145 changes: 75 additions & 70 deletions src/telemetry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -319,8 +324,8 @@ pub fn detect_project_language(root: &Path) -> Option<String> {
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 {
Expand Down
2 changes: 1 addition & 1 deletion tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 '{}'",
Expand Down
Loading