diff --git a/src/color.rs b/src/color.rs index fd67fdc..aa63bc3 100644 --- a/src/color.rs +++ b/src/color.rs @@ -8,6 +8,10 @@ pub struct Color { } impl Color { + pub fn hex(&self) -> String { + format!("#{:02x}{:02x}{:02x}", self.r, self.g, self.b) + } + pub fn color(&self, t: T) -> String where T: Colorize, @@ -59,4 +63,10 @@ mod tests { } ); } + + #[test] + fn hex() { + assert_eq!(Color::from([0, 0, 0]).hex(), "#000000"); + assert_eq!(Color::from(0xbeeeef).hex(), "#beeeef"); + } } diff --git a/src/help.txt b/src/help.txt index f6e7aef..72e7607 100644 --- a/src/help.txt +++ b/src/help.txt @@ -4,10 +4,14 @@ usage: kc [options] [directory] include hidden files and directories -A include ignored files and directories + --blame + list all of the files for each language -t, --top [number] only show the top few languages -x, --exclude [name | extension] exclude a language based on name or file extension + -o, --only [name | extension] + only include the languages specified -l, --lines only report the total number of lines in all files --reporter [name] diff --git a/src/langs.rs b/src/langs.rs index 05a71ad..f667ca2 100644 --- a/src/langs.rs +++ b/src/langs.rs @@ -126,16 +126,7 @@ impl Language { impl Display for Language { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let info = LanguageInfo::from(self); - - write!( - f, - "{} {}", - info - .color - .map(|color| color.color("●")) - .unwrap_or_else(|| "●".to_string()), - &info.name - ) + write!(f, "{}", &info.name) } } diff --git a/src/options.rs b/src/options.rs index 72db639..ecd855f 100644 --- a/src/options.rs +++ b/src/options.rs @@ -72,7 +72,7 @@ where println!("{}", include_str!("./help.txt")); exit(0); } - "-reporter" | "--reporter" => { + "-O" | "-reporter" | "--reporter" => { options.reporter = args .next() .expect(&format!("expected a reporter to follow {} flag", arg)) @@ -105,7 +105,7 @@ where .expect(&format!("unable to parse \"{}\" as a number", arg)) .into(); } - "-x" | "-exclude" | "--exclude" => { + "-x" | "-exclude" | "--exclude" | "-ignore" | "--ignore" => { let exclusions = args.next(); let list = exclusions .as_ref() @@ -140,14 +140,14 @@ where options.reporter = TotalLines; } _ => { - println!("unrecognized option: {}", arg); + eprintln!("unrecognized option: {}", arg); exit(1); } } } if !options.only_include.is_empty() && !options.excluded.is_empty() { - println!("warning: both --only and --exclude have been set, which doesn't really make sense") + eprintln!("warning: both --only and --exclude have been set, which doesn't really make sense") } options @@ -254,5 +254,13 @@ mod tests { ..Default::default() }, ); + + assert_eq!( + ["-O", "html"].into_iter().collect::(), + Options { + reporter: Reporter::Html, + ..Default::default() + }, + ); } } diff --git a/src/reporters.rs b/src/reporters.rs index 6bd8f4d..f3afdb2 100644 --- a/src/reporters.rs +++ b/src/reporters.rs @@ -1,10 +1,12 @@ use std::str::FromStr; +pub mod html; pub mod terminal; pub mod total_lines; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Reporter { + Html, Terminal, TotalLines, } @@ -14,6 +16,7 @@ impl FromStr for Reporter { fn from_str(s: &str) -> Result { match s.to_ascii_lowercase().as_ref() { + "html" => Ok(Self::Html), "terminal" => Ok(Self::Terminal), "total" | "total_lines" | "total-lines" | "totalLines" => Ok(Self::TotalLines), _ => Err(()), @@ -23,6 +26,6 @@ impl FromStr for Reporter { impl Reporter { pub fn help() -> &'static str { - "\"terminal\", \"total-lines\"" + "\"html\", \"terminal\", \"total-lines\"" } } diff --git a/src/reporters/html.rs b/src/reporters/html.rs new file mode 100644 index 0000000..861082d --- /dev/null +++ b/src/reporters/html.rs @@ -0,0 +1,86 @@ +use crate::color::Color; +use crate::langs::Language; +use crate::langs::LanguageInfo; +use crate::langs::LanguageSummary; +use crate::options::Options; + +pub struct HtmlReporter; + +const ROW_STYLES: &str = include_str!("./html_reporter.css"); + +impl HtmlReporter { + pub fn report( + summaries: Vec<(Language, LanguageSummary)>, + options: Options, + ) -> anyhow::Result<()> { + print!("\n"); + print!( + "\n\nkc — {}\n\n\n", + options.root_dir.display(), + ROW_STYLES + ); + print!("\n\n"); + + // The bar + let total_lines = summaries + .iter() + .map(|(_, summary)| summary.lines) + .sum::(); + let mut remaining_lines = total_lines; + let total_lines = total_lines as f32; + + println!("
"); + { + for (_, stat) in summaries.iter() { + // If there are 0 total lines, then just say everything is 0%. + let percent = stat.lines as f32 / total_lines; + if percent.is_nan() || percent < 0.02 { + break; + } + + remaining_lines -= stat.lines; + + let lang = LanguageInfo::from(&stat.language); + let color = lang + .color + .as_ref() + .map(Color::hex) + .unwrap_or("gray".to_string()); + + println!( + "\t
", + lang.name, color, stat.lines, + ); + } + + println!( + "\t
", + remaining_lines, + ); + } + print!("
\n\n"); + + print!( + "\n\ + \n\ + \n" + ); + + for (_, stat) in summaries.iter() { + let lang = LanguageInfo::from(&stat.language); + let color = lang + .color + .as_ref() + .map(Color::hex) + .unwrap_or("gray".to_string()); + println!( + "\t", + color, stat.language, stat.lines, stat.blank_lines + ); + } + print!("
LanguageLinesBlank
 {}{}{}
