use std::{fmt::Display, fs, io::ErrorKind, path::PathBuf, usize};
use clap::Parser;
use colored::Colorize;
use ignore::WalkBuilder;
use regex::Regex;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
dir: Option<PathBuf>,
#[arg(short, long)]
exclude: Vec<String>,
#[arg(long)]
hidden: bool,
#[arg(long)]
ascending: bool,
#[arg(short, long)]
num: bool
}
#[derive(Debug)]
struct Todo {
urgency: usize,
comment: String,
path: PathBuf,
line: usize
}
impl Display for Todo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.display(false))
}
}
impl Todo {
fn display(&self, num: bool) -> String {
let mut todo = "TOD".to_string();
if !num {
for _ in 0..self.urgency {
todo = todo + "O";
}
} else {
todo = todo + "O" + format!(" ({})", self.urgency).as_str();
}
let loc = format!("{}:{}", self.path.display(), self.line);
let mut tod = format!("{}: {}", todo, self.comment);
tod = match self.urgency {
0 | 1 | 2 => tod.green().to_string(),
3 | 4 => tod.yellow().to_string(),
5.. => tod.red().to_string()
};
format!("{} {}", loc, tod)
}
}
fn main() {
let mut args = Args::parse();
args.exclude.append(&mut vec!["LICENSE".to_string(), "README.md".to_string()]);
let dir = args.dir.unwrap_or(".".into());
let walker = WalkBuilder::new(dir)
.hidden(!args.hidden)
.follow_links(false)
.filter_entry(move |file| {
let file_name = file.file_name().to_str().unwrap_or("");
let not_lock = !file_name.ends_with(".lock");
let not_excluded = !args.exclude.contains(&file_name.to_string());
not_lock && not_excluded
})
.build();
let comment_regex = Regex::new(r"(?:.*)(?:(?:(?:/?)//)|(?:#))\s?TOD(O*)(?::)? (.*)").unwrap();
let mut todos: Vec<Todo> = vec![];
for ele in walker {
match ele {
Ok(a) => {
if a.file_type().unwrap().is_dir() {
continue;
} else {
let contents = match fs::read_to_string(a.path()) {
Ok(a) => a,
Err(e) => {
if e.kind() == ErrorKind::InvalidData { continue; }
eprintln!("Error reading file: {e}");
continue;
},
};
let lines = contents.split("\n");
let mut i = 0;
for line in lines {
i += 1;
for (_, [os, todo]) in comment_regex.captures_iter(line).map(|c| c.extract()) {
let todo = Todo {
urgency: os.len(),
comment: todo.to_string(),
line: i,
path: a.path().to_path_buf()
};
todos.push(todo);
};
}
}
},
Err(e) => {
eprintln!("Error: {e}");
},
}
}
if args.ascending {
todos.sort_by(|a, b| a.urgency.partial_cmp(&b.urgency).unwrap() );
} else {
todos.sort_by(|a, b| b.urgency.partial_cmp(&a.urgency).unwrap() );
}
if todos.is_empty() {
println!("No TODO's found. Well done ^^");
} else {
for todo in todos {
println!("{}", todo.display(args.num));
}
}
}