Skip to content

Commit

Permalink
Add extended color support
Browse files Browse the repository at this point in the history
Adds 256-color and 24-bit truecolor support to ripgrep. This only
provides output support on ansi terminals. Windows will fallback to
white for any argument given.
  • Loading branch information
tiehuis committed Apr 18, 2017
1 parent c50b8b4 commit 88cb148
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 4 deletions.
11 changes: 10 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,16 @@ lazy_static! {
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.");
rg --colors 'match:fg:magenta' --colors 'line:bg:yellow' foo.\n\n\
Extended colors can be used for {value} when the terminal \
supports ANSI color sequences. These are specified as either \
'x' (256-color) or 'x,x,x' (24-bit truecolor) where x is a \
number between 0 and 255 inclusive. \n\nFor example, the \
following command will change the match background color to that \
represented by the rgb value (0,128,255):\n\n\
rg --colors 'match:bg:0,128,255'\n\nNote that the the intense \
and nointense style flags will have no effect when used \
alongside these extended color codes.");
doc!(h, "encoding",
"Specify the text encoding of files to search.",
"Specify the text encoding that ripgrep will use on all files \
Expand Down
167 changes: 164 additions & 3 deletions termcolor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -954,6 +954,60 @@ impl<W: io::Write> Ansi<W> {
}
}
}
macro_rules! write_var_ansi_code {
($pre:expr, $($code:expr),+) => {{
// The loop generates at worst a literal of the form
// '255,255,255m' which is 12-bytes.
// The largest `pre` expression we currently use is 7 bytes.
// This gives us the maximum of 19-bytes for our work buffer.
let pre_len = $pre.len();
assert!(pre_len <= 7);
let mut fmt = [0u8; 19];
fmt[..pre_len].copy_from_slice($pre);
let mut i = pre_len - 1;
$(
let c1: u8 = ($code / 100) % 10;
let c2: u8 = ($code / 10) % 10;
let c3: u8 = $code % 10;
let mut printed = false;

if c1 != 0 {
printed = true;
i += 1;
fmt[i] = b'0' + c1;
}
if c2 != 0 || printed {
i += 1;
fmt[i] = b'0' + c2;
}
// If we received a zero value we must still print a value.
i += 1;
fmt[i] = b'0' + c3;
i += 1;
fmt[i] = b';';
)+

fmt[i] = b'm';
self.write_all(&fmt[0..i+1])
}}
}
macro_rules! write_custom {
($ansi256:expr) => {
if fg {
write_var_ansi_code!(b"\x1B[38;5;", $ansi256)
} else {
write_var_ansi_code!(b"\x1B[48;5;", $ansi256)
}
};

($r:expr, $g:expr, $b:expr) => {{
if fg {
write_var_ansi_code!(b"\x1B[38;2;", $r, $g, $b)
} else {
write_var_ansi_code!(b"\x1B[48;2;", $r, $g, $b)
}
}};
}
if intense {
match *c {
Color::Black => write_intense!("8"),
Expand All @@ -964,6 +1018,8 @@ impl<W: io::Write> Ansi<W> {
Color::Magenta => write_intense!("13"),
Color::Yellow => write_intense!("11"),
Color::White => write_intense!("15"),
Color::Ansi256(c) => write_custom!(c),
Color::Rgb(r, g, b) => write_custom!(r, g, b),
Color::__Nonexhaustive => unreachable!(),
}
} else {
Expand All @@ -976,6 +1032,8 @@ impl<W: io::Write> Ansi<W> {
Color::Magenta => write_normal!("5"),
Color::Yellow => write_normal!("3"),
Color::White => write_normal!("7"),
Color::Ansi256(c) => write_custom!(c),
Color::Rgb(r, g, b) => write_custom!(r, g, b),
Color::__Nonexhaustive => unreachable!(),
}
}
Expand Down Expand Up @@ -1170,7 +1228,13 @@ impl ColorSpec {
}
}

