mod analyzer;
mod config;
mod logger;
mod render;
mod utils;
use std::path::Path;
use std::time::Instant;
use clap::{Parser, Subcommand};
use dirs;
use crate::config::{LanguageIndex, LanguagesConfig};
use crate::logger::{logger, Level, Log, Verbosity};
#[derive(Parser, Debug)]
#[command(name = "lcount", version)]
#[command(about = "A command-line tool written in Rust that counts lines of code. It recursively scans directories, counts lines by programming language, and visualizes them in a colorful horizontal bar chart in the terminal.", long_about = None)]
#[command(propagate_version = true)]
struct Args {
#[arg(short, long, default_value = ".", global = true)]
path: String,
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
verbose: u8,
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
Stats {
#[arg(short, long, value_name = "COMMIT_HASH")]
commit: Option<String>,
},
Diff {
#[arg(short, long)]
base: String,
#[arg(short, long, default_value_t = String::from("WORKTREE"))]
target: String,
},
}
fn main() {
let args = Args::parse();
let path = Path::new(&args.path);
let mut cfg_path = match dirs::config_dir() {
Some(path) => path,
None => {
logger(
Log::new(
Level::Error,
"Failed to get the root configuration folder".to_string(),
Some(Verbosity::Normal),
),
Some(Verbosity::from_u8(&args.verbose)),
);
return;
}
};
cfg_path = cfg_path.join(Path::new("paline").join(Path::new("lang.toml")));
if !cfg_path.exists() {
if let Err(log) = LanguagesConfig::init_default_file(&cfg_path) {
logger(log, Some(Verbosity::from_u8(&args.verbose)));
return;
}
logger(
Log::new(
Level::Info,
"Created default language config file: lang.toml".to_string(),
Some(Verbosity::Verbose),
),
Some(Verbosity::from_u8(&args.verbose)),
);
}
let config = match LanguagesConfig::from_file(&cfg_path) {
Ok(lang_cfg) => lang_cfg,
Err(log) => {
logger(log, Some(Verbosity::from_u8(&args.verbose)));
return;
}
};
let index = config.build_index();
if !path.exists() || !path.is_dir() {
logger(
Log::new(
Level::Error,
format!(
"The path doesn't exist or isn't a directory: \"{}\". Please check if the path is correct.",
&args.path
),
None,
),
None,
);
return;
}
match args.command {
Commands::Stats { commit } => handle_stats(path, &args.verbose, commit, &config, &index),
Commands::Diff { base, target } => {
handle_diff(path, &args.verbose, base, target, &config, &index)
}
}
}
fn handle_stats(
path: &Path,
verbose: &u8,
commit: Option<String>,
_config: &LanguagesConfig,
index: &LanguageIndex,
) {
let t_start = Instant::now();
let stats = if let Some(commit_hash) = &commit {
let repo = match utils::open_repository(path) {
Ok(r) => r,
Err(log) => {
logger(log, Some(Verbosity::from_u8(verbose)));
return;
}
};
let commit_obj = match utils::get_commit_from(&repo, commit_hash) {
Ok(c) => c,
Err(log) => {
logger(log, Some(Verbosity::from_u8(verbose)));
return;
}
};
logger(
Log::new(
Level::Info,
format!(
"scanning commit {} ({})",
&commit_hash[..12.min(commit_hash.len())],
commit_obj.summary().unwrap_or("<no message>")
),
Some(Verbosity::Verbose),
),
Some(Verbosity::from_u8(verbose)),
);
analyzer::analyze_project(
&analyzer::Project::Commit {
repo: &repo,
commit: &commit_obj,
},
index,
verbose,
)
} else {
analyzer::analyze_project(&analyzer::Project::Disk { root: path }, index, verbose)
};
let t_analyzed = t_start.elapsed();
logger(
Log::new(
Level::Info,
format!("analyze: {:.2?}", t_analyzed),
Some(Verbosity::Verbose),
),
Some(Verbosity::from_u8(verbose)),
);
if let Err(log) = render::render(render::Mode::Stats { stats }, index) {
logger(log, Some(Verbosity::from_u8(verbose)));
}
}
fn handle_diff(
path: &Path,
verbose: &u8,
base: String,
target: String,
_config: &LanguagesConfig,
index: &LanguageIndex,
) {
let t_start = Instant::now();
let repo = match utils::open_repository(path) {
Ok(r) => r,
Err(log) => {
logger(log, Some(Verbosity::from_u8(verbose)));
return;
}
};
let base_commit = match utils::get_commit_from(&repo, &base) {
Ok(c) => c,
Err(log) => {
logger(log, Some(Verbosity::from_u8(verbose)));
return;
}
};
logger(
Log::new(
Level::Info,
format!(
"diff base commit {} ({})",
&base[..12.min(base.len())],
base_commit.summary().unwrap_or("<no message>")
),
Some(Verbosity::Verbose),
),
Some(Verbosity::from_u8(verbose)),
);
let base_project = analyzer::Project::Commit {
repo: &repo,
commit: &base_commit,
};
let base_stats = analyzer::analyze_project(&base_project, index, verbose);
let target_stats = if target.eq_ignore_ascii_case("WORKTREE") {
logger(
Log::new(
Level::Info,
"target: WORKTREE".to_string(),
Some(Verbosity::Verbose),
),
Some(Verbosity::from_u8(verbose)),
);
analyzer::analyze_project(&analyzer::Project::Disk { root: path }, index, verbose)
} else {
let target_commit = match utils::get_commit_from(&repo, &target) {
Ok(c) => c,
Err(log) => {
logger(log, Some(Verbosity::from_u8(verbose)));
return;
}
};
logger(
Log::new(
Level::Info,
format!(
"target commit {} ({})",
&target[..12.min(target.len())],
target_commit.summary().unwrap_or("<no message>")
),
Some(Verbosity::Verbose),
),
Some(Verbosity::from_u8(verbose)),
);
analyzer::analyze_project(
&analyzer::Project::Commit {
repo: &repo,
commit: &target_commit,
},
index,
verbose,
)
};
let t_analyzed = t_start.elapsed();
logger(
Log::new(
Level::Info,
format!("analyze: {:.2?}", t_analyzed),
Some(Verbosity::Verbose),
),
Some(Verbosity::from_u8(verbose)),
);
if let Err(log) = render::render(
render::Mode::Diff {
base: base_stats,
target: target_stats,
},
index,
) {
logger(log, Some(Verbosity::from_u8(verbose)));
}
}