mod cli;
use std::fs::File;
use std::io::{self, BufWriter, Write};
use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::{Context, Result};
use clap::Parser;
use colored::Colorize;
use tracing_subscriber::EnvFilter;
use codelens_core::output::{create_output, OutputOptions};
use codelens_core::{analyze, Config, LanguageRegistry};
use crate::cli::{Cli, OutputFormatArg, SortByArg};
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("{}: {e:#}", "error".red().bold());
ExitCode::FAILURE
}
}
}
fn run() -> Result<()> {
let cli = Cli::parse();
init_tracing(cli.output.verbose);
if cli.advanced.list_languages {
return list_languages();
}
let config = build_config(&cli)?;
let paths: Vec<PathBuf> = if cli.paths.is_empty() {
vec![PathBuf::from(".")]
} else {
cli.paths.clone()
};
let result = analyze(&paths, &config).context("Analysis failed")?;
let output_options = OutputOptions {
summary_only: cli.output.summary,
sort_by: cli.output.sort.into(),
top_n: cli.output.top,
colorize: !cli.output.quiet,
show_git_info: cli.advanced.git_info,
};
let formatter = create_output(cli.output.format.into());
if cli.output.quiet {
return Ok(());
}
if let Some(ref path) = cli.output.output_file {
let file = File::create(path).context("Failed to create output file")?;
let mut writer = BufWriter::new(file);
formatter.write(&result, &output_options, &mut writer)?;
writer.flush()?;
println!("Output written to: {}", path.display().to_string().green());
} else {
let stdout = io::stdout();
let mut writer = stdout.lock();
formatter.write(&result, &output_options, &mut writer)?;
}
Ok(())
}
fn init_tracing(verbose: bool) {
let filter = if verbose {
EnvFilter::new("debug")
} else {
EnvFilter::new("warn")
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.without_time()
.init();
}
fn list_languages() -> Result<()> {
let registry = LanguageRegistry::with_builtin()?;
println!("{}", "Supported Languages".bold());
println!("{}", "─".repeat(40));
let mut languages: Vec<_> = registry.all().collect();
languages.sort_by(|a, b| a.name.cmp(&b.name));
let count = languages.len();
for lang in languages {
let exts = lang.extensions.join(", ");
println!(" {} {}", lang.name.cyan(), format!("({})", exts).dimmed());
}
println!();
println!("Total: {} languages", count.to_string().green());
Ok(())
}
fn build_config(cli: &Cli) -> Result<Config> {
use codelens_core::config::{FilterConfig, OutputConfig};
use codelens_core::walker::WalkerConfig;
let base_config = if !cli.advanced.no_config {
if let Some(ref path) = cli.advanced.config {
Some(codelens_core::config::load_config_file(path)?)
} else {
let default_paths = [".codelens.toml", ".code_stats.yaml", ".code_stats.yml"];
default_paths.iter().find_map(|p| {
let path = PathBuf::from(p);
if path.exists() {
codelens_core::config::load_config_file(&path).ok()
} else {
None
}
})
}
} else {
None
};
let mut config = base_config.unwrap_or_default();
config.walker = WalkerConfig {
threads: cli.advanced.threads.unwrap_or_else(num_cpus::get),
use_gitignore: !cli.filter.no_gitignore,
max_depth: cli.filter.depth,
..config.walker
};
config.filter = FilterConfig {
excludes: cli.filter.exclude.clone().unwrap_or_default(),
exclude_files: cli
.filter
.exclude_files
.as_ref()
.map(|s| vec![s.clone()])
.unwrap_or_default(),
include_files: cli
.filter
.include_files
.as_ref()
.map(|s| vec![s.clone()])
.unwrap_or_default(),
languages: cli.filter.lang.clone().unwrap_or_default(),
min_lines: cli.filter.min_lines,
max_lines: cli.filter.max_lines,
smart_exclude: !cli.filter.no_smart_exclude,
include_all: cli.filter.all,
..config.filter
};
config.output = OutputConfig {
format: cli.output.format.into(),
file: cli.output.output_file.clone(),
summary_only: cli.output.summary,
sort_by: cli.output.sort.into(),
top_n: cli.output.top,
verbose: cli.output.verbose,
quiet: cli.output.quiet,
show_git_info: cli.advanced.git_info,
};
Ok(config)
}
impl From<OutputFormatArg> for codelens_core::config::OutputFormatType {
fn from(arg: OutputFormatArg) -> Self {
match arg {
OutputFormatArg::Console => Self::Console,
OutputFormatArg::Json => Self::Json,
OutputFormatArg::Csv => Self::Csv,
OutputFormatArg::Markdown => Self::Markdown,
OutputFormatArg::Html => Self::Html,
}
}
}
impl From<SortByArg> for codelens_core::config::SortBy {
fn from(arg: SortByArg) -> Self {
match arg {
SortByArg::Lines => Self::Lines,
SortByArg::Files => Self::Files,
SortByArg::Code => Self::Code,
SortByArg::Name => Self::Name,
SortByArg::Size => Self::Size,
}
}
}