\n\n"); + + print!("\n\n"); + Ok(()) + } +} diff --git a/src/reporters/html_reporter.css b/src/reporters/html_reporter.css new file mode 100644 index 0000000..2d0f2d5 --- /dev/null +++ b/src/reporters/html_reporter.css @@ -0,0 +1,46 @@ +:root { + font-family: Avenir, Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif; + font-weight: normal; +} + +body { + margin: 0 auto; + padding: 48px 24px 24px; + max-width: 968px; +} + +.bar { + display: flex; + gap: 2px; + border-radius: 10px; + height: 10px; + overflow: hidden; + margin-bottom: 32px; +} + +.bar > * { + flex-basis: 0; +} + +table { + width: 100%; +} + +th { + font-size: 1.2em; +} + +th:first-of-type, +td:first-of-type { + text-align: left; +} + +th, +td { + text-align: right; +} + +td { + border-bottom: 1px dotted black; + padding: 0.2em 0; +} diff --git a/src/reporters/terminal.rs b/src/reporters/terminal.rs index d52f651..8d9f1a9 100644 --- a/src/reporters/terminal.rs +++ b/src/reporters/terminal.rs @@ -34,11 +34,12 @@ impl TerminalReporter { ) } - let total_lines = summaries - .iter() - .map(|(_, summary)| summary.lines) - .reduce(|acc, lines| acc + lines) - .ok_or_else(|| anyhow!("no code found in \"{}\"", dir_path.display()))?; + let total_lines = summaries.iter().map(|(_, summary)| summary.lines).sum(); + + if total_lines == 0 { + println!(" no code found in {}", dir_path.display()); + return Ok(()); + } let mut filled = 0; @@ -102,7 +103,19 @@ impl<'a, 'b> Display for TerminalLanguageSummary<'a, 'b> { let inlay = format!("{:.>width$}", "", width = width) .bright_black() .to_string(); - write!(f, "{} {} {}", summary.language, inlay, right_side)?; + + let info = LanguageInfo::from(&summary.language); + write!( + f, + "{} {} {} {}", + info + .color + .map(|color| color.color("●")) + .unwrap_or_else(|| "●".to_string()), + info.name, + inlay, + right_side + )?; if options.blame { let mut files = summary.files.iter().peekable(); diff --git a/src/scan.rs b/src/scan.rs index 5fe335e..df563ec 100644 --- a/src/scan.rs +++ b/src/scan.rs @@ -8,6 +8,7 @@ use crate::fc::FileContent; use crate::langs::Language; use crate::langs::LanguageSummary; use crate::options::Options; +use crate::reporters::html::HtmlReporter; use crate::reporters::terminal::TerminalReporter; use crate::reporters::total_lines::TotalLinesReporter; use crate::reporters::Reporter::*; @@ -77,6 +78,7 @@ pub fn scan(options: Options) -> anyhow::Result<()> { } match options.reporter { + Html => HtmlReporter::report(summaries, options), Terminal => TerminalReporter::report(summaries, options), TotalLines => TotalLinesReporter::report(summaries, options), }