Skip to content

Commit e8a30cb

Browse files
committed
Completely re-work colored output and tty handling.
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
1 parent 03f7605 commit e8a30cb

23 files changed

+2153
-739
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@ target
44
/grep/Cargo.lock
55
/globset/Cargo.lock
66
/ignore/Cargo.lock
7+
/termcolor/Cargo.lock
8+
/wincolor/Cargo.lock

Cargo.lock

+20-15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3-3
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ memchr = "0.1"
3838
memmap = "0.5"
3939
num_cpus = "1"
4040
regex = "0.1.77"
41-
term = "0.4"
41+
termcolor = { version = "0.1.0", path = "termcolor" }
4242

4343
[target.'cfg(windows)'.dependencies]
44-
kernel32-sys = "0.2"
45-
winapi = "0.2"
44+
kernel32-sys = "0.2.2"
45+
winapi = "0.2.8"
4646

4747
[build-dependencies]
4848
clap = "2.18"

appveyor.yml

+2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ test_script:
3131
- cargo test --verbose --manifest-path grep/Cargo.toml
3232
- cargo test --verbose --manifest-path globset/Cargo.toml
3333
- cargo test --verbose --manifest-path ignore/Cargo.toml
34+
- cargo test --verbose --manifest-path wincolor/Cargo.toml
35+
- cargo test --verbose --manifest-path termcolor/Cargo.toml
3436

3537
before_deploy:
3638
# Generate artifacts for release

