Skip to content

Commit

Permalink
Completely re-work colored output and tty handling.
Browse files Browse the repository at this point in the history
This commit completely guts all of the color handling code and replaces
most of it with two new crates: wincolor and termcolor. wincolor
provides a simple API to coloring using the Windows console and
termcolor provides a platform independent coloring API tuned for
multithreaded command line programs. This required a lot more
flexibility than what the `term` crate provided, so it was dropped.
We instead switch to writing ANSI escape sequences directly and ignore
the TERMINFO database.

In addition to fixing several bugs, this commit also permits end users
to customize colors to a certain extent. For example, this command will
set the match color to magenta and the line number background to yellow:

    rg --colors 'match:fg:magenta' --colors 'line:bg:yellow' foo

For tty handling, we've adopted a hack from `git` to do tty detection in
MSYS/mintty terminals. As a result, ripgrep should get both color
detection and piping correct on Windows regardless of which terminal you
use.

Finally, switch to line buffering. Performance doesn't seem to be
impacted and it's an otherwise more user friendly option.

Fixes #37, Fixes #51, Fixes #94, Fixes #117, Fixes #182, Fixes #231
  • Loading branch information
BurntSushi committed Nov 20, 2016
1 parent 03f7605 commit e8a30cb
Show file tree
Hide file tree
Showing 23 changed files with 2,153 additions and 739 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ target
/grep/Cargo.lock
/globset/Cargo.lock
/ignore/Cargo.lock
/termcolor/Cargo.lock
/wincolor/Cargo.lock
35 changes: 20 additions & 15 deletions Cargo.lock

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

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ memchr = "0.1"
memmap = "0.5"
num_cpus = "1"
regex = "0.1.77"
term = "0.4"
termcolor = { version = "0.1.0", path = "termcolor" }

[target.'cfg(windows)'.dependencies]
kernel32-sys = "0.2"
winapi = "0.2"
kernel32-sys = "0.2.2"
winapi = "0.2.8"

[build-dependencies]
clap = "2.18"
Expand Down
2 changes: 2 additions & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ test_script:
- cargo test --verbose --manifest-path grep/Cargo.toml
- cargo test --verbose --manifest-path globset/Cargo.toml
- cargo test --verbose --manifest-path ignore/Cargo.toml
- cargo test --verbose --manifest-path wincolor/Cargo.toml
- cargo test --verbose --manifest-path termcolor/Cargo.toml

