Skip to content

Commit

Permalink
feat(cli): Colored output
Browse files Browse the repository at this point in the history
This supports
- Basic capability detection
- NO_COLOR env variable
- tty detection
- CLI overrides

This does not yet support CLICOLOR.  I'll be trying to upstream all of
this into `yansi` and get it taken care of there.

This only supports Windows Anniversary edition and later which I think
is a fine compromise due to the ergonomic difference between `yansi` and
`termcolor`.

Fixes #30
  • Loading branch information
Ed Page committed May 12, 2021
1 parent b9d1f9d commit fa38ad4
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 60 deletions.
35 changes: 17 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
28 changes: 18 additions & 10 deletions src/bin/typos-cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn typos_cli::report::Report> {
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),
}
}
}
Expand Down Expand Up @@ -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,
}
Expand Down
100 changes: 100 additions & 0 deletions src/bin/typos-cli/color.rs
Original file line number Diff line number Diff line change
@@ -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<bool> {
self.color.colored()
}
}

arg_enum! {
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ColorValue {
Always,
Never,
Auto,
}
}

impl ColorValue {
fn colored(self) -> Option<bool> {
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<bool> {
if atty::is(atty::Stream::Stdout) {
None
} else {
Some(false)
}
}

pub fn colored_stderr() -> Option<bool> {
if atty::is(atty::Stream::Stderr) {
None
} else {
Some(false)
}
}

pub fn colored_env() -> Option<bool> {
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<bool> {
// 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<bool> {
// 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
}
45 changes: 38 additions & 7 deletions src/bin/typos-cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::io::Write;
use structopt::StructOpt;

mod args;
mod color;
mod report;

use proc_exit::WithCodeResultExt;
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -245,9 +271,14 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult {
}
}

fn init_logging(level: Option<log::Level>) {
fn init_logging(level: Option<log::Level>, 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());

Expand Down
Loading

0 comments on commit fa38ad4

Please sign in to comment.