From f58aca9354e8cb3a59f9d9316fbd922684c1f3d7 Mon Sep 17 00:00:00 2001 From: Canop Date: Fri, 7 Feb 2020 14:06:41 +0100 Subject: [PATCH] Changed vec of Attributes to bitfield (#380) --- examples/interactive-demo/src/main.rs | 2 +- .../interactive-demo/src/test/attribute.rs | 2 +- src/style.rs | 26 +++ src/style/ansi.rs | 14 +- src/style/attributes.rs | 119 +++++++++++++ src/style/content_style.rs | 12 +- src/style/enums/attribute.rs | 159 ++++++++++++------ src/style/macros.rs | 2 +- src/style/styled_content.rs | 13 +- 9 files changed, 275 insertions(+), 74 deletions(-) create mode 100644 src/style/attributes.rs diff --git a/examples/interactive-demo/src/main.rs b/examples/interactive-demo/src/main.rs index 662b6363..56c1a394 100644 --- a/examples/interactive-demo/src/main.rs +++ b/examples/interactive-demo/src/main.rs @@ -21,7 +21,7 @@ Controls: - 'q' - quit interactive test (or return to this menu) - any other key - continue with next step -Available tests: +Available tests: 1. cursor 2. color (foreground, background) diff --git a/examples/interactive-demo/src/test/attribute.rs b/examples/interactive-demo/src/test/attribute.rs index b0169625..c4d4332c 100644 --- a/examples/interactive-demo/src/test/attribute.rs +++ b/examples/interactive-demo/src/test/attribute.rs @@ -5,7 +5,7 @@ use crossterm::{cursor, queue, style}; use std::io::Write; const ATTRIBUTES: [(style::Attribute, style::Attribute); 6] = [ - (style::Attribute::Bold, style::Attribute::NoBold), + (style::Attribute::Bold, style::Attribute::NormalIntensity), (style::Attribute::Italic, style::Attribute::NoItalic), (style::Attribute::Underlined, style::Attribute::NoUnderline), (style::Attribute::Reverse, style::Attribute::NoReverse), diff --git a/src/style.rs b/src/style.rs index 5db0c504..bc9c752a 100644 --- a/src/style.rs +++ b/src/style.rs @@ -118,6 +118,7 @@ use crate::{impl_display, Command}; pub(crate) use self::enums::Colored; pub use self::{ + attributes::Attributes, content_style::ContentStyle, enums::{Attribute, Color}, styled_content::StyledContent, @@ -127,6 +128,7 @@ pub use self::{ #[macro_use] mod macros; mod ansi; +mod attributes; mod content_style; mod enums; mod styled_content; @@ -290,6 +292,30 @@ impl Command for SetAttribute { } } +/// A command that sets several attributes. +/// +/// See [`Attributes`](struct.Attributes.html) for more info. +/// +/// # Notes +/// +/// Commands must be executed/queued for execution otherwise they do nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SetAttributes(pub Attributes); + +impl Command for SetAttributes { + type AnsiType = String; + + fn ansi_code(&self) -> Self::AnsiType { + ansi::set_attrs_csi_sequence(self.0) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> Result<()> { + // attributes are not supported by WinAPI. + Ok(()) + } +} + /// A command that prints styled content. /// /// See [`StyledContent`](struct.StyledContent.html) for more info. diff --git a/src/style/ansi.rs b/src/style/ansi.rs index ce8eff72..cd03f79c 100644 --- a/src/style/ansi.rs +++ b/src/style/ansi.rs @@ -3,7 +3,7 @@ use crate::{ csi, - style::{Attribute, Color, Colored}, + style::{Attribute, Attributes, Color, Colored}, }; pub(crate) fn set_fg_csi_sequence(fg_color: Color) -> String { @@ -21,7 +21,17 @@ pub(crate) fn set_bg_csi_sequence(bg_color: Color) -> String { } pub(crate) fn set_attr_csi_sequence(attribute: Attribute) -> String { - format!(csi!("{}m"), attribute as i16) + format!(csi!("{}m"), attribute.sgr()) +} + +pub(crate) fn set_attrs_csi_sequence(attributes: Attributes) -> String { + let mut ansi = String::new(); + for attr in Attribute::iterator() { + if attributes.has(attr) { + ansi.push_str(&format!(csi!("{}m"), attr.sgr())); + } + } + ansi } pub(crate) const RESET_CSI_SEQUENCE: &str = csi!("0m"); diff --git a/src/style/attributes.rs b/src/style/attributes.rs new file mode 100644 index 00000000..b09ab024 --- /dev/null +++ b/src/style/attributes.rs @@ -0,0 +1,119 @@ +use crate::style::Attribute; +use std::ops::{BitAnd, BitOr, BitXor}; + +/// a bitset for all possible attributes +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct Attributes(u32); + +impl From for Attributes { + fn from(attribute: Attribute) -> Self { + Self(attribute.bytes()) + } +} + +impl From<&[Attribute]> for Attributes { + fn from(arr: &[Attribute]) -> Self { + let mut attributes = Attributes::default(); + for &attr in arr { + attributes.set(attr); + } + attributes + } +} + +impl BitAnd for Attributes { + type Output = Self; + fn bitand(self, rhs: Attribute) -> Self { + Self(self.0 & rhs.bytes()) + } +} +impl BitAnd for Attributes { + type Output = Self; + fn bitand(self, rhs: Self) -> Self { + Self(self.0 & rhs.0) + } +} + +impl BitOr for Attributes { + type Output = Self; + fn bitor(self, rhs: Attribute) -> Self { + Self(self.0 | rhs.bytes()) + } +} +impl BitOr for Attributes { + type Output = Self; + fn bitor(self, rhs: Self) -> Self { + Self(self.0 | rhs.0) + } +} + +impl BitXor for Attributes { + type Output = Self; + fn bitxor(self, rhs: Attribute) -> Self { + Self(self.0 ^ rhs.bytes()) + } +} +impl BitXor for Attributes { + type Output = Self; + fn bitxor(self, rhs: Self) -> Self { + Self(self.0 ^ rhs.0) + } +} + +impl Attributes { + /// Sets the attribute. + /// If it's already set, this does nothing. + #[inline(always)] + pub fn set(&mut self, attribute: Attribute) { + self.0 |= attribute.bytes(); + } + + /// Unsets the attribute. + /// If it's not set, this changes nothing. + #[inline(always)] + pub fn unset(&mut self, attribute: Attribute) { + self.0 &= !attribute.bytes(); + } + + /// Sets the attribute if it's unset, unset it + /// if it is set. + #[inline(always)] + pub fn toggle(&mut self, attribute: Attribute) { + self.0 ^= attribute.bytes(); + } + + /// Returns whether the attribute is set. + #[inline(always)] + pub fn has(self, attribute: Attribute) -> bool { + self.0 & attribute.bytes() != 0 + } + + /// Sets all the passed attributes. Removes none. + #[inline(always)] + pub fn extend(&mut self, attributes: Attributes) { + self.0 |= attributes.0; + } + + /// Returns whether there is no attribute set. + #[inline(always)] + pub fn is_empty(self) -> bool { + self.0 == 0 + } +} + +#[cfg(test)] +mod tests { + use super::{Attribute, Attributes}; + + #[test] + fn test_attributes() { + let mut attributes: Attributes = Attribute::Bold.into(); + assert!(attributes.has(Attribute::Bold)); + attributes.set(Attribute::Italic); + assert!(attributes.has(Attribute::Italic)); + attributes.unset(Attribute::Italic); + assert!(!attributes.has(Attribute::Italic)); + attributes.toggle(Attribute::Bold); + assert!(attributes.is_empty()); + } +} diff --git a/src/style/content_style.rs b/src/style/content_style.rs index 21dde1b4..fd761add 100644 --- a/src/style/content_style.rs +++ b/src/style/content_style.rs @@ -2,7 +2,7 @@ use std::fmt::Display; -use crate::style::{Attribute, Color, StyledContent}; +use crate::style::{Attribute, Attributes, Color, StyledContent}; /// The style that can be put on content. #[derive(Debug, Clone, Default)] @@ -12,7 +12,7 @@ pub struct ContentStyle { /// The background color. pub background_color: Option, /// List of attributes. - pub attributes: Vec, + pub attributes: Attributes, } impl ContentStyle { @@ -51,7 +51,7 @@ impl ContentStyle { /// You can add more attributes by calling this method multiple times. #[inline] pub fn attribute(mut self, attr: Attribute) -> ContentStyle { - self.attributes.push(attr); + self.attributes.set(attr); self } } @@ -65,11 +65,11 @@ mod tests { let content_style = ContentStyle::new() .foreground(Color::Blue) .background(Color::Red) - .attribute(Attribute::Reset); + .attribute(Attribute::Bold); assert_eq!(content_style.foreground_color, Some(Color::Blue)); assert_eq!(content_style.background_color, Some(Color::Red)); - assert_eq!(content_style.attributes[0], Attribute::Reset); + assert!(content_style.attributes.has(Attribute::Bold)); } #[test] @@ -83,6 +83,6 @@ mod tests { assert_eq!(styled_content.style().foreground_color, Some(Color::Blue)); assert_eq!(styled_content.style().background_color, Some(Color::Red)); - assert_eq!(styled_content.style().attributes[0], Attribute::Reset); + assert!(styled_content.style().attributes.has(Attribute::Reset)); } } diff --git a/src/style/enums/attribute.rs b/src/style/enums/attribute.rs index 53376268..8f98b293 100644 --- a/src/style/enums/attribute.rs +++ b/src/style/enums/attribute.rs @@ -5,59 +5,89 @@ use serde::{Deserialize, Serialize}; use super::super::SetAttribute; -/// Represents an attribute. -/// -/// # Platform-specific Notes -/// -/// * Only UNIX and Windows 10 terminals do support text attributes. -/// * Keep in mind that not all terminals support all attributes. -/// * Crossterm implements almost all attributes listed in the -/// [SGR parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters). -/// -/// | Attribute | Windows | UNIX | Notes | -/// | :-- | :--: | :--: | :-- | -/// | `Reset` | ✓ | ✓ | | -/// | `Bold` | ✓ | ✓ | | -/// | `Dim` | ✓ | ✓ | | -/// | `Italic` | ? | ? | Not widely supported, sometimes treated as inverse. | -/// | `Underlined` | ✓ | ✓ | | -/// | `SlowBlink` | ? | ? | Not widely supported, sometimes treated as inverse. | -/// | `RapidBlink` | ? | ? | Not widely supported. MS-DOS ANSI.SYS; 150+ per minute. | -/// | `Reverse` | ✓ | ✓ | | -/// | `Hidden` | ✓ | ✓ | Also known as Conceal. | -/// | `Fraktur` | ✗ | ✓ | Legible characters, but marked for deletion. | -/// | `DefaultForegroundColor` | ? | ? | Implementation specific (according to standard). | -/// | `DefaultBackgroundColor` | ? | ? | Implementation specific (according to standard). | -/// | `Framed` | ? | ? | Not widely supported. | -/// | `Encircled` | ? | ? | This should turn on the encircled attribute. | -/// | `OverLined` | ? | ? | This should draw a line at the top of the text. | -/// -/// # Examples -/// -/// Basic usage: -/// -/// ```no_run -/// use crossterm::style::Attribute; -/// -/// println!( -/// "{} Underlined {} No Underline", -/// Attribute::Underlined, -/// Attribute::NoUnderline -/// ); -/// ``` -/// -/// Style existing text: -/// -/// ```no_run -/// use crossterm::style::Styler; -/// -/// println!("{}", "Bold text".bold()); -/// println!("{}", "Underlined text".underlined()); -/// println!("{}", "Negative text".negative()); -/// ``` -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub enum Attribute { +// This macro generates the Attribute enum, its iterator +// function, and the static array containing the sgr code +// of each attribute +macro_rules! Attribute { + ( + $( + $(#[$inner:ident $($args:tt)*])* + $name:ident = $sgr:expr, + )* + ) => { + /// Represents an attribute. + /// + /// # Platform-specific Notes + /// + /// * Only UNIX and Windows 10 terminals do support text attributes. + /// * Keep in mind that not all terminals support all attributes. + /// * Crossterm implements almost all attributes listed in the + /// [SGR parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters). + /// + /// | Attribute | Windows | UNIX | Notes | + /// | :-- | :--: | :--: | :-- | + /// | `Reset` | ✓ | ✓ | | + /// | `Bold` | ✓ | ✓ | | + /// | `Dim` | ✓ | ✓ | | + /// | `Italic` | ? | ? | Not widely supported, sometimes treated as inverse. | + /// | `Underlined` | ✓ | ✓ | | + /// | `SlowBlink` | ? | ? | Not widely supported, sometimes treated as inverse. | + /// | `RapidBlink` | ? | ? | Not widely supported. MS-DOS ANSI.SYS; 150+ per minute. | + /// | `Reverse` | ✓ | ✓ | | + /// | `Hidden` | ✓ | ✓ | Also known as Conceal. | + /// | `Fraktur` | ✗ | ✓ | Legible characters, but marked for deletion. | + /// | `DefaultForegroundColor` | ? | ? | Implementation specific (according to standard). | + /// | `DefaultBackgroundColor` | ? | ? | Implementation specific (according to standard). | + /// | `Framed` | ? | ? | Not widely supported. | + /// | `Encircled` | ? | ? | This should turn on the encircled attribute. | + /// | `OverLined` | ? | ? | This should draw a line at the top of the text. | + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// use crossterm::style::Attribute; + /// + /// println!( + /// "{} Underlined {} No Underline", + /// Attribute::Underlined, + /// Attribute::NoUnderline + /// ); + /// ``` + /// + /// Style existing text: + /// + /// ```no_run + /// use crossterm::style::Styler; + /// + /// println!("{}", "Bold text".bold()); + /// println!("{}", "Underlined text".underlined()); + /// println!("{}", "Negative text".negative()); + /// ``` + #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] + #[non_exhaustive] + #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] + pub enum Attribute { + $( + $(#[$inner $($args)*])* + $name, + )* + } + pub static SGR: &'static[i16] = &[ + $($sgr,)* + ]; + impl Attribute { + /// Iterates over all the variants of the Attribute enum. + pub fn iterator() -> impl Iterator { + use self::Attribute::*; + [ $($name,)* ].iter().copied() + } + } + } +} + +Attribute! { /// Resets all the attributes. Reset = 0, /// Increases the text intensity. @@ -82,7 +112,7 @@ pub enum Attribute { /// /// Mostly used for [mathematical alphanumeric symbols](https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols). Fraktur = 20, - /// Turns off the `Bold` attribute. + /// Turns off the `Bold` attribute. - Inconsistent - Prefer to use NormalIntensity NoBold = 21, /// Switches the text back to normal intensity (no bold, italic). NormalIntensity = 22, @@ -108,8 +138,6 @@ pub enum Attribute { NotFramedOrEncircled = 54, /// Turns off the `OverLined` attribute. NotOverLined = 55, - #[doc(hidden)] - __Nonexhaustive, } impl Display for Attribute { @@ -118,3 +146,22 @@ impl Display for Attribute { Ok(()) } } + +impl Attribute { + /// Returns a u32 with one bit set, which is the + /// signature of this attribute in the Attributes + /// bitset. + /// + /// The +1 enables storing Reset (whose index is 0) + /// in the bitset Attributes. + #[inline(always)] + pub const fn bytes(self) -> u32 { + 1 << ((self as u32) + 1) + } + /// Returns the SGR attribute value. + /// + /// See https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters + pub fn sgr(self) -> i16 { + SGR[self as usize] + } +} diff --git a/src/style/macros.rs b/src/style/macros.rs index c99f2e8d..0babf1dc 100644 --- a/src/style/macros.rs +++ b/src/style/macros.rs @@ -39,7 +39,7 @@ macro_rules! def_str_attr { fn $name(self) -> StyledContent<&'static str> { StyledContent::new( ContentStyle { - attributes: vec![ $attr ], + attributes: $attr.into(), ..Default::default() }, self diff --git a/src/style/styled_content.rs b/src/style/styled_content.rs index 5352fa94..47184bea 100644 --- a/src/style/styled_content.rs +++ b/src/style/styled_content.rs @@ -8,7 +8,7 @@ use std::{ use crate::{ queue, style::{ - Attribute, Color, Colorize, ContentStyle, ResetColor, SetAttribute, SetBackgroundColor, + Attribute, Color, Colorize, ContentStyle, ResetColor, SetAttributes, SetBackgroundColor, SetForegroundColor, Styler, }, }; @@ -104,8 +104,8 @@ impl Display for StyledContent { reset = true; } - for attr in &self.style.attributes { - queue!(f, SetAttribute(*attr)).map_err(|_| fmt::Error)?; + if !self.style.attributes.is_empty() { + queue!(f, SetAttributes(self.style.attributes)).map_err(|_| fmt::Error)?; reset = true; } @@ -183,7 +183,7 @@ mod tests { let style = ContentStyle::new() .foreground(Color::Blue) .background(Color::Red) - .attribute(Attribute::Reset); + .attribute(Attribute::Bold); let mut styled_content = style.apply("test"); @@ -194,8 +194,7 @@ mod tests { assert_eq!(styled_content.style.foreground_color, Some(Color::Green)); assert_eq!(styled_content.style.background_color, Some(Color::Magenta)); - assert_eq!(styled_content.style.attributes.len(), 2); - assert_eq!(styled_content.style.attributes[0], Attribute::Reset); - assert_eq!(styled_content.style.attributes[1], Attribute::NoItalic); + assert!(styled_content.style.attributes.has(Attribute::Bold)); + assert!(styled_content.style.attributes.has(Attribute::NoItalic)); } }