ci/script.sh

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ run_test_suite() {
2525
cargo test --target $TARGET --verbose --manifest-path globset/Cargo.toml
2626
cargo build --target $TARGET --verbose --manifest-path ignore/Cargo.toml
2727
cargo test --target $TARGET --verbose --manifest-path ignore/Cargo.toml
28+
cargo build --target $TARGET --verbose --manifest-path termcolor/Cargo.toml
29+
cargo test --target $TARGET --verbose --manifest-path termcolor/Cargo.toml
2830

2931
# sanity check the file type
3032
file target/$TARGET/debug/rg

doc/rg.1

+22
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,28 @@ Show NUM lines before and after each match.
143143
.RS
144144
.RE
145145
.TP
146+
.B \-\-colors \f[I]SPEC\f[] ...
147+
This flag specifies color settings for use in the output.
148+
This flag may be provided multiple times.
149+
Settings are applied iteratively.
150+
Colors are limited to one of eight choices: red, blue, green, cyan,
151+
magenta, yellow, white and black.
152+
Styles are limited to either nobold or bold.
153+
.RS
154+
.PP
155+
The format of the flag is {type}:{attribute}:{value}.
156+
{type} should be one of path, line or match.
157+
{attribute} can be fg, bg or style.
158+
Value is either a color (for fg and bg) or a text style.
159+
A special format, {type}:none, will clear all color settings for {type}.
160+
.PP
161+
For example, the following command will change the match color to
162+
magenta and the background color for line numbers to yellow:
163+
.PP
164+
rg \-\-colors \[aq]match:fg:magenta\[aq] \-\-colors
165+
\[aq]line:bg:yellow\[aq] foo.
166+
.RE
167+
.TP
146168
.B \-\-column
147169
Show column numbers (1 based) in output.
148170
This only shows the column numbers for the first match on each line.

doc/rg.1.md

+16
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,22 @@ Project home page: https://github.com/BurntSushi/ripgrep
9595
-C, --context *NUM*
9696
: Show NUM lines before and after each match.
9797

98+
--colors *SPEC* ...
99+
: This flag specifies color settings for use in the output. This flag may be
100+
provided multiple times. Settings are applied iteratively. Colors are limited
101+
to one of eight choices: red, blue, green, cyan, magenta, yellow, white and
102+
black. Styles are limited to either nobold or bold.
103+
104+
The format of the flag is {type}:{attribute}:{value}. {type} should be one
105+
of path, line or match. {attribute} can be fg, bg or style. Value is either
106+
a color (for fg and bg) or a text style. A special format, {type}:none,
107+
will clear all color settings for {type}.
108+
109+
For example, the following command will change the match color to magenta
110+
and the background color for line numbers to yellow:
111+
112+
rg --colors 'match:fg:magenta' --colors 'line:bg:yellow' foo.
113+
98114
--column
99115
: Show column numbers (1 based) in output. This only shows the column
100116
numbers for the first match on each line. Note that this doesn't try

src/app.rs

+22-2
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@ fn app<F>(next_line_help: bool, doc: F) -> App<'static, 'static>
8888
.value_name("WHEN")
8989
.takes_value(true)
9090
.hide_possible_values(true)
91-
.possible_values(&["never", "always", "auto"]))
91+
.possible_values(&["never", "auto", "always", "ansi"]))
92+
.arg(flag("colors").value_name("SPEC")
93+
.takes_value(true).multiple(true).number_of_values(1))
9294
.arg(flag("fixed-strings").short("F"))
9395
.arg(flag("glob").short("g")
9496
.takes_value(true).multiple(true).number_of_values(1)
@@ -220,7 +222,25 @@ lazy_static! {
220222
doc!(h, "color",
221223
"When to use color. [default: auto]",
222224
"When to use color in the output. The possible values are \
223-
never, always or auto. The default is auto.");
225+
never, auto, always or ansi. The default is auto. When always \
226+
is used, coloring is attempted based on your environment. When \
227+
ansi used, coloring is forcefully done using ANSI escape color \
228+
codes.");
229+
doc!(h, "colors",
230+
"Configure color settings and styles.",
231+
"This flag specifies color settings for use in the output. \
232+
This flag may be provided multiple times. Settings are applied \
233+
iteratively. Colors are limited to one of eight choices: \
234+
red, blue, green, cyan, magenta, yellow, white and black. \
235+
Styles are limited to either nobold or bold.\n\nThe format \
236+
of the flag is {type}:{attribute}:{value}. {type} should be \
237+
one of path, line or match. {attribute} can be fg, bg or style. \
238+
{value} is either a color (for fg and bg) or a text style. \
239+
A special format, {type}:none, will clear all color settings \
240+
for {type}.\n\nFor example, the following command will change \
241+
the match color to magenta and the background color for line \
242+
numbers to yellow:\n\n\
243+
rg --colors 'match:fg:magenta' --colors 'line:bg:yellow' foo.");
224244
doc!(h, "fixed-strings",
225245
"Treat the pattern as a literal string.",
226246
"Treat the pattern as a literal string instead of a regular \

src/args.rs

+62-43
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,14 @@ use grep::{Grep, GrepBuilder};
1313
use log;
1414
use num_cpus;
1515
use regex;
16-
use term::Terminal;
17-
#[cfg(not(windows))]
18-
use term;
19-
#[cfg(windows)]
20-
use term::WinConsole;
16+
use termcolor;
2117

22-
use atty;
2318
use app;
19+
use atty;
2420
use ignore::overrides::{Override, OverrideBuilder};
2521
use ignore::types::{FileTypeDef, Types, TypesBuilder};
2622
use ignore;
27-
use out::{Out, ColoredTerminal};
28-
use printer::Printer;
29-
#[cfg(windows)]
30-
use terminal_win::WindowsBuffer;
23+
use printer::{ColorSpecs, Printer};
3124
use unescape::unescape;
3225
use worker::{Worker, WorkerBuilder};
3326

@@ -40,6 +33,8 @@ pub struct Args {
4033
after_context: usize,
4134
before_context: usize,
4235
color: bool,
36+
color_choice: termcolor::ColorChoice,
37+
colors: ColorSpecs,
4338
column: bool,
4439
context_separator: Vec<u8>,
4540
count: bool,
@@ -132,8 +127,9 @@ impl Args {
132127

133128
/// Create a new printer of individual search results that writes to the
134129
/// writer given.
135-
pub fn printer<W: Terminal + Send>(&self, wtr: W) -> Printer<W> {
130+
pub fn printer<W: termcolor::WriteColor>(&self, wtr: W) -> Printer<W> {
136131
let mut p = Printer::new(wtr)
132+
.colors(self.colors.clone())
137133
.column(self.column)
138134
.context_separator(self.context_separator.clone())
139135
.eol(self.eol)
@@ -147,16 +143,6 @@ impl Args {
147143
p
148144
}
149145

150-
/// Create a new printer of search results for an entire file that writes
151-
/// to the writer given.
152-
pub fn out(&self) -> Out {
153-
let mut out = Out::new(self.color);
154-
if let Some(filesep) = self.file_separator() {
155-
out = out.file_separator(filesep);
156-
}
157-
out
158-
}
159-
160146
/// Retrieve the configured file separator.
161147
pub fn file_separator(&self) -> Option<Vec<u8>> {
162148
if self.heading && !self.count && !self.files_with_matches && !self.files_without_matches {
@@ -173,30 +159,17 @@ impl Args {
173159
self.max_count == Some(0)
174160
}
175161

176-
/// Create a new buffer for use with searching.
177-
#[cfg(not(windows))]
178-
pub fn outbuf(&self) -> ColoredTerminal<term::TerminfoTerminal<Vec<u8>>> {
179-
ColoredTerminal::new(vec![], self.color)
180-
}
181-
182-
/// Create a new buffer for use with searching.
183-
#[cfg(windows)]
184-
pub fn outbuf(&self) -> ColoredTerminal<WindowsBuffer> {
185-
ColoredTerminal::new_buffer(self.color)
186-
}
187-
188-
/// Create a new buffer for use with searching.
189-
#[cfg(not(windows))]
190-
pub fn stdout(
191-
&self,
192-
) -> ColoredTerminal<term::TerminfoTerminal<io::BufWriter<io::Stdout>>> {
193-
ColoredTerminal::new(io::BufWriter::new(io::stdout()), self.color)
162+
/// Create a new writer for single-threaded searching with color support.
163+
pub fn stdout(&self) -> termcolor::Stdout {
164+
termcolor::Stdout::new(self.color_choice)
194165
}
195166

196-
/// Create a new buffer for use with searching.
197-
#[cfg(windows)]
198-
pub fn stdout(&self) -> ColoredTerminal<WinConsole<io::Stdout>> {
199-
ColoredTerminal::new_stdout(self.color)
167+
/// Create a new buffer writer for multi-threaded searching with color
168+
/// support.
169+
pub fn buffer_writer(&self) -> termcolor::BufferWriter {
170+
let mut wtr = termcolor::BufferWriter::stdout(self.color_choice);
171+
wtr.separator(self.file_separator());
172+
wtr
200173
}
201174

202175
/// Return the paths that should be searched.
@@ -312,6 +285,8 @@ impl<'a> ArgMatches<'a> {
312285
after_context: after_context,
313286
before_context: before_context,
314287
color: self.color(),
288+
color_choice: self.color_choice(),
289+
colors: try!(self.color_specs()),
315290
column: self.column(),
316291
context_separator: self.context_separator(),
317292
count: self.is_present("count"),
@@ -617,6 +592,50 @@ impl<'a> ArgMatches<'a> {
617592
}
618593
}
619594

595+
/// Returns the user's color choice based on command line parameters and
596+
/// environment.
597+
fn color_choice(&self) -> termcolor::ColorChoice {
598+
let preference = match self.0.value_of_lossy("color") {
599+
None => "auto".to_string(),
600+
Some(v) => v.into_owned(),
601+
};
602+
if preference == "always" {
603+
termcolor::ColorChoice::Always
604+
} else if preference == "ansi" {
605+
termcolor::ColorChoice::AlwaysAnsi
606+
} else if self.is_present("vimgrep") {
607+
termcolor::ColorChoice::Never
608+
} else if preference == "auto" {
609+
if atty::on_stdout() || self.is_present("pretty") {
610+
termcolor::ColorChoice::Auto
611+
} else {
612+
termcolor::ColorChoice::Never
613+
}
614+
} else {
615+
termcolor::ColorChoice::Never
616+
}
617+
}
618+
619+
/// Returns the color specifications given by the user on the CLI.
620+
///
621+
/// If the was a problem parsing any of the provided specs, then an error
622+
/// is returned.
623+
fn color_specs(&self) -> Result<ColorSpecs> {
624+
// Start with a default set of color specs.
625+
let mut specs = vec![
626+
"path:fg:green".parse().unwrap(),
627+
"path:style:bold".parse().unwrap(),
628+
"line:fg:blue".parse().unwrap(),
629+
"line:style:bold".parse().unwrap(),
630+
"match:fg:red".parse().unwrap(),
631+
"match:style:bold".parse().unwrap(),
632+
];
633+
for spec_str in self.values_of_lossy_vec("colors") {
634+
specs.push(try!(spec_str.parse()));
635+
}
636+
Ok(ColorSpecs::new(&specs))
637+
}
638+
620639
/// Returns the approximate number of threads that ripgrep should use.
621640
fn threads(&self) -> Result<usize> {
622641
let threads = try!(self.usize_of("threads")).unwrap_or(0);

0 commit comments

Comments
 (0)