diff --git a/src/style.rs b/src/style.rs index a704f24..9fa90e6 100644 --- a/src/style.rs +++ b/src/style.rs @@ -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 { + 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; + + #[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 Display for Print { impl_display!(for SetForegroundColor); impl_display!(for SetBackgroundColor); +impl_display!(for SetColors); impl_display!(for SetAttribute); impl_display!(for PrintStyledContent); impl_display!(for PrintStyledContent<&'static str>); diff --git a/src/style/ansi.rs b/src/style/ansi.rs index 03f2b00..7b789fb 100644 --- a/src/style/ansi.rs +++ b/src/style/ansi.rs @@ -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) -> Option { + 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 [ 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 { + 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::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) -> Option { + let color = match parse_next_u8(values)? { + // 8 bit colors: `5;` + 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;;;` + 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"); + } } diff --git a/src/style/enums.rs b/src/style/enums.rs deleted file mode 100644 index b7df3d4..0000000 --- a/src/style/enums.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub(crate) use self::colored::Colored; -pub use self::{attribute::Attribute, color::Color}; - -mod attribute; -mod color; -mod colored; diff --git a/src/style/enums/colored.rs b/src/style/enums/colored.rs deleted file mode 100644 index 38b51cb..0000000 --- a/src/style/enums/colored.rs +++ /dev/null @@ -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), -} diff --git a/src/style/types.rs b/src/style/types.rs new file mode 100644 index 0000000..7cd7d6e --- /dev/null +++ b/src/style/types.rs @@ -0,0 +1,6 @@ +pub use self::{attribute::Attribute, color::Color, colored::Colored, colors::Colors}; + +mod attribute; +mod color; +mod colored; +mod colors; diff --git a/src/style/enums/attribute.rs b/src/style/types/attribute.rs similarity index 100% rename from src/style/enums/attribute.rs rename to src/style/types/attribute.rs diff --git a/src/style/enums/color.rs b/src/style/types/color.rs similarity index 100% rename from src/style/enums/color.rs rename to src/style/types/color.rs diff --git a/src/style/types/colored.rs b/src/style/types/colored.rs new file mode 100644 index 0000000..1ee8a2d --- /dev/null +++ b/src/style/types/colored.rs @@ -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), +} diff --git a/src/style/types/colors.rs b/src/style/types/colors.rs new file mode 100644 index 0000000..44e4233 --- /dev/null +++ b/src/style/types/colors.rs @@ -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, + pub background: Option, +} + +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 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, + } + ); + } +}