before_deploy:
# Generate artifacts for release
Expand Down
2 changes: 2 additions & 0 deletions ci/script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ run_test_suite() {
cargo test --target $TARGET --verbose --manifest-path globset/Cargo.toml
cargo build --target $TARGET --verbose --manifest-path ignore/Cargo.toml
cargo test --target $TARGET --verbose --manifest-path ignore/Cargo.toml
cargo build --target $TARGET --verbose --manifest-path termcolor/Cargo.toml
cargo test --target $TARGET --verbose --manifest-path termcolor/Cargo.toml

# sanity check the file type
file target/$TARGET/debug/rg
Expand Down
22 changes: 22 additions & 0 deletions doc/rg.1
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,28 @@ Show NUM lines before and after each match.
.RS
.RE
.TP
.B \-\-colors \f[I]SPEC\f[] ...
This flag specifies color settings for use in the output.
This flag may be provided multiple times.
Settings are applied iteratively.
Colors are limited to one of eight choices: red, blue, green, cyan,
magenta, yellow, white and black.
Styles are limited to either nobold or bold.
.RS
.PP
The format of the flag is {type}:{attribute}:{value}.
{type} should be one of path, line or match.
{attribute} can be fg, bg or style.
Value is either a color (for fg and bg) or a text style.
A special format, {type}:none, will clear all color settings for {type}.
.PP
For example, the following command will change the match color to
magenta and the background color for line numbers to yellow:
.PP
rg \-\-colors \[aq]match:fg:magenta\[aq] \-\-colors
\[aq]line:bg:yellow\[aq] foo.
.RE
.TP
.B \-\-column
Show column numbers (1 based) in output.
This only shows the column numbers for the first match on each line.
Expand Down
16 changes: 16 additions & 0 deletions doc/rg.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,22 @@ Project home page: https://github.com/BurntSushi/ripgrep
-C, --context *NUM*
: Show NUM lines before and after each match.

--colors *SPEC* ...
: This flag specifies color settings for use in the output. This flag may be
provided multiple times. Settings are applied iteratively. Colors are limited
to one of eight choices: red, blue, green, cyan, magenta, yellow, white and
black. Styles are limited to either nobold or bold.

The format of the flag is {type}:{attribute}:{value}. {type} should be one
of path, line or match. {attribute} can be fg, bg or style. Value is either
a color (for fg and bg) or a text style. A special format, {type}:none,
will clear all color settings for {type}.

For example, the following command will change the match color to magenta
and the background color for line numbers to yellow:

rg --colors 'match:fg:magenta' --colors 'line:bg:yellow' foo.

--column
: Show column numbers (1 based) in output. This only shows the column
numbers for the first match on each line. Note that this doesn't try
Expand Down
24 changes: 22 additions & 2 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ fn app<F>(next_line_help: bool, doc: F) -> App<'static, 'static>
.value_name("WHEN")
.takes_value(true)
.hide_possible_values(true)
.possible_values(&["never", "always", "auto"]))
.possible_values(&["never", "auto", "always", "ansi"]))
.arg(flag("colors").value_name("SPEC")
.takes_value(true).multiple(true).number_of_values(1))
.arg(flag("fixed-strings").short("F"))
.arg(flag("glob").short("g")
.takes_value(true).multiple(true).number_of_values(1)
Expand Down Expand Up @@ -220,7 +222,25 @@ lazy_static! {
doc!(h, "color",
"When to use color. [default: auto]",
"When to use color in the output. The possible values are \
never, always or auto. The default is auto.");
never, auto, always or ansi. The default is auto. When always \
is used, coloring is attempted based on your environment. When \
ansi used, coloring is forcefully done using ANSI escape color \
codes.");
doc!(h, "colors",
"Configure color settings and styles.",
"This flag specifies color settings for use in the output. \
This flag may be provided multiple times. Settings are applied \
iteratively. Colors are limited to one of eight choices: \
red, blue, green, cyan, magenta, yellow, white and black. \
Styles are limited to either nobold or bold.\n\nThe format \
of the flag is {type}:{attribute}:{value}. {type} should be \
one of path, line or match. {attribute} can be fg, bg or style. \
{value} is either a color (for fg and bg) or a text style. \
A special format, {type}:none, will clear all color settings \
for {type}.\n\nFor example, the following command will change \
the match color to magenta and the background color for line \
numbers to yellow:\n\n\
rg --colors 'match:fg:magenta' --colors 'line:bg:yellow' foo.");
doc!(h, "fixed-strings",
"Treat the pattern as a literal string.",
"Treat the pattern as a literal string instead of a regular \
Expand Down
105 changes: 62 additions & 43 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,14 @@ use grep::{Grep, GrepBuilder};
use log;
use num_cpus;
use regex;
use term::Terminal;
#[cfg(not(windows))]
use term;
#[cfg(windows)]
use term::WinConsole;
use termcolor;

use atty;
use app;
use atty;
use ignore::overrides::{Override, OverrideBuilder};
use ignore::types::{FileTypeDef, Types, TypesBuilder};
use ignore;
use out::{Out, ColoredTerminal};
use printer::Printer;
#[cfg(windows)]
use terminal_win::WindowsBuffer;
use printer::{ColorSpecs, Printer};
use unescape::unescape;
use worker::{Worker, WorkerBuilder};

Expand All @@ -40,6 +33,8 @@ pub struct Args {
after_context: usize,
before_context: usize,
color: bool,
color_choice: termcolor::ColorChoice,
colors: ColorSpecs,
column: bool,
context_separator: Vec<u8>,
count: bool,
Expand Down Expand Up @@ -132,8 +127,9 @@ impl Args {

/// Create a new printer of individual search results that writes to the
/// writer given.
pub fn printer<W: Terminal + Send>(&self, wtr: W) -> Printer<W> {
pub fn printer<W: termcolor::WriteColor>(&self, wtr: W) -> Printer<W> {
let mut p = Printer::new(wtr)
.colors(self.colors.clone())
.column(self.column)
.context_separator(self.context_separator.clone())
.eol(self.eol)
Expand All @@ -147,16 +143,6 @@ impl Args {
p
}

/// Create a new printer of search results for an entire file that writes
/// to the writer given.
pub fn out(&self) -> Out {
let mut out = Out::new(self.color);
if let Some(filesep) = self.file_separator() {
out = out.file_separator(filesep);
}
out
}

/// Retrieve the configured file separator.
pub fn file_separator(&self) -> Option<Vec<u8>> {
if self.heading && !self.count && !self.files_with_matches && !self.files_without_matches {
Expand All @@ -173,30 +159,17 @@ impl Args {
self.max_count == Some(0)
}

/// Create a new buffer for use with searching.
#[cfg(not(windows))]
pub fn outbuf(&self) -> ColoredTerminal<term::TerminfoTerminal<Vec<u8>>> {
ColoredTerminal::new(vec![], self.color)
}

/// Create a new buffer for use with searching.
#[cfg(windows)]
pub fn outbuf(&self) -> ColoredTerminal<WindowsBuffer> {
ColoredTerminal::new_buffer(self.color)
}

/// Create a new buffer for use with searching.
#[cfg(not(windows))]
pub fn stdout(
&self,
) -> ColoredTerminal<term::TerminfoTerminal<io::BufWriter<io::Stdout>>> {
ColoredTerminal::new(io::BufWriter::new(io::stdout()), self.color)
/// Create a new writer for single-threaded searching with color support.
pub fn stdout(&self) -> termcolor::Stdout {
termcolor::Stdout::new(self.color_choice)
}

/// Create a new buffer for use with searching.
#[cfg(windows)]
pub fn stdout(&self) -> ColoredTerminal<WinConsole<io::Stdout>> {
ColoredTerminal::new_stdout(self.color)
/// Create a new buffer writer for multi-threaded searching with color
/// support.
pub fn buffer_writer(&self) -> termcolor::BufferWriter {
let mut wtr = termcolor::BufferWriter::stdout(self.color_choice);
wtr.separator(self.file_separator());
wtr
}

/// Return the paths that should be searched.
Expand Down Expand Up @@ -312,6 +285,8 @@ impl<'a> ArgMatches<'a> {
after_context: after_context,
before_context: before_context,
color: self.color(),
color_choice: self.color_choice(),
colors: try!(self.color_specs()),
column: self.column(),
context_separator: self.context_separator(),
count: self.is_present("count"),
Expand Down Expand Up @@ -617,6 +592,50 @@ impl<'a> ArgMatches<'a> {
}
}

/// Returns the user's color choice based on command line parameters and
/// environment.
fn color_choice(&self) -> termcolor::ColorChoice {
let preference = match self.0.value_of_lossy("color") {
None => "auto".to_string(),
Some(v) => v.into_owned(),
};
if preference == "always" {
termcolor::ColorChoice::Always
} else if preference == "ansi" {
termcolor::ColorChoice::AlwaysAnsi
} else if self.is_present("vimgrep") {
termcolor::ColorChoice::Never
} else if preference == "auto" {
if atty::on_stdout() || self.is_present("pretty") {
termcolor::ColorChoice::Auto
} else {
termcolor::ColorChoice::Never
}
} else {
termcolor::ColorChoice::Never
}
}

/// Returns the color specifications given by the user on the CLI.
///
/// If the was a problem parsing any of the provided specs, then an error
/// is returned.
fn color_specs(&self) -> Result<ColorSpecs> {
// Start with a default set of color specs.
let mut specs = vec![
"path:fg:green".parse().unwrap(),
"path:style:bold".parse().unwrap(),
"line:fg:blue".parse().unwrap(),
"line:style:bold".parse().unwrap(),
"match:fg:red".parse().unwrap(),
"match:style:bold".parse().unwrap(),
];
for spec_str in self.values_of_lossy_vec("colors") {
specs.push(try!(spec_str.parse()));
}
Ok(ColorSpecs::new(&specs))
}

/// Returns the approximate number of threads that ripgrep should use.
fn threads(&self) -> Result<usize> {
let threads = try!(self.usize_of("threads")).unwrap_or(0);
Expand Down
Loading

0 comments on commit e8a30cb

Please sign in to comment.