/// The set of available English colors for the terminal foreground/background.
/// The set of available colors for the terminal foreground/background.
///
/// The `Ansi256` and `Rgb` colors will only output the correct codes when
/// paired with the `Ansi` `WriteColor` implementation.
///
/// Usage under windows currently falls back to `White` as the default if
/// encountered.
///
/// Note that this set may expand over time.
#[allow(missing_docs)]
Expand All @@ -1184,6 +1248,8 @@ pub enum Color {
Magenta,
Yellow,
White,
Ansi256(u8),
Rgb(u8, u8, u8),
#[doc(hidden)]
__Nonexhaustive,
}
Expand All @@ -1201,6 +1267,10 @@ impl Color {
Color::Magenta => wincolor::Color::Magenta,
Color::Yellow => wincolor::Color::Yellow,
Color::White => wincolor::Color::White,
// We cannot handle these color codes currently on windows so we
// fall back to white as a default.
Color::Ansi256(_) => wincolor::Color::White,
Color::Rgb(_, _, _) => wincolor::Color::White,
Color::__Nonexhaustive => unreachable!(),
}
}
Expand All @@ -1222,7 +1292,8 @@ impl error::Error for ParseColorError {
impl fmt::Display for ParseColorError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Unrecognized color name '{}'. Choose from: \
black, blue, green, red, cyan, magenta, yellow, white.",
black, blue, green, red, cyan, magenta, yellow, white, \
'[0-255]','[0-255],[0-255],[0-255]'",
self.0)
}
}
Expand All @@ -1240,7 +1311,34 @@ impl FromStr for Color {
"magenta" => Ok(Color::Magenta),
"yellow" => Ok(Color::Yellow),
"white" => Ok(Color::White),
_ => Err(ParseColorError(s.to_string())),

_ => {
// - Ansi256: '[0-255]'
// - Rgb: '[0-255],[0-255],[0-255]'
let codes: Vec<&str> = s.split(',').collect();
if codes.len() == 1 {
let p = codes[0].parse::<u8>();
if p.is_ok() {
Ok(Color::Ansi256(p.unwrap()))
} else {
Err(ParseColorError(s.to_string()))
}
}
else if codes.len() == 3 {
let mut v = Vec::new();
for code in codes {
let r = code.parse::<u8>();
if r.is_err() {
return Err(ParseColorError(s.to_string()));
}
v.push(r.unwrap());
}
Ok(Color::Rgb(v[0], v[1], v[2]))
} else {
Err(ParseColorError(s.to_string()))
}
}

}
}
}
Expand Down Expand Up @@ -1320,3 +1418,66 @@ fn write_lossy_utf8<W: io::Write>(mut w: W, buf: &[u8]) -> io::Result<usize> {
Err(e) => w.write(&buf[..e.valid_up_to()]),
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_simple_parse_ok() {
let color = "green".parse::<Color>();
assert_eq!(color, Ok(Color::Green));
}

#[test]
fn test_256_parse_ok() {
let color = "7".parse::<Color>();
assert_eq!(color, Ok(Color::Ansi256(7)));

// In range of standard color codes
let color = "32".parse::<Color>();
assert_eq!(color, Ok(Color::Ansi256(32)));
}

#[test]
fn test_rgb_parse_ok() {
let color = "0,0,0".parse::<Color>();
assert_eq!(color, Ok(Color::Rgb(0, 0, 0)));

let color = "0,128,255".parse::<Color>();
assert_eq!(color, Ok(Color::Rgb(0, 128, 255)));
}

#[test]
fn test_rgb_parse_err_out_of_range() {
let color = "0,0,256".parse::<Color>();
assert_eq!(color, Err(ParseColorError("0,0,256".into())));
}

#[test]
fn test_rgb_parse_err_bad_format() {
let color = "0,0".parse::<Color>();
assert_eq!(color, Err(ParseColorError("0,0".into())));

let color = "not_a_color".parse::<Color>();
assert_eq!(color, Err(ParseColorError("not_a_color".into())));
}

#[test]
fn test_var_ansi_write_rgb() {
let mut buf = Ansi::new(Vec::new());
let _ = buf.write_color(true, &Color::Rgb(254, 253, 255), false);
assert_eq!(buf.0, b"\x1B[38;2;254;253;255m");
}

#[test]
fn test_var_ansi_write_256() {
let mut buf = Ansi::new(Vec::new());
let _ = buf.write_color(false, &Color::Ansi256(7), false);
assert_eq!(buf.0, b"\x1B[48;5;7m");

let mut buf = Ansi::new(Vec::new());
let _ = buf.write_color(false, &Color::Ansi256(208), false);
assert_eq!(buf.0, b"\x1B[48;5;208m");
}
}

0 comments on commit 88cb148

Please sign in to comment.