Add ANSI color parsing and expose Colored enum (#451)

This commit is contained in:
Jacob O'Toole 2020-07-06 18:14:53 +01:00 committed by GitHub
parent d80afb51be
commit 9d9dfeae26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 539 additions and 27 deletions

View File

@ -117,13 +117,12 @@ use crate::Result;
use crate::{impl_display, Ansi, Command};
use std::fmt;
pub(crate) use self::enums::Colored;
pub use self::{
attributes::Attributes,
content_style::ContentStyle,
enums::{Attribute, Color},
styled_content::StyledContent,
traits::{Colorize, Styler},
types::{Attribute, Color, Colored, Colors},
};
#[macro_use]
@ -131,10 +130,10 @@ mod macros;
mod ansi;
mod attributes;
mod content_style;
mod enums;
mod styled_content;
mod sys;
mod traits;
mod types;
/// Creates a `StyledContent`.
///
@ -183,6 +182,9 @@ pub fn available_color_count() -> u16 {
///
/// See [`Color`](enum.Color.html) for more info.
///
/// [`SetColors`](struct.SetColors.html) can also be used to set both the foreground and background
/// color in one command.
///
/// # Notes
///
/// Commands must be executed/queued for execution otherwise they do nothing.
@ -213,6 +215,9 @@ impl Command for SetForegroundColor {
///
/// See [`Color`](enum.Color.html) for more info.
///
/// [`SetColors`](struct.SetColors.html) can also be used to set both the foreground and background
/// color with one command.
///
/// # Notes
///
/// Commands must be executed/queued for execution otherwise they do nothing.
@ -239,6 +244,61 @@ impl Command for SetBackgroundColor {
}
}
/// A command that optionally sets the foreground and/or background color.
///
/// For example:
/// ```no_run
/// use std::io::{stdout, Write};
/// use crossterm::execute;
/// use crossterm::style::{Color::{Green, Black}, Colors, Print, SetColors};
///
/// execute!(
/// stdout(),
/// SetColors(Colors::new(Green, Black)),
/// Print("Hello, world!".to_string()),
/// ).unwrap();
/// ```
///
/// See [`Colors`](struct.Colors.html) for more info.
///
/// # Notes
///
/// Commands must be executed/queued for execution otherwise they do nothing.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SetColors(pub Colors);
impl fmt::Display for Ansi<SetColors> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Some(color) = (self.0).0.foreground {
ansi::set_fg_csi_sequence(f, color)?;
}
if let Some(color) = (self.0).0.background {
ansi::set_bg_csi_sequence(f, color)?;
}
Ok(())
}
}
impl Command for SetColors {
type AnsiType = Ansi<Self>;
#[inline]
fn ansi_code(&self) -> Self::AnsiType {
Ansi(*self)
}
#[cfg(windows)]
fn execute_winapi(&self) -> Result<()> {
if let Some(color) = self.0.foreground {
sys::windows::set_foreground_color(color)?;
}
if let Some(color) = self.0.background {
sys::windows::set_background_color(color)?;
}
Ok(())
}
}
/// A command that sets an attribute.
///
/// See [`Attribute`](enum.Attribute.html) for more info.
@ -378,6 +438,7 @@ impl<T: Display + Clone> Display for Print<T> {
impl_display!(for SetForegroundColor);
impl_display!(for SetBackgroundColor);
impl_display!(for SetColors);
impl_display!(for SetAttribute);
impl_display!(for PrintStyledContent<String>);
impl_display!(for PrintStyledContent<&'static str>);

View File

@ -1,13 +1,13 @@
//! This is a ANSI specific implementation for styling related action.
//! This module is used for Windows 10 terminals and Unix terminals by default.
use std::fmt::{self, Formatter};
use crate::{
csi,
style::{Attribute, Attributes, Color, Colored},
};
use std::fmt::{self, Formatter};
pub(crate) fn set_fg_csi_sequence(f: &mut Formatter, fg_color: Color) -> fmt::Result {
write!(f, csi!("{}m"), Colored::ForegroundColor(fg_color))
}
@ -78,43 +78,257 @@ impl fmt::Display for Colored {
}
}
/// Utility function for ANSI parsing in Color and Colored.
/// Gets the next element of `iter` and tries to parse it as a u8.
fn parse_next_u8<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Option<u8> {
iter.next()
.and_then(|s| u8::from_str_radix(s, 10).map(Some).unwrap_or(None))
}
impl Colored {
/// Parse an ANSI foreground or background color.
/// This is the string that would appear within an `ESC [ <str> m` escape sequence, as found in
/// various configuration files.
///
/// For example: `38;5;0 -> ForegroundColor(Black)`,
/// `38;5;26 -> ForegroundColor(AnsiValue(26))`
/// `48;2;50;60;70 -> BackgroundColor(Rgb(50, 60, 70))`
/// `49 -> BackgroundColor(Reset)`
/// Invalid sequences map to None.
///
/// Currently, 3/4 bit color values aren't supported so return None.
///
/// See also: [Color::parse_ansi](enum.Color.html#method.parse_ansi)
pub fn parse_ansi(ansi: &str) -> Option<Self> {
use Colored::{BackgroundColor, ForegroundColor};
let values = &mut ansi.split(';');
let output = match parse_next_u8(values)? {
38 => return Color::parse_ansi_iter(values).map(ForegroundColor),
48 => return Color::parse_ansi_iter(values).map(BackgroundColor),
39 => ForegroundColor(Color::Reset),
49 => BackgroundColor(Color::Reset),
_ => return None,
};
if values.next().is_some() {
return None;
}
Some(output)
}
}
impl<'a> Color {
/// Parses an ANSI color sequence.
/// For example: `5;0 -> Black`, `5;26 -> AnsiValue(26)`, `2;50;60;70 -> Rgb(50, 60, 70)`.
/// Invalid sequences map to None.
///
/// Currently, 3/4 bit color values aren't supported so return None.
///
/// See also: [Colored::parse_ansi](enum.Colored.html#method.parse_ansi)
pub fn parse_ansi(ansi: &str) -> Option<Self> {
Self::parse_ansi_iter(&mut ansi.split(';'))
}
/// The logic for parse_ansi, takes an iterator of the sequences terms (the numbers between the
/// ';'). It's a separate function so it can be used by both Color::parse_ansi and
/// colored::parse_ansi.
/// Tested in Colored tests.
fn parse_ansi_iter(values: &mut impl Iterator<Item = &'a str>) -> Option<Self> {
let color = match parse_next_u8(values)? {
// 8 bit colors: `5;<n>`
5 => {
let n = parse_next_u8(values)?;
use Color::*;
[
Black, // 0
DarkRed, // 1
DarkGreen, // 2
DarkYellow, // 3
DarkBlue, // 4
DarkMagenta, // 5
DarkCyan, // 6
Grey, // 7
DarkGrey, // 8
Red, // 9
Green, // 10
Yellow, // 11
Blue, // 12
Magenta, // 13
Cyan, // 14
White, // 15
]
.get(n as usize)
.copied()
.unwrap_or(Color::AnsiValue(n))
}
// 24 bit colors: `2;<r>;<g>;<b>`
2 => Color::Rgb {
r: parse_next_u8(values)?,
g: parse_next_u8(values)?,
b: parse_next_u8(values)?,
},
_ => return None,
};
// If there's another value, it's unexpected so return None.
if values.next().is_some() {
return None;
}
Some(color)
}
}
#[cfg(test)]
mod tests {
use crate::style::{Color, Colored};
#[test]
fn test_parse_fg_color() {
fn test_format_fg_color() {
let colored = Colored::ForegroundColor(Color::Red);
assert_eq!(colored.to_string(), "38;5;9");
}
#[test]
fn test_parse_bg_color() {
fn test_format_bg_color() {
let colored = Colored::BackgroundColor(Color::Red);
assert_eq!(colored.to_string(), "48;5;9");
}
#[test]
fn test_parse_reset_fg_color() {
fn test_format_reset_fg_color() {
let colored = Colored::ForegroundColor(Color::Reset);
assert_eq!(colored.to_string(), "39");
}
#[test]
fn test_parse_reset_bg_color() {
fn test_format_reset_bg_color() {
let colored = Colored::BackgroundColor(Color::Reset);
assert_eq!(colored.to_string(), "49");
}
#[test]
fn test_parse_fg_rgb_color() {
fn test_format_fg_rgb_color() {
let colored = Colored::BackgroundColor(Color::Rgb { r: 1, g: 2, b: 3 });
assert_eq!(colored.to_string(), "48;2;1;2;3");
}
#[test]
fn test_parse_fg_ansi_color() {
fn test_format_fg_ansi_color() {
let colored = Colored::ForegroundColor(Color::AnsiValue(255));
assert_eq!(colored.to_string(), "38;5;255");
}
#[test]
fn test_parse_ansi_fg() {
test_parse_ansi(Colored::ForegroundColor)
}
#[test]
fn test_parse_ansi_bg() {
test_parse_ansi(Colored::ForegroundColor)
}
/// Used for test_parse_ansi_fg and test_parse_ansi_bg
fn test_parse_ansi(bg_or_fg: impl Fn(Color) -> Colored) {
/// Formats a re-parses `color` to check the result.
macro_rules! test {
($color:expr) => {
let colored = bg_or_fg($color);
assert_eq!(Colored::parse_ansi(&format!("{}", colored)), Some(colored));
};
}
use Color::*;
test!(Reset);
test!(Black);
test!(DarkGrey);
test!(Red);
test!(DarkRed);
test!(Green);
test!(DarkGreen);
test!(Yellow);
test!(DarkYellow);
test!(Blue);
test!(DarkBlue);
test!(Magenta);
test!(DarkMagenta);
test!(Cyan);
test!(DarkCyan);
test!(White);
test!(Grey);
// n in 0..=15 will give us the color values above back.
for n in 16..=255 {
test!(AnsiValue(n));
}
for r in 0..=255 {
for g in [0, 2, 18, 19, 60, 100, 200, 250, 254, 255].iter().copied() {
for b in [0, 12, 16, 99, 100, 161, 200, 255].iter().copied() {
test!(Rgb { r, g, b });
}
}
}
}
#[test]
fn test_parse_invalid_ansi_color() {
/// Checks that trying to parse `s` yields None.
fn test(s: &str) {
assert_eq!(Colored::parse_ansi(s), None);
}
test("");
test(";");
test(";;");
test(";;");
test("0");
test("1");
test("12");
test("100");
test("100048949345");
test("39;");
test("49;");
test("39;2");
test("49;2");
test("38");
test("38;");
test("38;0");
test("38;5");
test("38;5;0;");
test("38;5;0;2");
test("38;5;80;");
test("38;5;80;2");
test("38;5;257");
test("38;2");
test("38;2;");
test("38;2;0");
test("38;2;0;2");
test("38;2;0;2;257");
test("38;2;0;2;25;");
test("38;2;0;2;25;3");
test("48");
test("48;");
test("48;0");
test("48;5");
test("48;5;0;");
test("48;5;0;2");
test("48;5;80;");
test("48;5;80;2");
test("48;5;257");
test("48;2");
test("48;2;");
test("48;2;0");
test("48;2;0;2");
test("48;2;0;2;257");
test("48;2;0;2;25;");
test("48;2;0;2;25;3");
}
}

View File

@ -1,6 +0,0 @@
pub(crate) use self::colored::Colored;
pub use self::{attribute::Attribute, color::Color};
mod attribute;
mod color;
mod colored;

View File

@ -1,10 +0,0 @@
use crate::style::Color;
/// Represents a foreground or a background color.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub(crate) enum Colored {
/// A foreground color.
ForegroundColor(Color),
/// A background color.
BackgroundColor(Color),
}

6
src/style/types.rs Normal file
View File

@ -0,0 +1,6 @@
pub use self::{attribute::Attribute, color::Color, colored::Colored, colors::Colors};
mod attribute;
mod color;
mod colored;
mod colors;

View File

@ -0,0 +1,17 @@
use crate::style::Color;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
/// Represents a foreground or background color.
///
/// This can be converted to a [Colors](struct.Colors.html) by calling `into()` and applied
/// using the [SetColors](struct.SetColors.html) command.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub enum Colored {
/// A foreground color.
ForegroundColor(Color),
/// A background color.
BackgroundColor(Color),
}

230
src/style/types/colors.rs Normal file
View File

@ -0,0 +1,230 @@
use crate::style::{Color, Colored};
/// Represents, optionally, a foreground and/or a background color.
///
/// It can be applied using the `SetColors` command.
///
/// It can also be created from a [Colored](enum.Colored.html) value or a tuple of
/// `(Color, Color)` in the order `(foreground, background)`.
///
/// The [then](#method.then) method can be used to combine `Colors` values.
///
/// For example:
/// ```no_run
/// use crossterm::style::{Color, Colors, Colored};
///
/// // An example color, loaded from a config, file in ANSI format.
/// let config_color = "38;2;23;147;209";
///
/// // Default to green text on a black background.
/// let default_colors = Colors::new(Color::Green, Color::Black);
/// // Load a colored value from a config and override the default colors
/// let colors = match Colored::parse_ansi(config_color) {
/// Some(colored) => default_colors.then(&colored.into()),
/// None => default_colors,
/// };
/// ```
///
/// See [Color](enum.Color.html).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Colors {
pub foreground: Option<Color>,
pub background: Option<Color>,
}
impl Colors {
/// Returns a new `Color` which, when applied, has the same effect as applying `self` and *then*
/// `other`.
pub fn then(&self, other: &Colors) -> Colors {
Colors {
foreground: other.foreground.or(self.foreground),
background: other.background.or(self.background),
}
}
}
impl Colors {
pub fn new(foreground: Color, background: Color) -> Colors {
Colors {
foreground: Some(foreground),
background: Some(background),
}
}
}
impl From<Colored> for Colors {
fn from(colored: Colored) -> Colors {
match colored {
Colored::ForegroundColor(color) => Colors {
foreground: Some(color),
background: None,
},
Colored::BackgroundColor(color) => Colors {
foreground: None,
background: Some(color),
},
}
}
}
#[cfg(test)]
mod tests {
use crate::style::{Color, Colors};
#[test]
fn test_colors_then() {
use Color::*;
assert_eq!(
Colors {
foreground: None,
background: None,
}
.then(&Colors {
foreground: None,
background: None,
}),
Colors {
foreground: None,
background: None,
}
);
assert_eq!(
Colors {
foreground: None,
background: None,
}
.then(&Colors {
foreground: Some(Black),
background: None,
}),
Colors {
foreground: Some(Black),
background: None,
}
);
assert_eq!(
Colors {
foreground: None,
background: None,
}
.then(&Colors {
foreground: None,
background: Some(Grey),
}),
Colors {
foreground: None,
background: Some(Grey),
}
);
assert_eq!(
Colors {
foreground: None,
background: None,
}
.then(&Colors::new(White, Grey)),
Colors::new(White, Grey),
);
assert_eq!(
Colors {
foreground: None,
background: Some(Blue),
}
.then(&Colors::new(White, Grey)),
Colors::new(White, Grey),
);
assert_eq!(
Colors {
foreground: Some(Blue),
background: None,
}
.then(&Colors::new(White, Grey)),
Colors::new(White, Grey),
);
assert_eq!(
Colors::new(Blue, Green).then(&Colors::new(White, Grey)),
Colors::new(White, Grey),
);
assert_eq!(
Colors {
foreground: Some(Blue),
background: Some(Green),
}
.then(&Colors {
foreground: None,
background: Some(Grey),
}),
Colors {
foreground: Some(Blue),
background: Some(Grey),
}
);
assert_eq!(
Colors {
foreground: Some(Blue),
background: Some(Green),
}
.then(&Colors {
foreground: Some(White),
background: None,
}),
Colors {
foreground: Some(White),
background: Some(Green),
}
);
assert_eq!(
Colors {
foreground: Some(Blue),
background: Some(Green),
}
.then(&Colors {
foreground: None,
background: None,
}),
Colors {
foreground: Some(Blue),
background: Some(Green),
}
);
assert_eq!(
Colors {
foreground: None,
background: Some(Green),
}
.then(&Colors {
foreground: None,
background: None,
}),
Colors {
foreground: None,
background: Some(Green),
}
);
assert_eq!(
Colors {
foreground: Some(Blue),
background: None,
}
.then(&Colors {
foreground: None,
background: None,
}),
Colors {
foreground: Some(Blue),
background: None,
}
);
}
}