termcolor: permit hex colors

This commit adds support for specifying Ansi256 or RGB colors using
hexadecimal notation.
This commit is contained in:
Andrew Gallant 2018-02-10 13:02:48 -05:00
parent 09c5b2c4ea
commit 7e5589f07d
2 changed files with 94 additions and 53 deletions

View File

@ -696,13 +696,18 @@ the background color for line numbers to yellow:
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.
truecolor) where x is a number between 0 and 255 inclusive. x may be given as
a normal decimal number or a hexadecimal number, which is prefixed by `0x`.
For example, the following command will change the match background color to
that represented by the rgb value (0,128,255):
rg --colors 'match:bg:0,128,255'
or, equivalently,
rg --colors 'match:bg:0x0,0x80,0xFF'
Note that the the intense and nointense style flags will have no effect when
used alongside these extended color codes.
");

View File

@ -1293,7 +1293,18 @@ impl ColorSpec {
/// 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.
/// This set may expand over time.
///
/// This type has a `FromStr` impl that can parse colors from their human
/// readable form. The format is as follows:
///
/// 1. Any of the explicitly listed colors in English. They are matched
/// case insensitively.
/// 2. A single 8-bit integer, in either decimal or hexadecimal format.
/// 3. A triple of 8-bit integers separated by a comma, where each integer is
/// in decimal or hexadecimal format.
///
/// Hexadecimal numbers are written with a `0x` prefix.
#[allow(missing_docs)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Color {
@ -1311,9 +1322,9 @@ pub enum Color {
__Nonexhaustive,
}
#[cfg(windows)]
impl Color {
/// Translate this color to a wincolor::Color.
#[cfg(windows)]
fn to_windows(&self) -> Option<wincolor::Color> {
match *self {
Color::Black => Some(wincolor::Color::Black),
@ -1329,6 +1340,68 @@ impl Color {
Color::__Nonexhaustive => unreachable!(),
}
}
/// Parses a numeric color string, either ANSI or RGB.
fn from_str_numeric(s: &str) -> Result<Color, ParseColorError> {
// The "ansi256" format is a single number (decimal or hex)
// corresponding to one of 256 colors.
//
// The "rgb" format is a triple of numbers (decimal or hex) delimited
// by a comma corresponding to one of 256^3 colors.
fn parse_number(s: &str) -> Option<u8> {
use std::u8;
if s.starts_with("0x") {
u8::from_str_radix(&s[2..], 16).ok()
} else {
u8::from_str_radix(s, 10).ok()
}
}
let codes: Vec<&str> = s.split(',').collect();
if codes.len() == 1 {
if let Some(n) = parse_number(&codes[0]) {
Ok(Color::Ansi256(n))
} else {
if s.chars().all(|c| c.is_digit(16)) {
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 = parse_number(code).ok_or_else(|| {
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(),
}
})
}
}
}
/// An error from parsing an invalid color specification.
@ -1373,13 +1446,13 @@ impl fmt::Display for ParseColorError {
}
InvalidAnsi256 => {
write!(f, "unrecognized ansi256 color number, \
should be '[0-255]', but is '{}'",
should be '[0-255]' (or a hex number), but is '{}'",
self.given)
}
InvalidRgb => {
write!(f, "unrecognized RGB color triple, \
should be '[0-255],[0-255],[0-255]', but is '{}'",
self.given)
should be '[0-255],[0-255],[0-255]' (or a hex \
triple), but is '{}'", self.given)
}
}
}
@ -1398,52 +1471,7 @@ impl FromStr for Color {
"magenta" => Ok(Color::Magenta),
"yellow" => Ok(Color::Yellow),
"white" => Ok(Color::White),
_ => {
// - 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(),
}
})
}
}
_ => Color::from_str_numeric(s),
}
}
}
@ -1550,9 +1578,11 @@ mod tests {
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)));
let color = "0xFF".parse::<Color>();
assert_eq!(color, Ok(Color::Ansi256(0xFF)));
}
#[test]
@ -1571,6 +1601,12 @@ mod tests {
let color = "0,128,255".parse::<Color>();
assert_eq!(color, Ok(Color::Rgb(0, 128, 255)));
let color = "0x0,0x0,0x0".parse::<Color>();
assert_eq!(color, Ok(Color::Rgb(0, 0, 0)));
let color = "0x33,0x66,0xFF".parse::<Color>();
assert_eq!(color, Ok(Color::Rgb(0x33, 0x66, 0xFF)));
}
#[test]