diff --git a/Cargo.lock b/Cargo.lock index 2b1d83bf2..7f71a1715 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "bstr" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" +checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" dependencies = [ "lazy_static", "memchr", @@ -521,7 +521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", - "humantime 1.3.0", + "humantime", "log", "regex", "termcolor", @@ -533,10 +533,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" dependencies = [ - "atty", - "humantime 2.1.0", "log", - "regex", "termcolor", ] @@ -661,12 +658,6 @@ dependencies = [ "quick-error", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "ident_case" version = "1.0.1" @@ -1174,9 +1165,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efb2352a0f4d4b128f734b5c44c79ff80117351138733f12f982fe3e2b13343" +checksum = "ce5f1ceb7f74abbce32601642fcf8e8508a8a8991e0621c7d750295b9095702b" dependencies = [ "aho-corasick", "memchr", @@ -1194,9 +1185,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.24" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00efb87459ba4f6fb2169d20f68565555688e1250ee6825cdf6254f8b48fafb2" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "remove_dir_all" @@ -1209,9 +1200,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" +checksum = "410f7acf3cb3a44527c5d9546bad4bf4e6c460915d5f9f2fc524498bfe8f70ce" [[package]] name = "rustc_version" @@ -1485,6 +1476,7 @@ dependencies = [ "ahash", "anyhow", "assert_fs", + "atty", "bstr", "clap", "clap-verbosity-flag", @@ -1516,6 +1508,7 @@ dependencies = [ "unicase", "unicode-segmentation", "varcon-core", + "yansi", ] [[package]] @@ -1797,3 +1790,9 @@ name = "wyz" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "yansi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" diff --git a/Cargo.toml b/Cargo.toml index 658389e17..bc31c8991 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,9 @@ ignore = "0.4" serde = { version = "1.0", features = ["derive"] } toml = "0.5" log = "0.4" -env_logger = "0.8" +env_logger = { version = "0.8", default-features = false, features = ["termcolor"] } +atty = "0.2.14" +yansi = "0.5.0" bstr = "0.2" once_cell = "1.2.0" ahash = "0.7" diff --git a/src/bin/typos-cli/args.rs b/src/bin/typos-cli/args.rs index fd6c9f4b0..5f170d9bc 100644 --- a/src/bin/typos-cli/args.rs +++ b/src/bin/typos-cli/args.rs @@ -12,18 +12,23 @@ arg_enum! { } } -pub const PRINT_SILENT: crate::report::PrintSilent = crate::report::PrintSilent; -pub const PRINT_BRIEF: crate::report::PrintBrief = crate::report::PrintBrief; -pub const PRINT_LONG: crate::report::PrintLong = crate::report::PrintLong; -pub const PRINT_JSON: crate::report::PrintJson = crate::report::PrintJson; - impl Format { - pub(crate) fn reporter(self) -> &'static dyn typos_cli::report::Report { + pub(crate) fn reporter( + self, + stdout_palette: crate::report::Palette, + stderr_palette: crate::report::Palette, + ) -> Box { match self { - Format::Silent => &PRINT_SILENT, - Format::Brief => &PRINT_BRIEF, - Format::Long => &PRINT_LONG, - Format::Json => &PRINT_JSON, + Format::Silent => Box::new(crate::report::PrintSilent), + Format::Brief => Box::new(crate::report::PrintBrief { + stdout_palette, + stderr_palette, + }), + Format::Long => Box::new(crate::report::PrintLong { + stdout_palette, + stderr_palette, + }), + Format::Json => Box::new(crate::report::PrintJson), } } } @@ -98,6 +103,9 @@ pub(crate) struct Args { #[structopt(flatten)] pub(crate) config: ConfigArgs, + #[structopt(flatten)] + pub(crate) color: crate::color::ColorArgs, + #[structopt(flatten)] pub(crate) verbose: clap_verbosity_flag::Verbosity, } diff --git a/src/bin/typos-cli/color.rs b/src/bin/typos-cli/color.rs new file mode 100644 index 000000000..2a3aa9772 --- /dev/null +++ b/src/bin/typos-cli/color.rs @@ -0,0 +1,100 @@ +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +pub struct ColorArgs { + /// "Specify when to use colored output. The automatic mode + /// only enables colors if an interactive terminal is detected - + /// colors are automatically disabled if the output goes to a pipe. + /// + /// Possible values: *auto*, never, always. + #[structopt( + long, + value_name="when", + possible_values(&ColorValue::variants()), + case_insensitive(true), + default_value("auto"), + hide_possible_values(true), + hide_default_value(true), + help="When to use colors (*auto*, never, always).")] + color: ColorValue, +} + +impl ColorArgs { + pub fn colored(&self) -> Option { + self.color.colored() + } +} + +arg_enum! { + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + pub enum ColorValue { + Always, + Never, + Auto, + } +} + +impl ColorValue { + fn colored(self) -> Option { + match self { + ColorValue::Always => Some(true), + ColorValue::Never => Some(false), + ColorValue::Auto => None, + } + } +} + +impl Default for ColorValue { + fn default() -> Self { + ColorValue::Auto + } +} + +pub fn colored_stdout() -> Option { + if atty::is(atty::Stream::Stdout) { + None + } else { + Some(false) + } +} + +pub fn colored_stderr() -> Option { + if atty::is(atty::Stream::Stderr) { + None + } else { + Some(false) + } +} + +pub fn colored_env() -> Option { + match std::env::var_os("TERM") { + None => noterm_colored(), + Some(k) => { + if k == "dumb" { + Some(false) + } else { + None + } + } + } + .or_else(|| { + // See https://no-color.org/ + std::env::var_os("NO_COLOR").map(|_| true) + }) +} + +#[cfg(not(windows))] +fn noterm_colored() -> Option { + // If TERM isn't set, then we are in a weird environment that + // probably doesn't support colors. + Some(false) +} + +#[cfg(windows)] +fn noterm_colored() -> Option { + // On Windows, if TERM isn't set, then we shouldn't automatically + // assume that colors aren't allowed. This is unlike Unix environments + // where TERM is more rigorously set. + None +} diff --git a/src/bin/typos-cli/main.rs b/src/bin/typos-cli/main.rs index eba486ba2..7b71d1d79 100644 --- a/src/bin/typos-cli/main.rs +++ b/src/bin/typos-cli/main.rs @@ -7,6 +7,7 @@ use std::io::Write; use structopt::StructOpt; mod args; +mod color; mod report; use proc_exit::WithCodeResultExt; @@ -30,14 +31,35 @@ fn run() -> proc_exit::ExitResult { } }; - init_logging(args.verbose.log_level()); + let colored = args.color.colored().or_else(color::colored_env); + let mut colored_stdout = colored.or_else(color::colored_stdout).unwrap_or(true); + let mut colored_stderr = colored.or_else(color::colored_stderr).unwrap_or(true); + if colored_stdout || colored_stderr { + if !yansi::Paint::enable_windows_ascii() { + colored_stdout = false; + colored_stderr = false; + } + } + + init_logging(args.verbose.log_level(), colored_stderr); + + let stdout_palette = if colored_stdout { + report::Palette::colored() + } else { + report::Palette::plain() + }; + let stderr_palette = if colored_stderr { + report::Palette::colored() + } else { + report::Palette::plain() + }; if let Some(output_path) = args.dump_config.as_ref() { run_dump_config(&args, output_path) } else if args.type_list { run_type_list(&args) } else { - run_checks(&args) + run_checks(&args, stdout_palette, stderr_palette) } } @@ -136,7 +158,11 @@ fn run_type_list(args: &args::Args) -> proc_exit::ExitResult { Ok(()) } -fn run_checks(args: &args::Args) -> proc_exit::ExitResult { +fn run_checks( + args: &args::Args, + stdout_palette: report::Palette, + stderr_palette: report::Palette, +) -> proc_exit::ExitResult { let global_cwd = std::env::current_dir()?; let storage = typos_cli::policy::ConfigStorage::new(); @@ -187,11 +213,11 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult { // HACK: Diff doesn't handle mixing content let output_reporter = if args.diff { - &args::PRINT_SILENT + Box::new(crate::report::PrintSilent) } else { - args.format.reporter() + args.format.reporter(stdout_palette, stderr_palette) }; - let status_reporter = report::MessageStatus::new(output_reporter); + let status_reporter = report::MessageStatus::new(output_reporter.as_ref()); let reporter: &dyn typos_cli::report::Report = &status_reporter; let selected_checks: &dyn typos_cli::file::FileChecker = if args.files { @@ -245,9 +271,14 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult { } } -fn init_logging(level: Option) { +fn init_logging(level: Option, colored: bool) { if let Some(level) = level { let mut builder = env_logger::Builder::new(); + builder.write_style(if colored { + env_logger::WriteStyle::Always + } else { + env_logger::WriteStyle::Never + }); builder.filter(None, level.to_level_filter()); diff --git a/src/bin/typos-cli/report.rs b/src/bin/typos-cli/report.rs index afaad559f..79d1b7615 100644 --- a/src/bin/typos-cli/report.rs +++ b/src/bin/typos-cli/report.rs @@ -5,6 +5,34 @@ use std::sync::atomic; use typos_cli::report::{Context, Message, Report, Typo}; +#[derive(Copy, Clone, Debug)] +pub struct Palette { + error: yansi::Style, + warn: yansi::Style, + info: yansi::Style, + strong: yansi::Style, +} + +impl Palette { + pub fn colored() -> Self { + Self { + error: yansi::Style::new(yansi::Color::Red), + warn: yansi::Style::new(yansi::Color::Yellow), + info: yansi::Style::new(yansi::Color::Blue), + strong: yansi::Style::default().bold(), + } + } + + pub fn plain() -> Self { + Self { + error: yansi::Style::default(), + warn: yansi::Style::default(), + info: yansi::Style::default(), + strong: yansi::Style::default(), + } + } +} + pub struct MessageStatus<'r> { typos_found: atomic::AtomicBool, errors_found: atomic::AtomicBool, @@ -59,8 +87,10 @@ impl Report for PrintSilent { } } -#[derive(Copy, Clone, Debug)] -pub struct PrintBrief; +pub struct PrintBrief { + pub stdout_palette: Palette, + pub stderr_palette: Palette, +} impl Report for PrintBrief { fn report(&self, msg: Message) -> Result<(), std::io::Error> { @@ -68,7 +98,7 @@ impl Report for PrintBrief { Message::BinaryFile(msg) => { log::info!("{}", msg); } - Message::Typo(msg) => print_brief_correction(msg)?, + Message::Typo(msg) => print_brief_correction(msg, self.stdout_palette)?, Message::File(msg) => { writeln!(io::stdout(), "{}", msg.path.display())?; } @@ -84,8 +114,10 @@ impl Report for PrintBrief { } } -#[derive(Copy, Clone, Debug)] -pub struct PrintLong; +pub struct PrintLong { + pub stdout_palette: Palette, + pub stderr_palette: Palette, +} impl Report for PrintLong { fn report(&self, msg: Message) -> Result<(), std::io::Error> { @@ -93,7 +125,7 @@ impl Report for PrintLong { Message::BinaryFile(msg) => { log::info!("{}", msg); } - Message::Typo(msg) => print_long_correction(msg)?, + Message::Typo(msg) => print_long_correction(msg, self.stdout_palette)?, Message::File(msg) => { writeln!(io::stdout(), "{}", msg.path.display())?; } @@ -109,7 +141,7 @@ impl Report for PrintLong { } } -fn print_brief_correction(msg: &Typo) -> Result<(), std::io::Error> { +fn print_brief_correction(msg: &Typo, palette: Palette) -> Result<(), std::io::Error> { let line = String::from_utf8_lossy(msg.buffer.as_ref()); let line = line.replace("\t", " "); let column = unicode_segmentation::UnicodeSegmentation::graphemes( @@ -120,22 +152,31 @@ fn print_brief_correction(msg: &Typo) -> Result<(), std::io::Error> { match &msg.corrections { typos::Status::Valid => {} typos::Status::Invalid => { + let divider = ":"; writeln!( io::stdout(), - "{}:{}: `{}` is disallowed", - context_display(&msg.context), - column, - msg.typo, + "{}{}{}: {}", + palette.info.paint(context_display(&msg.context)), + palette.info.paint(divider), + palette.info.paint(column), + palette + .strong + .paint(format_args!("`{}` is disallowed:", msg.typo)), )?; } typos::Status::Corrections(corrections) => { + let divider = ":"; writeln!( io::stdout(), - "{}:{}: `{}` -> {}", - context_display(&msg.context), - column, - msg.typo, - itertools::join(corrections.iter().map(|s| format!("`{}`", s)), ", ") + "{}{}{}: {}", + palette.info.paint(context_display(&msg.context)), + palette.info.paint(divider), + palette.info.paint(column), + palette.strong.paint(format_args!( + "`{}` -> {}", + msg.typo, + itertools::join(corrections.iter().map(|s| format!("`{}`", s)), ", ") + )), )?; } } @@ -143,7 +184,7 @@ fn print_brief_correction(msg: &Typo) -> Result<(), std::io::Error> { Ok(()) } -fn print_long_correction(msg: &Typo) -> Result<(), std::io::Error> { +fn print_long_correction(msg: &Typo, palette: Palette) -> Result<(), std::io::Error> { let stdout = io::stdout(); let mut handle = stdout.lock(); @@ -157,18 +198,36 @@ fn print_long_correction(msg: &Typo) -> Result<(), std::io::Error> { match &msg.corrections { typos::Status::Valid => {} typos::Status::Invalid => { - writeln!(handle, "error: `{}` is disallowed`", msg.typo,)?; + writeln!( + handle, + "{}: {}", + palette.error.bold().paint("error"), + palette + .strong + .paint(format_args!("`{}` is disallowed`", msg.typo)) + )?; } typos::Status::Corrections(corrections) => { writeln!( handle, - "error: `{}` should be {}", - msg.typo, - itertools::join(corrections.iter().map(|s| format!("`{}`", s)), ", ") + "{}: {}", + palette.error.bold().paint("error"), + palette.strong.paint(format_args!( + "`{}` should be {}", + msg.typo, + itertools::join(corrections.iter().map(|s| format!("`{}`", s)), ", ") + )) )?; } } - writeln!(handle, " --> {}:{}", context_display(&msg.context), column)?; + let divider = ":"; + writeln!( + handle, + " --> {}{}{}", + palette.info.paint(context_display(&msg.context)), + palette.info.paint(divider), + palette.info.paint(column) + )?; if let Some(Context::File(context)) = &msg.context { let line_num = context.line_num.to_string(); @@ -178,8 +237,19 @@ fn print_long_correction(msg: &Typo) -> Result<(), std::io::Error> { let hl: String = itertools::repeat_n("^", msg.typo.len()).collect(); writeln!(handle, "{} |", line_indent)?; - writeln!(handle, "{} | {}", line_num, line.trim_end())?; - writeln!(handle, "{} | {}{}", line_indent, hl_indent, hl)?; + writeln!( + handle, + "{} | {}", + palette.info.paint(line_num), + line.trim_end() + )?; + writeln!( + handle, + "{} | {}{}", + line_indent, + hl_indent, + palette.error.paint(hl) + )?; writeln!(handle, "{} |", line_indent)?; }