mirror of
https://github.com/BurntSushi/ripgrep.git
synced 2025-05-19 09:40:22 -07:00
termcolor: add extended color support
This commit adds 256-color and 24-bit truecolor support to ripgrep. This only provides output support on ANSI terminals. If the Windows console is used for coloring, then 256-color and 24-bit color settings are ignored.
This commit is contained in:
parent
ef9e17d28a
commit
a8543f798d
11
src/app.rs
11
src/app.rs
@ -288,7 +288,16 @@ lazy_static! {
|
|||||||
color settings for {type}.\n\nFor example, the following \
|
color settings for {type}.\n\nFor example, the following \
|
||||||
command will change the match color to magenta and the \
|
command will change the match color to magenta and the \
|
||||||
background color for line numbers to yellow:\n\n\
|
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",
|
doc!(h, "encoding",
|
||||||
"Specify the text encoding of files to search.",
|
"Specify the text encoding of files to search.",
|
||||||
"Specify the text encoding that ripgrep will use on all files \
|
"Specify the text encoding that ripgrep will use on all files \
|
||||||
|
@ -1006,6 +1006,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 {
|
if intense {
|
||||||
match *c {
|
match *c {
|
||||||
Color::Black => write_intense!("8"),
|
Color::Black => write_intense!("8"),
|
||||||
@ -1016,6 +1070,8 @@ impl<W: io::Write> Ansi<W> {
|
|||||||
Color::Magenta => write_intense!("13"),
|
Color::Magenta => write_intense!("13"),
|
||||||
Color::Yellow => write_intense!("11"),
|
Color::Yellow => write_intense!("11"),
|
||||||
Color::White => write_intense!("15"),
|
Color::White => write_intense!("15"),
|
||||||
|
Color::Ansi256(c) => write_custom!(c),
|
||||||
|
Color::Rgb(r, g, b) => write_custom!(r, g, b),
|
||||||
Color::__Nonexhaustive => unreachable!(),
|
Color::__Nonexhaustive => unreachable!(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -1028,6 +1084,8 @@ impl<W: io::Write> Ansi<W> {
|
|||||||
Color::Magenta => write_normal!("5"),
|
Color::Magenta => write_normal!("5"),
|
||||||
Color::Yellow => write_normal!("3"),
|
Color::Yellow => write_normal!("3"),
|
||||||
Color::White => write_normal!("7"),
|
Color::White => write_normal!("7"),
|
||||||
|
Color::Ansi256(c) => write_custom!(c),
|
||||||
|
Color::Rgb(r, g, b) => write_custom!(r, g, b),
|
||||||
Color::__Nonexhaustive => unreachable!(),
|
Color::__Nonexhaustive => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1212,17 +1270,28 @@ impl ColorSpec {
|
|||||||
use wincolor::Intense;
|
use wincolor::Intense;
|
||||||
|
|
||||||
let intense = if self.intense { Intense::Yes } else { Intense::No };
|
let intense = if self.intense { Intense::Yes } else { Intense::No };
|
||||||
if let Some(color) = self.fg_color.as_ref().map(|c| c.to_windows()) {
|
|
||||||
|
let fg_color = self.fg_color.as_ref().and_then(|c| c.to_windows());
|
||||||
|
if let Some(color) = fg_color {
|
||||||
console.fg(intense, color)?;
|
console.fg(intense, color)?;
|
||||||
}
|
}
|
||||||
if let Some(color) = self.bg_color.as_ref().map(|c| c.to_windows()) {
|
|
||||||
|
let bg_color = self.bg_color.as_ref().and_then(|c| c.to_windows());
|
||||||
|
if let Some(color) = bg_color {
|
||||||
console.bg(intense, color)?;
|
console.bg(intense, color)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
|
///
|
||||||
|
/// The `Ansi256` and `Rgb` color types are not supported when writing colors
|
||||||
|
/// on Windows using the console. If they are used on Windows, then they are
|
||||||
|
/// silently ignored and no colors will be emitted.
|
||||||
///
|
///
|
||||||
/// Note that this set may expand over time.
|
/// Note that this set may expand over time.
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
@ -1236,6 +1305,8 @@ pub enum Color {
|
|||||||
Magenta,
|
Magenta,
|
||||||
Yellow,
|
Yellow,
|
||||||
White,
|
White,
|
||||||
|
Ansi256(u8),
|
||||||
|
Rgb(u8, u8, u8),
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
__Nonexhaustive,
|
__Nonexhaustive,
|
||||||
}
|
}
|
||||||
@ -1243,39 +1314,74 @@ pub enum Color {
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
impl Color {
|
impl Color {
|
||||||
/// Translate this color to a wincolor::Color.
|
/// Translate this color to a wincolor::Color.
|
||||||
fn to_windows(&self) -> wincolor::Color {
|
fn to_windows(&self) -> Option<wincolor::Color> {
|
||||||
match *self {
|
match *self {
|
||||||
Color::Black => wincolor::Color::Black,
|
Color::Black => Some(wincolor::Color::Black),
|
||||||
Color::Blue => wincolor::Color::Blue,
|
Color::Blue => Some(wincolor::Color::Blue),
|
||||||
Color::Green => wincolor::Color::Green,
|
Color::Green => Some(wincolor::Color::Green),
|
||||||
Color::Red => wincolor::Color::Red,
|
Color::Red => Some(wincolor::Color::Red),
|
||||||
Color::Cyan => wincolor::Color::Cyan,
|
Color::Cyan => Some(wincolor::Color::Cyan),
|
||||||
Color::Magenta => wincolor::Color::Magenta,
|
Color::Magenta => Some(wincolor::Color::Magenta),
|
||||||
Color::Yellow => wincolor::Color::Yellow,
|
Color::Yellow => Some(wincolor::Color::Yellow),
|
||||||
Color::White => wincolor::Color::White,
|
Color::White => Some(wincolor::Color::White),
|
||||||
|
Color::Ansi256(_) => None,
|
||||||
|
Color::Rgb(_, _, _) => None,
|
||||||
Color::__Nonexhaustive => unreachable!(),
|
Color::__Nonexhaustive => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error from parsing an invalid color name.
|
/// An error from parsing an invalid color specification.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct ParseColorError(String);
|
pub struct ParseColorError {
|
||||||
|
kind: ParseColorErrorKind,
|
||||||
|
given: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
enum ParseColorErrorKind {
|
||||||
|
InvalidName,
|
||||||
|
InvalidAnsi256,
|
||||||
|
InvalidRgb,
|
||||||
|
}
|
||||||
|
|
||||||
impl ParseColorError {
|
impl ParseColorError {
|
||||||
/// Return the string that couldn't be parsed as a valid color.
|
/// Return the string that couldn't be parsed as a valid color.
|
||||||
pub fn invalid(&self) -> &str { &self.0 }
|
pub fn invalid(&self) -> &str { &self.given }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl error::Error for ParseColorError {
|
impl error::Error for ParseColorError {
|
||||||
fn description(&self) -> &str { "unrecognized color name" }
|
fn description(&self) -> &str {
|
||||||
|
use self::ParseColorErrorKind::*;
|
||||||
|
match self.kind {
|
||||||
|
InvalidName => "unrecognized color name",
|
||||||
|
InvalidAnsi256 => "invalid ansi256 color number",
|
||||||
|
InvalidRgb => "invalid RGB color triple",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ParseColorError {
|
impl fmt::Display for ParseColorError {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "Unrecognized color name '{}'. Choose from: \
|
use self::ParseColorErrorKind::*;
|
||||||
black, blue, green, red, cyan, magenta, yellow, white.",
|
match self.kind {
|
||||||
self.0)
|
InvalidName => {
|
||||||
|
write!(f, "unrecognized color name '{}'. Choose from: \
|
||||||
|
black, blue, green, red, cyan, magenta, yellow, \
|
||||||
|
white",
|
||||||
|
self.given)
|
||||||
|
}
|
||||||
|
InvalidAnsi256 => {
|
||||||
|
write!(f, "unrecognized ansi256 color number, \
|
||||||
|
should be '[0-255]', but is '{}'",
|
||||||
|
self.given)
|
||||||
|
}
|
||||||
|
InvalidRgb => {
|
||||||
|
write!(f, "unrecognized RGB color triple, \
|
||||||
|
should be '[0-255],[0-255],[0-255]', but is '{}'",
|
||||||
|
self.given)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1292,7 +1398,52 @@ impl FromStr for Color {
|
|||||||
"magenta" => Ok(Color::Magenta),
|
"magenta" => Ok(Color::Magenta),
|
||||||
"yellow" => Ok(Color::Yellow),
|
"yellow" => Ok(Color::Yellow),
|
||||||
"white" => Ok(Color::White),
|
"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 {
|
||||||
|
if let Ok(n) = codes[0].parse::<u8>() {
|
||||||
|
Ok(Color::Ansi256(n))
|
||||||
|
} else {
|
||||||
|
if s.chars().all(|c| c.is_digit(10)) {
|
||||||
|
Err(ParseColorError {
|
||||||
|
kind: ParseColorErrorKind::InvalidAnsi256,
|
||||||
|
given: s.to_string(),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
Err(ParseColorError {
|
||||||
|
kind: ParseColorErrorKind::InvalidName,
|
||||||
|
given: s.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if codes.len() == 3 {
|
||||||
|
let mut v = vec![];
|
||||||
|
for code in codes {
|
||||||
|
let n = code.parse::<u8>().map_err(|_| {
|
||||||
|
ParseColorError {
|
||||||
|
kind: ParseColorErrorKind::InvalidRgb,
|
||||||
|
given: s.to_string(),
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
v.push(n);
|
||||||
|
}
|
||||||
|
Ok(Color::Rgb(v[0], v[1], v[2]))
|
||||||
|
} else {
|
||||||
|
Err(if s.contains(",") {
|
||||||
|
ParseColorError {
|
||||||
|
kind: ParseColorErrorKind::InvalidRgb,
|
||||||
|
given: s.to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ParseColorError {
|
||||||
|
kind: ParseColorErrorKind::InvalidName,
|
||||||
|
given: s.to_string(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1377,7 +1528,9 @@ fn write_lossy_utf8<W: io::Write>(mut w: W, buf: &[u8]) -> io::Result<usize> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::StandardStream;
|
use super::{
|
||||||
|
Ansi, Color, ParseColorError, ParseColorErrorKind, StandardStream,
|
||||||
|
};
|
||||||
|
|
||||||
fn assert_is_send<T: Send>() {}
|
fn assert_is_send<T: Send>() {}
|
||||||
|
|
||||||
@ -1385,4 +1538,80 @@ mod tests {
|
|||||||
fn standard_stream_is_send() {
|
fn standard_stream_is_send() {
|
||||||
assert_is_send::<StandardStream>();
|
assert_is_send::<StandardStream>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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_256_parse_err_out_of_range() {
|
||||||
|
let color = "256".parse::<Color>();
|
||||||
|
assert_eq!(color, Err(ParseColorError {
|
||||||
|
kind: ParseColorErrorKind::InvalidAnsi256,
|
||||||
|
given: "256".to_string(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
kind: ParseColorErrorKind::InvalidRgb,
|
||||||
|
given: "0,0,256".to_string(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rgb_parse_err_bad_format() {
|
||||||
|
let color = "0,0".parse::<Color>();
|
||||||
|
assert_eq!(color, Err(ParseColorError {
|
||||||
|
kind: ParseColorErrorKind::InvalidRgb,
|
||||||
|
given: "0,0".to_string(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
let color = "not_a_color".parse::<Color>();
|
||||||
|
assert_eq!(color, Err(ParseColorError {
|
||||||
|
kind: ParseColorErrorKind::InvalidName,
|
||||||
|
given: "not_a_color".to_string(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_var_ansi_write_rgb() {
|
||||||
|
let mut buf = Ansi::new(vec![]);
|
||||||
|
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![]);
|
||||||
|
let _ = buf.write_color(false, &Color::Ansi256(7), false);
|
||||||
|
assert_eq!(buf.0, b"\x1B[48;5;7m");
|
||||||
|
|
||||||
|
let mut buf = Ansi::new(vec![]);
|
||||||
|
let _ = buf.write_color(false, &Color::Ansi256(208), false);
|
||||||
|
assert_eq!(buf.0, b"\x1B[48;5;208m");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user