From 7b06a7b3113c11982813f2d425f4f3f648bb4e4a Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Wed, 26 Mar 2025 19:29:59 +0100 Subject: [PATCH 01/27] Replace compute_stlye with a Writer --- src/lib.rs | 103 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 51eb8b3..a568b15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,6 +44,7 @@ pub mod customcolors; pub use color::*; +use core::fmt::{Display, Write}; use std::{ borrow::Cow, error::Error, @@ -500,39 +501,7 @@ impl ColoredString { false } - fn compute_style(&self) -> String { - if !Self::has_colors() || self.is_plain() { - return String::new(); - } - - let mut res = String::from("\x1B["); - let mut has_wrote = if self.style == style::CLEAR { - false - } else { - res.push_str(&self.style.to_str()); - true - }; - - if let Some(ref bgcolor) = self.bgcolor { - if has_wrote { - res.push(';'); - } - res.push_str(&bgcolor.to_bg_str()); - has_wrote = true; - } - - if let Some(ref fgcolor) = self.fgcolor { - if has_wrote { - res.push(';'); - } - - res.push_str(&fgcolor.to_fg_str()); - } - - res.push('m'); - res - } fn escape_inner_reset_sequences(&self) -> Cow { if !Self::has_colors() || self.is_plain() { @@ -541,7 +510,7 @@ impl ColoredString { // TODO: BoyScoutRule let reset = "\x1B[0m"; - let style = self.compute_style(); + let style = ColoredStringDisplayHelper::new(self).to_string(); let matches: Vec = self .input .match_indices(reset) @@ -569,6 +538,51 @@ impl ColoredString { } } +#[derive(Debug)] +struct ColoredStringDisplayHelper<'a> { + inner: &'a ColoredString, +} + +impl<'a> ColoredStringDisplayHelper<'a> { + fn new(inner: &'a ColoredString) -> Self { + Self { inner } + } +} + +impl Display for ColoredStringDisplayHelper<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !ColoredString::has_colors() || self.inner.is_plain() { + return Ok(()); + } + f.write_str("\x1B[")?; + let mut has_wrote = if self.inner.style == style::CLEAR { + false + } else { + f.write_str(&self.inner.style.to_str())?; + true + }; + + if let Some(ref bgcolor) = self.inner.bgcolor { + if has_wrote { + f.write_char(';')?; + } + f.write_str(&bgcolor.to_bg_str())?; + has_wrote = true; + } + + if let Some(ref fgcolor) = self.inner.fgcolor { + if has_wrote { + f.write_char(';')?; + } + + f.write_str(&fgcolor.to_fg_str())?; + } + + f.write_char('m')?; + Ok(()) + } +} + impl Deref for ColoredString { type Target = str; fn deref(&self) -> &Self::Target { @@ -720,8 +734,7 @@ impl fmt::Display for ColoredString { // XXX: see tests. Useful when nesting colored strings let escaped_input = self.escape_inner_reset_sequences(); - - f.write_str(&self.compute_style())?; + ColoredStringDisplayHelper::new(self).fmt(f)?; escaped_input.fmt(f)?; f.write_str("\x1B[0m")?; Ok(()) @@ -800,7 +813,7 @@ mod tests { #[test] fn compute_style_empty_string() { - assert_eq!("", "".clear().compute_style()); + assert_eq!("", ColoredStringDisplayHelper::new(&"".clear()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -808,7 +821,7 @@ mod tests { fn compute_style_simple_fg_blue() { let blue = "\x1B[34m"; - assert_eq!(blue, "".blue().compute_style()); + assert_eq!(blue, ColoredStringDisplayHelper::new(&"".blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -816,7 +829,7 @@ mod tests { fn compute_style_simple_bg_blue() { let on_blue = "\x1B[44m"; - assert_eq!(on_blue, "".on_blue().compute_style()); + assert_eq!(on_blue, ColoredStringDisplayHelper::new(&"".on_blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -824,7 +837,7 @@ mod tests { fn compute_style_blue_on_blue() { let blue_on_blue = "\x1B[44;34m"; - assert_eq!(blue_on_blue, "".blue().on_blue().compute_style()); + assert_eq!(blue_on_blue, ColoredStringDisplayHelper::new(&"".blue().on_blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -832,7 +845,7 @@ mod tests { fn compute_style_simple_fg_bright_blue() { let blue = "\x1B[94m"; - assert_eq!(blue, "".bright_blue().compute_style()); + assert_eq!(blue, ColoredStringDisplayHelper::new(&"".bright_blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -840,7 +853,7 @@ mod tests { fn compute_style_simple_bg_bright_blue() { let on_blue = "\x1B[104m"; - assert_eq!(on_blue, "".on_bright_blue().compute_style()); + assert_eq!(on_blue, ColoredStringDisplayHelper::new(&"".on_bright_blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -850,7 +863,7 @@ mod tests { assert_eq!( blue_on_blue, - "".bright_blue().on_bright_blue().compute_style() + ColoredStringDisplayHelper::new(&"".bright_blue().on_bright_blue()).to_string() ); } @@ -859,7 +872,7 @@ mod tests { fn compute_style_simple_bold() { let bold = "\x1B[1m"; - assert_eq!(bold, "".bold().compute_style()); + assert_eq!(bold, ColoredStringDisplayHelper::new(&"".bold()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -867,7 +880,7 @@ mod tests { fn compute_style_blue_bold() { let blue_bold = "\x1B[1;34m"; - assert_eq!(blue_bold, "".blue().bold().compute_style()); + assert_eq!(blue_bold, ColoredStringDisplayHelper::new(&"".blue().bold()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -877,7 +890,7 @@ mod tests { assert_eq!( blue_bold_on_blue, - "".blue().bold().on_blue().compute_style() + ColoredStringDisplayHelper::new(&"".blue().bold().on_blue()).to_string() ); } From 1ad8d7148f75967e2553eae3980238a223ba5494 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Wed, 26 Mar 2025 19:30:25 +0100 Subject: [PATCH 02/27] fmt --- src/lib.rs | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a568b15..f8d4379 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -501,8 +501,6 @@ impl ColoredString { false } - - fn escape_inner_reset_sequences(&self) -> Cow { if !Self::has_colors() || self.is_plain() { return self.input.as_str().into(); @@ -821,7 +819,10 @@ mod tests { fn compute_style_simple_fg_blue() { let blue = "\x1B[34m"; - assert_eq!(blue, ColoredStringDisplayHelper::new(&"".blue()).to_string()); + assert_eq!( + blue, + ColoredStringDisplayHelper::new(&"".blue()).to_string() + ); } #[cfg_attr(feature = "no-color", ignore)] @@ -829,7 +830,10 @@ mod tests { fn compute_style_simple_bg_blue() { let on_blue = "\x1B[44m"; - assert_eq!(on_blue, ColoredStringDisplayHelper::new(&"".on_blue()).to_string()); + assert_eq!( + on_blue, + ColoredStringDisplayHelper::new(&"".on_blue()).to_string() + ); } #[cfg_attr(feature = "no-color", ignore)] @@ -837,7 +841,10 @@ mod tests { fn compute_style_blue_on_blue() { let blue_on_blue = "\x1B[44;34m"; - assert_eq!(blue_on_blue, ColoredStringDisplayHelper::new(&"".blue().on_blue()).to_string()); + assert_eq!( + blue_on_blue, + ColoredStringDisplayHelper::new(&"".blue().on_blue()).to_string() + ); } #[cfg_attr(feature = "no-color", ignore)] @@ -845,7 +852,10 @@ mod tests { fn compute_style_simple_fg_bright_blue() { let blue = "\x1B[94m"; - assert_eq!(blue, ColoredStringDisplayHelper::new(&"".bright_blue()).to_string()); + assert_eq!( + blue, + ColoredStringDisplayHelper::new(&"".bright_blue()).to_string() + ); } #[cfg_attr(feature = "no-color", ignore)] @@ -853,7 +863,10 @@ mod tests { fn compute_style_simple_bg_bright_blue() { let on_blue = "\x1B[104m"; - assert_eq!(on_blue, ColoredStringDisplayHelper::new(&"".on_bright_blue()).to_string()); + assert_eq!( + on_blue, + ColoredStringDisplayHelper::new(&"".on_bright_blue()).to_string() + ); } #[cfg_attr(feature = "no-color", ignore)] @@ -872,7 +885,10 @@ mod tests { fn compute_style_simple_bold() { let bold = "\x1B[1m"; - assert_eq!(bold, ColoredStringDisplayHelper::new(&"".bold()).to_string()); + assert_eq!( + bold, + ColoredStringDisplayHelper::new(&"".bold()).to_string() + ); } #[cfg_attr(feature = "no-color", ignore)] @@ -880,7 +896,10 @@ mod tests { fn compute_style_blue_bold() { let blue_bold = "\x1B[1;34m"; - assert_eq!(blue_bold, ColoredStringDisplayHelper::new(&"".blue().bold()).to_string()); + assert_eq!( + blue_bold, + ColoredStringDisplayHelper::new(&"".blue().bold()).to_string() + ); } #[cfg_attr(feature = "no-color", ignore)] From 82fa2c2528a183c3866ef1aeaddf1d2e150d1b6d Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Wed, 26 Mar 2025 19:34:39 +0100 Subject: [PATCH 03/27] Replace loop with insert_str --- src/lib.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f8d4379..f60d300 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -522,14 +522,10 @@ impl ColoredString { input.reserve(matches.len() * style.len()); for (idx_in_matches, offset) in matches.into_iter().enumerate() { - // shift the offset to the end of the reset sequence and take in account - // the number of matches we have escaped (which shift the index to insert) - let mut offset = offset + reset.len() + idx_in_matches * style.len(); + // shift the offset to the end of the reset sequence + let offset = offset + reset.len() + idx_in_matches * style.len(); - for cchar in style.chars() { - input.insert(offset, cchar); - offset += 1; - } + input.insert_str(offset, &style); } input.into() From 19faeba6b7fa2dc16ec91d95df71e36846789e21 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Wed, 26 Mar 2025 20:05:12 +0100 Subject: [PATCH 04/27] Replaced escape_inner_reset_sequences --- src/lib.rs | 103 ++++++++++++++++++++++++++--------------------------- 1 file changed, 50 insertions(+), 53 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f60d300..503a3d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -500,50 +500,60 @@ impl ColoredString { fn has_colors() -> bool { false } +} - fn escape_inner_reset_sequences(&self) -> Cow { - if !Self::has_colors() || self.is_plain() { - return self.input.as_str().into(); - } +#[derive(Debug)] +struct EscapeInnerResetSequencesHelper<'a> { + inner: &'a ColoredString, +} - // TODO: BoyScoutRule - let reset = "\x1B[0m"; - let style = ColoredStringDisplayHelper::new(self).to_string(); - let matches: Vec = self - .input - .match_indices(reset) - .map(|(idx, _)| idx) - .collect(); - if matches.is_empty() { - return self.input.as_str().into(); - } +impl<'a> EscapeInnerResetSequencesHelper<'a> { + fn new(inner: &'a ColoredString) -> Self { + Self { inner } + } +} - let mut input = self.input.clone(); - input.reserve(matches.len() * style.len()); +impl Display for EscapeInnerResetSequencesHelper<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + const RESET: &str = "\x1B[0m"; - for (idx_in_matches, offset) in matches.into_iter().enumerate() { + if !ColoredString::has_colors() || self.inner.is_plain() { + return f.write_str(self.inner); + } + let mut matches = self + .inner + .match_indices(RESET) + .map(|(idx, _)| idx + RESET.len()) + .peekable(); + if matches.peek().is_none() { + return f.write_str(self.inner); + } + let mut start = 0; + for offset in matches { // shift the offset to the end of the reset sequence - let offset = offset + reset.len() + idx_in_matches * style.len(); - input.insert_str(offset, &style); + f.write_str(&self.inner.input[start..offset])?; + start = offset; + ComputeStyleHelper::new(self.inner).fmt(f)?; } + f.write_str(&self.inner.input[start..])?; - input.into() + Ok(()) } } #[derive(Debug)] -struct ColoredStringDisplayHelper<'a> { +struct ComputeStyleHelper<'a> { inner: &'a ColoredString, } -impl<'a> ColoredStringDisplayHelper<'a> { +impl<'a> ComputeStyleHelper<'a> { fn new(inner: &'a ColoredString) -> Self { Self { inner } } } -impl Display for ColoredStringDisplayHelper<'_> { +impl Display for ComputeStyleHelper<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if !ColoredString::has_colors() || self.inner.is_plain() { return Ok(()); @@ -727,9 +737,8 @@ impl fmt::Display for ColoredString { } // XXX: see tests. Useful when nesting colored strings - let escaped_input = self.escape_inner_reset_sequences(); - ColoredStringDisplayHelper::new(self).fmt(f)?; - escaped_input.fmt(f)?; + ComputeStyleHelper::new(self).fmt(f)?; + EscapeInnerResetSequencesHelper::new(self).fmt(f)?; f.write_str("\x1B[0m")?; Ok(()) } @@ -807,7 +816,7 @@ mod tests { #[test] fn compute_style_empty_string() { - assert_eq!("", ColoredStringDisplayHelper::new(&"".clear()).to_string()); + assert_eq!("", ComputeStyleHelper::new(&"".clear()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -815,10 +824,7 @@ mod tests { fn compute_style_simple_fg_blue() { let blue = "\x1B[34m"; - assert_eq!( - blue, - ColoredStringDisplayHelper::new(&"".blue()).to_string() - ); + assert_eq!(blue, ComputeStyleHelper::new(&"".blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -826,10 +832,7 @@ mod tests { fn compute_style_simple_bg_blue() { let on_blue = "\x1B[44m"; - assert_eq!( - on_blue, - ColoredStringDisplayHelper::new(&"".on_blue()).to_string() - ); + assert_eq!(on_blue, ComputeStyleHelper::new(&"".on_blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -839,7 +842,7 @@ mod tests { assert_eq!( blue_on_blue, - ColoredStringDisplayHelper::new(&"".blue().on_blue()).to_string() + ComputeStyleHelper::new(&"".blue().on_blue()).to_string() ); } @@ -848,10 +851,7 @@ mod tests { fn compute_style_simple_fg_bright_blue() { let blue = "\x1B[94m"; - assert_eq!( - blue, - ColoredStringDisplayHelper::new(&"".bright_blue()).to_string() - ); + assert_eq!(blue, ComputeStyleHelper::new(&"".bright_blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -861,7 +861,7 @@ mod tests { assert_eq!( on_blue, - ColoredStringDisplayHelper::new(&"".on_bright_blue()).to_string() + ComputeStyleHelper::new(&"".on_bright_blue()).to_string() ); } @@ -872,7 +872,7 @@ mod tests { assert_eq!( blue_on_blue, - ColoredStringDisplayHelper::new(&"".bright_blue().on_bright_blue()).to_string() + ComputeStyleHelper::new(&"".bright_blue().on_bright_blue()).to_string() ); } @@ -881,10 +881,7 @@ mod tests { fn compute_style_simple_bold() { let bold = "\x1B[1m"; - assert_eq!( - bold, - ColoredStringDisplayHelper::new(&"".bold()).to_string() - ); + assert_eq!(bold, ComputeStyleHelper::new(&"".bold()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -894,7 +891,7 @@ mod tests { assert_eq!( blue_bold, - ColoredStringDisplayHelper::new(&"".blue().bold()).to_string() + ComputeStyleHelper::new(&"".blue().bold()).to_string() ); } @@ -905,7 +902,7 @@ mod tests { assert_eq!( blue_bold_on_blue, - ColoredStringDisplayHelper::new(&"".blue().bold().on_blue()).to_string() + ComputeStyleHelper::new(&"".blue().bold().on_blue()).to_string() ); } @@ -914,7 +911,7 @@ mod tests { let style = ColoredString::default(); let expected = String::new(); - let output = style.escape_inner_reset_sequences(); + let output = EscapeInnerResetSequencesHelper::new(&style).to_string(); assert_eq!(expected, output); } @@ -927,7 +924,7 @@ mod tests { }; let expected = String::from("hello world !"); - let output = style.escape_inner_reset_sequences(); + let output = EscapeInnerResetSequencesHelper::new(&style).to_string(); assert_eq!(expected, output); } @@ -938,7 +935,7 @@ mod tests { let input = format!("start {} end", String::from("hello world !").red()); let style = input.blue(); - let output = style.escape_inner_reset_sequences(); + let output = EscapeInnerResetSequencesHelper::new(&style).to_string(); let blue = "\x1B[34m"; let red = "\x1B[31m"; let reset = "\x1B[0m"; @@ -954,7 +951,7 @@ mod tests { let input = format!("start 1:{italic_str} 2:{italic_str} 3:{italic_str} end"); let style = input.blue(); - let output = style.escape_inner_reset_sequences(); + let output = EscapeInnerResetSequencesHelper::new(&style).to_string(); let blue = "\x1B[34m"; let italic = "\x1B[3m"; let reset = "\x1B[0m"; From 19eec9b5ab73b062674d6e040e67396207a0aa60 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Wed, 26 Mar 2025 20:56:32 +0100 Subject: [PATCH 05/27] Fix tests --- src/color.rs | 60 +++++++++++++++++++++++++++++++++++ src/lib.rs | 90 ++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 127 insertions(+), 23 deletions(-) diff --git a/src/color.rs b/src/color.rs index 504a362..d0b85e1 100644 --- a/src/color.rs +++ b/src/color.rs @@ -33,6 +33,32 @@ fn truecolor_support() -> bool { #[allow(missing_docs)] impl Color { + pub(crate) fn to_fg_fmt_with_len(self, f : &mut core::fmt::Formatter) -> Result { + match self { + Self::Black => {f.write_str("30")?; Ok(2)}, + Self::Red => {f.write_str("31")?; Ok(2)}, + Self::Green => {f.write_str("32")?; Ok(2)}, + Self::Yellow => {f.write_str("33")?; Ok(2)}, + Self::Blue => {f.write_str("34")?; Ok(2)}, + Self::Magenta => {f.write_str("35")?; Ok(2)}, + Self::Cyan => {f.write_str("36")?; Ok(2)}, + Self::White => {f.write_str("37")?; Ok(2)}, + Self::BrightBlack => {f.write_str("90")?; Ok(2)}, + Self::BrightRed => {f.write_str("91")?; Ok(2)}, + Self::BrightGreen => {f.write_str("92")?; Ok(2)}, + Self::BrightYellow => {f.write_str("93")?; Ok(2)}, + Self::BrightBlue => {f.write_str("94")?; Ok(2)}, + Self::BrightMagenta => {f.write_str("95")?; Ok(2)}, + Self::BrightCyan => {f.write_str("96")?; Ok(2)}, + Self::BrightWhite => {f.write_str("97")?; Ok(2)}, + Self::TrueColor { .. } if !truecolor_support() => { + self.closest_color_euclidean().to_fg_fmt_with_len(f) + } + Self::TrueColor { r, g, b } => {write!(f, "38;2;{r};{g};{b}")?; Ok(7 + u8_len(r)+ u8_len(g) + u8_len(b))}, + } + } + + #[must_use] pub fn to_fg_str(&self) -> Cow<'static, str> { match *self { @@ -59,6 +85,32 @@ impl Color { } } + pub(crate) fn to_bg_fmt_with_len(self, f : &mut core::fmt::Formatter) -> Result { + match self { + Self::Black => {f.write_str("40")?; Ok(2)}, + Self::Red => {f.write_str("41")?; Ok(2)}, + Self::Green => {f.write_str("42")?; Ok(2)}, + Self::Yellow => {f.write_str("43")?; Ok(2)}, + Self::Blue => {f.write_str("44")?; Ok(2)}, + Self::Magenta => {f.write_str("45")?; Ok(2)}, + Self::Cyan => {f.write_str("46")?; Ok(2)}, + Self::White => {f.write_str("47")?; Ok(2)}, + Self::BrightBlack => {f.write_str("100")?; Ok(3)}, + Self::BrightRed => {f.write_str("101")?; Ok(3)}, + Self::BrightGreen => {f.write_str("102")?; Ok(3)}, + Self::BrightYellow => {f.write_str("103")?; Ok(3)}, + Self::BrightBlue => {f.write_str("104")?; Ok(3)}, + Self::BrightMagenta => {f.write_str("105")?; Ok(3)}, + Self::BrightCyan => {f.write_str("106")?; Ok(3)}, + Self::BrightWhite => {f.write_str("107")?; Ok(3)}, + Self::TrueColor { .. } if !truecolor_support() => { + self.closest_color_euclidean().to_bg_fmt_with_len(f) + } + Self::TrueColor { r, g, b } => {write!(f, "48;2;{r};{g};{b}")?; Ok(7 + u8_len(r)+ u8_len(g) + u8_len(b))}, + } + } + + #[must_use] pub fn to_bg_str(&self) -> Cow<'static, str> { match *self { @@ -196,6 +248,14 @@ impl Color { } } +fn u8_len(num : u8) -> usize { + match num { + 0..10 => 1, + 10..100 => 2, + 100.. => 3, + } +} + impl From<&str> for Color { fn from(src: &str) -> Self { src.parse().unwrap_or(Self::White) diff --git a/src/lib.rs b/src/lib.rs index 503a3d3..7579969 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,6 @@ pub use color::*; use core::fmt::{Display, Write}; use std::{ - borrow::Cow, error::Error, fmt, ops::{Deref, DerefMut}, @@ -500,6 +499,7 @@ impl ColoredString { fn has_colors() -> bool { false } + } #[derive(Debug)] @@ -511,14 +511,13 @@ impl<'a> EscapeInnerResetSequencesHelper<'a> { fn new(inner: &'a ColoredString) -> Self { Self { inner } } -} -impl Display for EscapeInnerResetSequencesHelper<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt_with_len(&self, f: &mut fmt::Formatter<'_>) -> Result { const RESET: &str = "\x1B[0m"; - + let mut len = self.inner.len(); if !ColoredString::has_colors() || self.inner.is_plain() { - return f.write_str(self.inner); + f.write_str(self.inner)?; + return Ok(len); } let mut matches = self .inner @@ -526,7 +525,8 @@ impl Display for EscapeInnerResetSequencesHelper<'_> { .map(|(idx, _)| idx + RESET.len()) .peekable(); if matches.peek().is_none() { - return f.write_str(self.inner); + f.write_str(self.inner)?; + return Ok(len); } let mut start = 0; for offset in matches { @@ -534,11 +534,17 @@ impl Display for EscapeInnerResetSequencesHelper<'_> { f.write_str(&self.inner.input[start..offset])?; start = offset; - ComputeStyleHelper::new(self.inner).fmt(f)?; + len += ComputeStyleHelper::new(self.inner).fmt_with_len(f)?; } f.write_str(&self.inner.input[start..])?; - Ok(()) + Ok(len) + } +} + +impl Display for EscapeInnerResetSequencesHelper<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.fmt_with_len(f).map(|_| ()) } } @@ -551,14 +557,14 @@ impl<'a> ComputeStyleHelper<'a> { fn new(inner: &'a ColoredString) -> Self { Self { inner } } -} -impl Display for ComputeStyleHelper<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt_with_len(&self, f: &mut fmt::Formatter<'_>)-> Result { + let mut len = 0; if !ColoredString::has_colors() || self.inner.is_plain() { - return Ok(()); + return Ok(len); } f.write_str("\x1B[")?; + len += 5; let mut has_wrote = if self.inner.style == style::CLEAR { false } else { @@ -568,22 +574,30 @@ impl Display for ComputeStyleHelper<'_> { if let Some(ref bgcolor) = self.inner.bgcolor { if has_wrote { + len += 1; f.write_char(';')?; } - f.write_str(&bgcolor.to_bg_str())?; + len += bgcolor.to_bg_fmt_with_len(f)?; has_wrote = true; } if let Some(ref fgcolor) = self.inner.fgcolor { if has_wrote { + len += 1; f.write_char(';')?; } - - f.write_str(&fgcolor.to_fg_str())?; + len += fgcolor.to_fg_fmt_with_len(f)?; } f.write_char('m')?; - Ok(()) + len += 1; + Ok(len) + } +} + +impl Display for ComputeStyleHelper<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.fmt_with_len(f).map(|_| ()) } } @@ -730,20 +744,50 @@ impl Colorize for &str { } } -impl fmt::Display for ColoredString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if !Self::has_colors() || self.is_plain() { - return ::fmt(&self.input, f); +struct ColoredStringDisplay<'a>(&'a ColoredString); + +impl Display for ColoredStringDisplay<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !ColoredString::has_colors() || self.0.is_plain() { + return ::fmt(&self.0.input, f); } + //let mut written = 0; + // XXX: see tests. Useful when nesting colored strings - ComputeStyleHelper::new(self).fmt(f)?; - EscapeInnerResetSequencesHelper::new(self).fmt(f)?; + ComputeStyleHelper::new(self.0).fmt(f)?; + //written += ComputeStyleHelper::new(self.0).fmt_with_len(f)?; + EscapeInnerResetSequencesHelper::new(self.0).fmt(f)?; + //written +=EscapeInnerResetSequencesHelper::new(self.0).fmt_with_len(f)?; f.write_str("\x1B[0m")?; + //written += 7; + + // FIXME + // Do padding + //if let Some(width) = dbg!(f.width()) { + // let to_pad = dbg!(width.saturating_sub(written)); + // let c = f.fill(); + // for _ in 0..to_pad { + // f.write_char(c)?; + // } + //} + Ok(()) } } + +impl fmt::Display for ColoredString { + #[allow(clippy::to_string_in_format_args)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if f.width().is_some() || f.precision().is_some() || f.align().is_some() { + f.pad(&ColoredStringDisplay(self).to_string()) + } else { + ColoredStringDisplay(self).fmt(f) + } + } +} + impl From for Box { fn from(cs: ColoredString) -> Self { Box::from(error::ColoredStringError(cs)) From 0c6f6c3703cf62cbd88950769409c287814887df Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:01:43 +0100 Subject: [PATCH 06/27] Cleanup --- src/color.rs | 90 ++++++++++++++++++++++++---------------------------- src/lib.rs | 58 ++++++++++----------------------- 2 files changed, 59 insertions(+), 89 deletions(-) diff --git a/src/color.rs b/src/color.rs index d0b85e1..c835738 100644 --- a/src/color.rs +++ b/src/color.rs @@ -33,32 +33,33 @@ fn truecolor_support() -> bool { #[allow(missing_docs)] impl Color { - pub(crate) fn to_fg_fmt_with_len(self, f : &mut core::fmt::Formatter) -> Result { + pub(crate) fn to_fg_fmt(self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { match self { - Self::Black => {f.write_str("30")?; Ok(2)}, - Self::Red => {f.write_str("31")?; Ok(2)}, - Self::Green => {f.write_str("32")?; Ok(2)}, - Self::Yellow => {f.write_str("33")?; Ok(2)}, - Self::Blue => {f.write_str("34")?; Ok(2)}, - Self::Magenta => {f.write_str("35")?; Ok(2)}, - Self::Cyan => {f.write_str("36")?; Ok(2)}, - Self::White => {f.write_str("37")?; Ok(2)}, - Self::BrightBlack => {f.write_str("90")?; Ok(2)}, - Self::BrightRed => {f.write_str("91")?; Ok(2)}, - Self::BrightGreen => {f.write_str("92")?; Ok(2)}, - Self::BrightYellow => {f.write_str("93")?; Ok(2)}, - Self::BrightBlue => {f.write_str("94")?; Ok(2)}, - Self::BrightMagenta => {f.write_str("95")?; Ok(2)}, - Self::BrightCyan => {f.write_str("96")?; Ok(2)}, - Self::BrightWhite => {f.write_str("97")?; Ok(2)}, + Self::Black => f.write_str("30"), + Self::Red => f.write_str("31"), + Self::Green => f.write_str("32"), + Self::Yellow => f.write_str("33"), + Self::Blue => f.write_str("34"), + Self::Magenta => f.write_str("35"), + Self::Cyan => f.write_str("36"), + Self::White => f.write_str("37"), + Self::BrightBlack => f.write_str("90"), + Self::BrightRed => f.write_str("91"), + Self::BrightGreen => f.write_str("92"), + Self::BrightYellow => f.write_str("93"), + Self::BrightBlue => f.write_str("94"), + Self::BrightMagenta => f.write_str("95"), + Self::BrightCyan => f.write_str("96"), + Self::BrightWhite => f.write_str("97"), Self::TrueColor { .. } if !truecolor_support() => { - self.closest_color_euclidean().to_fg_fmt_with_len(f) + self.closest_color_euclidean().to_fg_fmt(f) + } + Self::TrueColor { r, g, b } => { + write!(f, "38;2;{r};{g};{b}") } - Self::TrueColor { r, g, b } => {write!(f, "38;2;{r};{g};{b}")?; Ok(7 + u8_len(r)+ u8_len(g) + u8_len(b))}, } } - #[must_use] pub fn to_fg_str(&self) -> Cow<'static, str> { match *self { @@ -85,32 +86,33 @@ impl Color { } } - pub(crate) fn to_bg_fmt_with_len(self, f : &mut core::fmt::Formatter) -> Result { + pub(crate) fn to_bg_fmt(self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { match self { - Self::Black => {f.write_str("40")?; Ok(2)}, - Self::Red => {f.write_str("41")?; Ok(2)}, - Self::Green => {f.write_str("42")?; Ok(2)}, - Self::Yellow => {f.write_str("43")?; Ok(2)}, - Self::Blue => {f.write_str("44")?; Ok(2)}, - Self::Magenta => {f.write_str("45")?; Ok(2)}, - Self::Cyan => {f.write_str("46")?; Ok(2)}, - Self::White => {f.write_str("47")?; Ok(2)}, - Self::BrightBlack => {f.write_str("100")?; Ok(3)}, - Self::BrightRed => {f.write_str("101")?; Ok(3)}, - Self::BrightGreen => {f.write_str("102")?; Ok(3)}, - Self::BrightYellow => {f.write_str("103")?; Ok(3)}, - Self::BrightBlue => {f.write_str("104")?; Ok(3)}, - Self::BrightMagenta => {f.write_str("105")?; Ok(3)}, - Self::BrightCyan => {f.write_str("106")?; Ok(3)}, - Self::BrightWhite => {f.write_str("107")?; Ok(3)}, + Self::Black => f.write_str("40"), + Self::Red => f.write_str("41"), + Self::Green => f.write_str("42"), + Self::Yellow => f.write_str("43"), + Self::Blue => f.write_str("44"), + Self::Magenta => f.write_str("45"), + Self::Cyan => f.write_str("46"), + Self::White => f.write_str("47"), + Self::BrightBlack => f.write_str("100"), + Self::BrightRed => f.write_str("101"), + Self::BrightGreen => f.write_str("102"), + Self::BrightYellow => f.write_str("103"), + Self::BrightBlue => f.write_str("104"), + Self::BrightMagenta => f.write_str("105"), + Self::BrightCyan => f.write_str("106"), + Self::BrightWhite => f.write_str("107"), Self::TrueColor { .. } if !truecolor_support() => { - self.closest_color_euclidean().to_bg_fmt_with_len(f) + self.closest_color_euclidean().to_bg_fmt(f) + } + Self::TrueColor { r, g, b } => { + write!(f, "48;2;{r};{g};{b}") } - Self::TrueColor { r, g, b } => {write!(f, "48;2;{r};{g};{b}")?; Ok(7 + u8_len(r)+ u8_len(g) + u8_len(b))}, } } - #[must_use] pub fn to_bg_str(&self) -> Cow<'static, str> { match *self { @@ -248,14 +250,6 @@ impl Color { } } -fn u8_len(num : u8) -> usize { - match num { - 0..10 => 1, - 10..100 => 2, - 100.. => 3, - } -} - impl From<&str> for Color { fn from(src: &str) -> Self { src.parse().unwrap_or(Self::White) diff --git a/src/lib.rs b/src/lib.rs index 7579969..f88a716 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -499,7 +499,6 @@ impl ColoredString { fn has_colors() -> bool { false } - } #[derive(Debug)] @@ -512,12 +511,10 @@ impl<'a> EscapeInnerResetSequencesHelper<'a> { Self { inner } } - fn fmt_with_len(&self, f: &mut fmt::Formatter<'_>) -> Result { + fn private_fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { const RESET: &str = "\x1B[0m"; - let mut len = self.inner.len(); if !ColoredString::has_colors() || self.inner.is_plain() { - f.write_str(self.inner)?; - return Ok(len); + return f.write_str(self.inner); } let mut matches = self .inner @@ -525,8 +522,7 @@ impl<'a> EscapeInnerResetSequencesHelper<'a> { .map(|(idx, _)| idx + RESET.len()) .peekable(); if matches.peek().is_none() { - f.write_str(self.inner)?; - return Ok(len); + return f.write_str(self.inner); } let mut start = 0; for offset in matches { @@ -534,17 +530,17 @@ impl<'a> EscapeInnerResetSequencesHelper<'a> { f.write_str(&self.inner.input[start..offset])?; start = offset; - len += ComputeStyleHelper::new(self.inner).fmt_with_len(f)?; + ComputeStyleHelper::new(self.inner).private_fmt(f)?; } f.write_str(&self.inner.input[start..])?; - Ok(len) + Ok(()) } } impl Display for EscapeInnerResetSequencesHelper<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.fmt_with_len(f).map(|_| ()) + self.private_fmt(f) } } @@ -558,13 +554,11 @@ impl<'a> ComputeStyleHelper<'a> { Self { inner } } - fn fmt_with_len(&self, f: &mut fmt::Formatter<'_>)-> Result { - let mut len = 0; + fn private_fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { if !ColoredString::has_colors() || self.inner.is_plain() { - return Ok(len); + return Ok(()); } f.write_str("\x1B[")?; - len += 5; let mut has_wrote = if self.inner.style == style::CLEAR { false } else { @@ -574,30 +568,27 @@ impl<'a> ComputeStyleHelper<'a> { if let Some(ref bgcolor) = self.inner.bgcolor { if has_wrote { - len += 1; f.write_char(';')?; } - len += bgcolor.to_bg_fmt_with_len(f)?; + bgcolor.to_bg_fmt(f)?; has_wrote = true; } if let Some(ref fgcolor) = self.inner.fgcolor { if has_wrote { - len += 1; f.write_char(';')?; } - len += fgcolor.to_fg_fmt_with_len(f)?; + fgcolor.to_fg_fmt(f)?; } f.write_char('m')?; - len += 1; - Ok(len) + Ok(()) } } impl Display for ComputeStyleHelper<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.fmt_with_len(f).map(|_| ()) + self.private_fmt(f) } } @@ -752,39 +743,24 @@ impl Display for ColoredStringDisplay<'_> { return ::fmt(&self.0.input, f); } - //let mut written = 0; - // XXX: see tests. Useful when nesting colored strings ComputeStyleHelper::new(self.0).fmt(f)?; - //written += ComputeStyleHelper::new(self.0).fmt_with_len(f)?; EscapeInnerResetSequencesHelper::new(self.0).fmt(f)?; - //written +=EscapeInnerResetSequencesHelper::new(self.0).fmt_with_len(f)?; f.write_str("\x1B[0m")?; - //written += 7; - - // FIXME - // Do padding - //if let Some(width) = dbg!(f.width()) { - // let to_pad = dbg!(width.saturating_sub(written)); - // let c = f.fill(); - // for _ in 0..to_pad { - // f.write_char(c)?; - // } - //} Ok(()) } } - impl fmt::Display for ColoredString { #[allow(clippy::to_string_in_format_args)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if f.width().is_some() || f.precision().is_some() || f.align().is_some() { + // FIXME, deal with padding and do not allocate for this + if f.width().is_some() || f.precision().is_some() || f.align().is_some() { f.pad(&ColoredStringDisplay(self).to_string()) - } else { - ColoredStringDisplay(self).fmt(f) - } + } else { + ColoredStringDisplay(self).fmt(f) + } } } From d540f3d968b0ac7f23560b1905270d43393dec67 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Wed, 26 Mar 2025 21:26:08 +0100 Subject: [PATCH 07/27] Change Helper Structs to Tuples --- src/lib.rs | 79 ++++++++++++++++++++++-------------------------------- 1 file changed, 32 insertions(+), 47 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index f88a716..69cc6ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -502,37 +502,31 @@ impl ColoredString { } #[derive(Debug)] -struct EscapeInnerResetSequencesHelper<'a> { - inner: &'a ColoredString, -} - -impl<'a> EscapeInnerResetSequencesHelper<'a> { - fn new(inner: &'a ColoredString) -> Self { - Self { inner } - } +struct EscapeInnerResetSequencesHelper<'a>(&'a ColoredString); +impl EscapeInnerResetSequencesHelper<'_> { fn private_fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { const RESET: &str = "\x1B[0m"; - if !ColoredString::has_colors() || self.inner.is_plain() { - return f.write_str(self.inner); + if !ColoredString::has_colors() || self.0.is_plain() { + return f.write_str(self.0); } let mut matches = self - .inner + .0 .match_indices(RESET) .map(|(idx, _)| idx + RESET.len()) .peekable(); if matches.peek().is_none() { - return f.write_str(self.inner); + return f.write_str(self.0); } let mut start = 0; for offset in matches { // shift the offset to the end of the reset sequence - f.write_str(&self.inner.input[start..offset])?; + f.write_str(&self.0.input[start..offset])?; start = offset; - ComputeStyleHelper::new(self.inner).private_fmt(f)?; + ComputeStyleHelper(self.0).private_fmt(f)?; } - f.write_str(&self.inner.input[start..])?; + f.write_str(&self.0.input[start..])?; Ok(()) } @@ -545,28 +539,22 @@ impl Display for EscapeInnerResetSequencesHelper<'_> { } #[derive(Debug)] -struct ComputeStyleHelper<'a> { - inner: &'a ColoredString, -} - -impl<'a> ComputeStyleHelper<'a> { - fn new(inner: &'a ColoredString) -> Self { - Self { inner } - } +struct ComputeStyleHelper<'a>(&'a ColoredString); +impl ComputeStyleHelper<'_> { fn private_fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - if !ColoredString::has_colors() || self.inner.is_plain() { + if !ColoredString::has_colors() || self.0.is_plain() { return Ok(()); } f.write_str("\x1B[")?; - let mut has_wrote = if self.inner.style == style::CLEAR { + let mut has_wrote = if self.0.style == style::CLEAR { false } else { - f.write_str(&self.inner.style.to_str())?; + f.write_str(&self.0.style.to_str())?; true }; - if let Some(ref bgcolor) = self.inner.bgcolor { + if let Some(ref bgcolor) = self.0.bgcolor { if has_wrote { f.write_char(';')?; } @@ -574,7 +562,7 @@ impl<'a> ComputeStyleHelper<'a> { has_wrote = true; } - if let Some(ref fgcolor) = self.inner.fgcolor { + if let Some(ref fgcolor) = self.0.fgcolor { if has_wrote { f.write_char(';')?; } @@ -744,8 +732,8 @@ impl Display for ColoredStringDisplay<'_> { } // XXX: see tests. Useful when nesting colored strings - ComputeStyleHelper::new(self.0).fmt(f)?; - EscapeInnerResetSequencesHelper::new(self.0).fmt(f)?; + ComputeStyleHelper(self.0).fmt(f)?; + EscapeInnerResetSequencesHelper(self.0).fmt(f)?; f.write_str("\x1B[0m")?; Ok(()) @@ -836,7 +824,7 @@ mod tests { #[test] fn compute_style_empty_string() { - assert_eq!("", ComputeStyleHelper::new(&"".clear()).to_string()); + assert_eq!("", ComputeStyleHelper(&"".clear()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -844,7 +832,7 @@ mod tests { fn compute_style_simple_fg_blue() { let blue = "\x1B[34m"; - assert_eq!(blue, ComputeStyleHelper::new(&"".blue()).to_string()); + assert_eq!(blue, ComputeStyleHelper(&"".blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -852,7 +840,7 @@ mod tests { fn compute_style_simple_bg_blue() { let on_blue = "\x1B[44m"; - assert_eq!(on_blue, ComputeStyleHelper::new(&"".on_blue()).to_string()); + assert_eq!(on_blue, ComputeStyleHelper(&"".on_blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -862,7 +850,7 @@ mod tests { assert_eq!( blue_on_blue, - ComputeStyleHelper::new(&"".blue().on_blue()).to_string() + ComputeStyleHelper(&"".blue().on_blue()).to_string() ); } @@ -871,7 +859,7 @@ mod tests { fn compute_style_simple_fg_bright_blue() { let blue = "\x1B[94m"; - assert_eq!(blue, ComputeStyleHelper::new(&"".bright_blue()).to_string()); + assert_eq!(blue, ComputeStyleHelper(&"".bright_blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -881,7 +869,7 @@ mod tests { assert_eq!( on_blue, - ComputeStyleHelper::new(&"".on_bright_blue()).to_string() + ComputeStyleHelper(&"".on_bright_blue()).to_string() ); } @@ -892,7 +880,7 @@ mod tests { assert_eq!( blue_on_blue, - ComputeStyleHelper::new(&"".bright_blue().on_bright_blue()).to_string() + ComputeStyleHelper(&"".bright_blue().on_bright_blue()).to_string() ); } @@ -901,7 +889,7 @@ mod tests { fn compute_style_simple_bold() { let bold = "\x1B[1m"; - assert_eq!(bold, ComputeStyleHelper::new(&"".bold()).to_string()); + assert_eq!(bold, ComputeStyleHelper(&"".bold()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -909,10 +897,7 @@ mod tests { fn compute_style_blue_bold() { let blue_bold = "\x1B[1;34m"; - assert_eq!( - blue_bold, - ComputeStyleHelper::new(&"".blue().bold()).to_string() - ); + assert_eq!(blue_bold, ComputeStyleHelper(&"".blue().bold()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -922,7 +907,7 @@ mod tests { assert_eq!( blue_bold_on_blue, - ComputeStyleHelper::new(&"".blue().bold().on_blue()).to_string() + ComputeStyleHelper(&"".blue().bold().on_blue()).to_string() ); } @@ -931,7 +916,7 @@ mod tests { let style = ColoredString::default(); let expected = String::new(); - let output = EscapeInnerResetSequencesHelper::new(&style).to_string(); + let output = EscapeInnerResetSequencesHelper(&style).to_string(); assert_eq!(expected, output); } @@ -944,7 +929,7 @@ mod tests { }; let expected = String::from("hello world !"); - let output = EscapeInnerResetSequencesHelper::new(&style).to_string(); + let output = EscapeInnerResetSequencesHelper(&style).to_string(); assert_eq!(expected, output); } @@ -955,7 +940,7 @@ mod tests { let input = format!("start {} end", String::from("hello world !").red()); let style = input.blue(); - let output = EscapeInnerResetSequencesHelper::new(&style).to_string(); + let output = EscapeInnerResetSequencesHelper(&style).to_string(); let blue = "\x1B[34m"; let red = "\x1B[31m"; let reset = "\x1B[0m"; @@ -971,7 +956,7 @@ mod tests { let input = format!("start 1:{italic_str} 2:{italic_str} 3:{italic_str} end"); let style = input.blue(); - let output = EscapeInnerResetSequencesHelper::new(&style).to_string(); + let output = EscapeInnerResetSequencesHelper(&style).to_string(); let blue = "\x1B[34m"; let italic = "\x1B[3m"; let reset = "\x1B[0m"; From b217771ad50450f877620cf6b59a8e0c52f39f4b Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 07:39:32 +0100 Subject: [PATCH 08/27] Padding without alloc --- src/lib.rs | 180 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 127 insertions(+), 53 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 69cc6ca..74851c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,7 +47,7 @@ pub use color::*; use core::fmt::{Display, Write}; use std::{ error::Error, - fmt, + fmt::{self}, ops::{Deref, DerefMut}, }; @@ -501,32 +501,48 @@ impl ColoredString { } } -#[derive(Debug)] -struct EscapeInnerResetSequencesHelper<'a>(&'a ColoredString); +#[derive(Debug, Clone, Copy)] +struct EscapeInnerResetSequencesHelper<'a> { + input: &'a str, + fgcolor: Option, + bgcolor: Option, + style: style::Style, + is_plain: bool, +} + +impl<'a> EscapeInnerResetSequencesHelper<'a> { + fn new(input: &'a str, color: &ColoredString) -> Self { + Self { + input, + fgcolor: color.fgcolor, + bgcolor: color.bgcolor, + style: color.style, + is_plain: color.is_plain(), + } + } -impl EscapeInnerResetSequencesHelper<'_> { fn private_fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { const RESET: &str = "\x1B[0m"; - if !ColoredString::has_colors() || self.0.is_plain() { - return f.write_str(self.0); + if !ColoredString::has_colors() || self.is_plain { + return f.write_str(self.input); } let mut matches = self - .0 + .input .match_indices(RESET) .map(|(idx, _)| idx + RESET.len()) .peekable(); if matches.peek().is_none() { - return f.write_str(self.0); + return f.write_str(self.input); } let mut start = 0; for offset in matches { // shift the offset to the end of the reset sequence - f.write_str(&self.0.input[start..offset])?; + f.write_str(&self.input[start..offset])?; start = offset; - ComputeStyleHelper(self.0).private_fmt(f)?; + ComputeStyleHelper::from(self).private_fmt(f)?; } - f.write_str(&self.0.input[start..])?; + f.write_str(&self.input[start..])?; Ok(()) } @@ -538,23 +554,54 @@ impl Display for EscapeInnerResetSequencesHelper<'_> { } } -#[derive(Debug)] -struct ComputeStyleHelper<'a>(&'a ColoredString); +#[derive(Debug, Clone, Copy)] +struct ComputeStyleHelper { + fgcolor: Option, + bgcolor: Option, + style: style::Style, + is_plain: bool, +} + +impl From<&ColoredString> for ComputeStyleHelper { + fn from(value: &ColoredString) -> Self { + Self::new(value) + } +} + +impl From<&EscapeInnerResetSequencesHelper<'_>> for ComputeStyleHelper { + fn from(value: &EscapeInnerResetSequencesHelper) -> Self { + Self { + fgcolor: value.fgcolor, + bgcolor: value.bgcolor, + style: value.style, + is_plain: value.is_plain, + } + } +} + +impl ComputeStyleHelper { + fn new(color: &ColoredString) -> Self { + Self { + fgcolor: color.fgcolor, + bgcolor: color.bgcolor, + style: color.style, + is_plain: color.is_plain(), + } + } -impl ComputeStyleHelper<'_> { fn private_fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - if !ColoredString::has_colors() || self.0.is_plain() { + if !ColoredString::has_colors() || self.is_plain { return Ok(()); } f.write_str("\x1B[")?; - let mut has_wrote = if self.0.style == style::CLEAR { + let mut has_wrote = if self.style == style::CLEAR { false } else { - f.write_str(&self.0.style.to_str())?; + f.write_str(&self.style.to_str())?; true }; - if let Some(ref bgcolor) = self.0.bgcolor { + if let Some(ref bgcolor) = self.bgcolor { if has_wrote { f.write_char(';')?; } @@ -562,7 +609,7 @@ impl ComputeStyleHelper<'_> { has_wrote = true; } - if let Some(ref fgcolor) = self.0.fgcolor { + if let Some(ref fgcolor) = self.fgcolor { if has_wrote { f.write_char(';')?; } @@ -574,7 +621,7 @@ impl ComputeStyleHelper<'_> { } } -impl Display for ComputeStyleHelper<'_> { +impl Display for ComputeStyleHelper { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.private_fmt(f) } @@ -723,32 +770,53 @@ impl Colorize for &str { } } -struct ColoredStringDisplay<'a>(&'a ColoredString); +impl fmt::Display for ColoredString { + #[allow(clippy::to_string_in_format_args)] + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if !Self::has_colors() || self.is_plain() { + return ::fmt(&self.input, f); + } + + let mut input = &*self.input; + let mut padding = 0; + + // Calculate padding and input + if let Some(precision) = f.precision() { + let mut iter = input.char_indices(); + + // FIXME + // feature(iter_advance_by) is not stable + // CharIndeces::offset is MSRV 1.82.0 + // the std impl uses this methods in it is a lot nicer. + let mut count = 0; + let mut offset = 0; + for _ in 0..precision { + if let Some((i, c)) = iter.next() { + offset = i + c.len_utf8(); + count += 1; + } + } -impl Display for ColoredStringDisplay<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if !ColoredString::has_colors() || self.0.is_plain() { - return ::fmt(&self.0.input, f); + input = &input[..offset]; + if let Some(width) = f.width() { + padding = width.saturating_sub(count); + } + } else if let Some(width) = f.width() { + padding = width.saturating_sub(self.chars().take(width).count()); } + // Do the actual formatting // XXX: see tests. Useful when nesting colored strings - ComputeStyleHelper(self.0).fmt(f)?; - EscapeInnerResetSequencesHelper(self.0).fmt(f)?; + ComputeStyleHelper::from(self).fmt(f)?; + EscapeInnerResetSequencesHelper::new(input, self).fmt(f)?; f.write_str("\x1B[0m")?; - Ok(()) - } -} - -impl fmt::Display for ColoredString { - #[allow(clippy::to_string_in_format_args)] - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - // FIXME, deal with padding and do not allocate for this - if f.width().is_some() || f.precision().is_some() || f.align().is_some() { - f.pad(&ColoredStringDisplay(self).to_string()) - } else { - ColoredStringDisplay(self).fmt(f) + // Add padding + for _ in 0..padding { + f.write_char(f.fill())?; } + + Ok(()) } } @@ -824,7 +892,7 @@ mod tests { #[test] fn compute_style_empty_string() { - assert_eq!("", ComputeStyleHelper(&"".clear()).to_string()); + assert_eq!("", ComputeStyleHelper::from(&"".clear()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -832,7 +900,7 @@ mod tests { fn compute_style_simple_fg_blue() { let blue = "\x1B[34m"; - assert_eq!(blue, ComputeStyleHelper(&"".blue()).to_string()); + assert_eq!(blue, ComputeStyleHelper::from(&"".blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -840,7 +908,7 @@ mod tests { fn compute_style_simple_bg_blue() { let on_blue = "\x1B[44m"; - assert_eq!(on_blue, ComputeStyleHelper(&"".on_blue()).to_string()); + assert_eq!(on_blue, ComputeStyleHelper::from(&"".on_blue()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -850,7 +918,7 @@ mod tests { assert_eq!( blue_on_blue, - ComputeStyleHelper(&"".blue().on_blue()).to_string() + ComputeStyleHelper::from(&"".blue().on_blue()).to_string() ); } @@ -859,7 +927,10 @@ mod tests { fn compute_style_simple_fg_bright_blue() { let blue = "\x1B[94m"; - assert_eq!(blue, ComputeStyleHelper(&"".bright_blue()).to_string()); + assert_eq!( + blue, + ComputeStyleHelper::from(&"".bright_blue()).to_string() + ); } #[cfg_attr(feature = "no-color", ignore)] @@ -869,7 +940,7 @@ mod tests { assert_eq!( on_blue, - ComputeStyleHelper(&"".on_bright_blue()).to_string() + ComputeStyleHelper::from(&"".on_bright_blue()).to_string() ); } @@ -880,7 +951,7 @@ mod tests { assert_eq!( blue_on_blue, - ComputeStyleHelper(&"".bright_blue().on_bright_blue()).to_string() + ComputeStyleHelper::from(&"".bright_blue().on_bright_blue()).to_string() ); } @@ -889,7 +960,7 @@ mod tests { fn compute_style_simple_bold() { let bold = "\x1B[1m"; - assert_eq!(bold, ComputeStyleHelper(&"".bold()).to_string()); + assert_eq!(bold, ComputeStyleHelper::from(&"".bold()).to_string()); } #[cfg_attr(feature = "no-color", ignore)] @@ -897,7 +968,10 @@ mod tests { fn compute_style_blue_bold() { let blue_bold = "\x1B[1;34m"; - assert_eq!(blue_bold, ComputeStyleHelper(&"".blue().bold()).to_string()); + assert_eq!( + blue_bold, + ComputeStyleHelper::from(&"".blue().bold()).to_string() + ); } #[cfg_attr(feature = "no-color", ignore)] @@ -907,7 +981,7 @@ mod tests { assert_eq!( blue_bold_on_blue, - ComputeStyleHelper(&"".blue().bold().on_blue()).to_string() + ComputeStyleHelper::from(&"".blue().bold().on_blue()).to_string() ); } @@ -916,7 +990,7 @@ mod tests { let style = ColoredString::default(); let expected = String::new(); - let output = EscapeInnerResetSequencesHelper(&style).to_string(); + let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); assert_eq!(expected, output); } @@ -929,7 +1003,7 @@ mod tests { }; let expected = String::from("hello world !"); - let output = EscapeInnerResetSequencesHelper(&style).to_string(); + let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); assert_eq!(expected, output); } @@ -940,7 +1014,7 @@ mod tests { let input = format!("start {} end", String::from("hello world !").red()); let style = input.blue(); - let output = EscapeInnerResetSequencesHelper(&style).to_string(); + let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); let blue = "\x1B[34m"; let red = "\x1B[31m"; let reset = "\x1B[0m"; @@ -956,7 +1030,7 @@ mod tests { let input = format!("start 1:{italic_str} 2:{italic_str} 3:{italic_str} end"); let style = input.blue(); - let output = EscapeInnerResetSequencesHelper(&style).to_string(); + let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); let blue = "\x1B[34m"; let italic = "\x1B[3m"; let reset = "\x1B[0m"; From 0ae0b78515f8028b3682dc84839819cc3556fa18 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 07:48:51 +0100 Subject: [PATCH 09/27] Enhance formatting tests --- src/lib.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 74851c3..ceb5dd9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -771,7 +771,6 @@ impl Colorize for &str { } impl fmt::Display for ColoredString { - #[allow(clippy::to_string_in_format_args)] fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if !Self::has_colors() || self.is_plain() { return ::fmt(&self.input, f); @@ -834,12 +833,61 @@ mod tests { #[test] fn formatting() { // respect the formatting. Escape sequence add some padding so >= 40 - assert!(format!("{:40}", "".blue()).len() >= 40); + assert_eq!( + format!("{:40}", "".blue()) + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 40 + ); + assert_eq!( + format!("{:2}", "CS".blue()) + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0 + ); + assert_eq!( + format!("{:1.2}", "CS".blue()) + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0 + ); + assert_eq!( + format!("{:1.2}", "CS".blue()) + .chars() + .filter(|c| *c == 'C' || *c == 'S') + .count(), + 2 + ); + assert_eq!( + format!("{:1.1}", "CS".blue()) + .chars() + .filter(|c| *c == 'C') + .count(), + 1 + ); + assert_eq!( + format!("{:1.1}", "CS".blue()) + .chars() + .filter(|c| *c == 'S') + .count(), + 0 + ); + // both should be truncated to 1 char before coloring assert_eq!( format!("{:1.1}", "toto".blue()).len(), format!("{:1.1}", "1".blue()).len() ); + + // Check handling of utf-8 characters + assert_ne!('πŸ¦€'.len_utf8(), 1); + let crab = format!("{:40.1}", "πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€".blue()); + assert_eq!(crab.chars().filter(|c| *c == 'πŸ¦€').count(), 1); + assert_eq!(crab.chars().filter(|c| c.is_whitespace()).count(), 39); + assert!(crab.len() >= 40); } #[test] From 57c53ea4ff057e3d73a1808f28a91f247af9026d Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 07:55:52 +0100 Subject: [PATCH 10/27] Enhance formatting tests --- src/lib.rs | 134 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index ceb5dd9..febc32e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -831,8 +831,11 @@ mod tests { use std::{error::Error, fmt::Write}; #[test] + #[allow(clippy::cognitive_complexity)] fn formatting() { // respect the formatting. Escape sequence add some padding so >= 40 + + // Only padding assert_eq!( format!("{:40}", "".blue()) .chars() @@ -847,6 +850,8 @@ mod tests { .count(), 0 ); + + // Padding with precision assert_eq!( format!("{:1.2}", "CS".blue()) .chars() @@ -861,6 +866,7 @@ mod tests { .count(), 2 ); + assert_eq!( format!("{:1.1}", "CS".blue()) .chars() @@ -875,6 +881,35 @@ mod tests { .count(), 0 ); + assert_eq!( + format!("{:1.1}", "CS".blue()) + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0 + ); + + assert_eq!( + format!("{:2.1}", "CS".blue()) + .chars() + .filter(|c| *c == 'C') + .count(), + 1 + ); + assert_eq!( + format!("{:2.1}", "CS".blue()) + .chars() + .filter(|c| *c == 'S') + .count(), + 0 + ); + assert_eq!( + format!("{:2.1}", "CS".blue()) + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 1 + ); // both should be truncated to 1 char before coloring assert_eq!( @@ -888,6 +923,105 @@ mod tests { assert_eq!(crab.chars().filter(|c| *c == 'πŸ¦€').count(), 1); assert_eq!(crab.chars().filter(|c| c.is_whitespace()).count(), 39); assert!(crab.len() >= 40); + let crab = format!("{:40.2}", "πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€".blue()); + assert_eq!(crab.chars().filter(|c| *c == 'πŸ¦€').count(), 2); + assert_eq!(crab.chars().filter(|c| c.is_whitespace()).count(), 38); + assert!(crab.len() >= 40); + + // Check equality to std string + + // Only padding + assert_eq!( + format!("{:40}", "") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 40 + ); + assert_eq!( + format!("{:2}", "CS") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0 + ); + + // Padding with precision + assert_eq!( + format!("{:1.2}", "CS") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0 + ); + assert_eq!( + format!("{:1.2}", "CS") + .chars() + .filter(|c| *c == 'C' || *c == 'S') + .count(), + 2 + ); + + assert_eq!( + format!("{:1.1}", "CS") + .chars() + .filter(|c| *c == 'C') + .count(), + 1 + ); + assert_eq!( + format!("{:1.1}", "CS") + .chars() + .filter(|c| *c == 'S') + .count(), + 0 + ); + assert_eq!( + format!("{:1.1}", "CS") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0 + ); + + assert_eq!( + format!("{:2.1}", "CS") + .chars() + .filter(|c| *c == 'C') + .count(), + 1 + ); + assert_eq!( + format!("{:2.1}", "CS") + .chars() + .filter(|c| *c == 'S') + .count(), + 0 + ); + assert_eq!( + format!("{:2.1}", "CS") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 1 + ); + + // both should be truncated to 1 char before coloring + assert_eq!( + format!("{:1.1}", "toto".blue()).len(), + format!("{:1.1}", "1".blue()).len() + ); + + // Check handling of utf-8 characters + assert_ne!('πŸ¦€'.len_utf8(), 1); + let crab = format!("{:40.1}", "πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€"); + assert_eq!(crab.chars().filter(|c| *c == 'πŸ¦€').count(), 1); + assert_eq!(crab.chars().filter(|c| c.is_whitespace()).count(), 39); + assert!(crab.len() >= 40); + let crab = format!("{:40.2}", "πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€"); + assert_eq!(crab.chars().filter(|c| *c == 'πŸ¦€').count(), 2); + assert_eq!(crab.chars().filter(|c| c.is_whitespace()).count(), 38); + assert!(crab.len() >= 40); } #[test] From ecd63550afd8f42347d67266d1a7e29000cab491 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 08:02:21 +0100 Subject: [PATCH 11/27] Cleanup --- src/lib.rs | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index febc32e..70b2e92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -520,9 +520,12 @@ impl<'a> EscapeInnerResetSequencesHelper<'a> { is_plain: color.is_plain(), } } +} - fn private_fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { +impl Display for EscapeInnerResetSequencesHelper<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { const RESET: &str = "\x1B[0m"; + if !ColoredString::has_colors() || self.is_plain { return f.write_str(self.input); } @@ -540,7 +543,7 @@ impl<'a> EscapeInnerResetSequencesHelper<'a> { f.write_str(&self.input[start..offset])?; start = offset; - ComputeStyleHelper::from(self).private_fmt(f)?; + ComputeStyleHelper::from(self).fmt(f)?; } f.write_str(&self.input[start..])?; @@ -548,12 +551,6 @@ impl<'a> EscapeInnerResetSequencesHelper<'a> { } } -impl Display for EscapeInnerResetSequencesHelper<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.private_fmt(f) - } -} - #[derive(Debug, Clone, Copy)] struct ComputeStyleHelper { fgcolor: Option, @@ -588,8 +585,10 @@ impl ComputeStyleHelper { is_plain: color.is_plain(), } } +} - fn private_fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { +impl Display for ComputeStyleHelper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if !ColoredString::has_colors() || self.is_plain { return Ok(()); } @@ -621,12 +620,6 @@ impl ComputeStyleHelper { } } -impl Display for ComputeStyleHelper { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.private_fmt(f) - } -} - impl Deref for ColoredString { type Target = str; fn deref(&self) -> &Self::Target { From 098fa4f2ba31d23ad1d5bc7366ea7607d203ff3e Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 08:19:59 +0100 Subject: [PATCH 12/27] Remove duplicate code --- src/color.rs | 155 ++++++++++++++++++++++----------------------------- 1 file changed, 67 insertions(+), 88 deletions(-) diff --git a/src/color.rs b/src/color.rs index c835738..3a521c9 100644 --- a/src/color.rs +++ b/src/color.rs @@ -33,109 +33,88 @@ fn truecolor_support() -> bool { #[allow(missing_docs)] impl Color { - pub(crate) fn to_fg_fmt(self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + const fn to_fg_static_str(self) -> Result<&'static str, (u8, u8, u8)> { match self { - Self::Black => f.write_str("30"), - Self::Red => f.write_str("31"), - Self::Green => f.write_str("32"), - Self::Yellow => f.write_str("33"), - Self::Blue => f.write_str("34"), - Self::Magenta => f.write_str("35"), - Self::Cyan => f.write_str("36"), - Self::White => f.write_str("37"), - Self::BrightBlack => f.write_str("90"), - Self::BrightRed => f.write_str("91"), - Self::BrightGreen => f.write_str("92"), - Self::BrightYellow => f.write_str("93"), - Self::BrightBlue => f.write_str("94"), - Self::BrightMagenta => f.write_str("95"), - Self::BrightCyan => f.write_str("96"), - Self::BrightWhite => f.write_str("97"), - Self::TrueColor { .. } if !truecolor_support() => { - self.closest_color_euclidean().to_fg_fmt(f) - } - Self::TrueColor { r, g, b } => { - write!(f, "38;2;{r};{g};{b}") - } + Self::Black => Ok("30"), + Self::Red => Ok("31"), + Self::Green => Ok("32"), + Self::Yellow => Ok("33"), + Self::Blue => Ok("34"), + Self::Magenta => Ok("35"), + Self::Cyan => Ok("36"), + Self::White => Ok("37"), + Self::BrightBlack => Ok("90"), + Self::BrightRed => Ok("91"), + Self::BrightGreen => Ok("92"), + Self::BrightYellow => Ok("93"), + Self::BrightBlue => Ok("94"), + Self::BrightMagenta => Ok("95"), + Self::BrightCyan => Ok("96"), + Self::BrightWhite => Ok("97"), + Self::TrueColor { r, g, b } => Err((r, g, b)), + } + } + + pub(crate) fn to_fg_fmt(self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { + match self.to_fg_static_str() { + Ok(s) => f.write_str(s), + Err((r, g, b)) if !truecolor_support() => Self::TrueColor { r, g, b } + .closest_color_euclidean() + .to_fg_fmt(f), + Err((r, g, b)) => write!(f, "38;2;{r};{g};{b}"), } } #[must_use] pub fn to_fg_str(&self) -> Cow<'static, str> { - match *self { - Self::Black => "30".into(), - Self::Red => "31".into(), - Self::Green => "32".into(), - Self::Yellow => "33".into(), - Self::Blue => "34".into(), - Self::Magenta => "35".into(), - Self::Cyan => "36".into(), - Self::White => "37".into(), - Self::BrightBlack => "90".into(), - Self::BrightRed => "91".into(), - Self::BrightGreen => "92".into(), - Self::BrightYellow => "93".into(), - Self::BrightBlue => "94".into(), - Self::BrightMagenta => "95".into(), - Self::BrightCyan => "96".into(), - Self::BrightWhite => "97".into(), - Self::TrueColor { .. } if !truecolor_support() => { - self.closest_color_euclidean().to_fg_str() - } - Self::TrueColor { r, g, b } => format!("38;2;{r};{g};{b}").into(), + match self.to_fg_static_str() { + Ok(s) => s.into(), + Err((r, g, b)) if !truecolor_support() => Self::TrueColor { r, g, b } + .closest_color_euclidean() + .to_fg_str(), + Err((r, g, b)) => format!("38;2;{r};{g};{b}").into(), + } + } + const fn to_bg_static_str(self) -> Result<&'static str, (u8, u8, u8)> { + match self { + Self::Black => Ok("40"), + Self::Red => Ok("41"), + Self::Green => Ok("42"), + Self::Yellow => Ok("43"), + Self::Blue => Ok("44"), + Self::Magenta => Ok("45"), + Self::Cyan => Ok("46"), + Self::White => Ok("47"), + Self::BrightBlack => Ok("100"), + Self::BrightRed => Ok("101"), + Self::BrightGreen => Ok("102"), + Self::BrightYellow => Ok("103"), + Self::BrightBlue => Ok("104"), + Self::BrightMagenta => Ok("105"), + Self::BrightCyan => Ok("106"), + Self::BrightWhite => Ok("107"), + Self::TrueColor { r, g, b } => Err((r, g, b)), } } pub(crate) fn to_bg_fmt(self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> { - match self { - Self::Black => f.write_str("40"), - Self::Red => f.write_str("41"), - Self::Green => f.write_str("42"), - Self::Yellow => f.write_str("43"), - Self::Blue => f.write_str("44"), - Self::Magenta => f.write_str("45"), - Self::Cyan => f.write_str("46"), - Self::White => f.write_str("47"), - Self::BrightBlack => f.write_str("100"), - Self::BrightRed => f.write_str("101"), - Self::BrightGreen => f.write_str("102"), - Self::BrightYellow => f.write_str("103"), - Self::BrightBlue => f.write_str("104"), - Self::BrightMagenta => f.write_str("105"), - Self::BrightCyan => f.write_str("106"), - Self::BrightWhite => f.write_str("107"), - Self::TrueColor { .. } if !truecolor_support() => { - self.closest_color_euclidean().to_bg_fmt(f) - } - Self::TrueColor { r, g, b } => { - write!(f, "48;2;{r};{g};{b}") - } + match self.to_bg_static_str() { + Ok(s) => f.write_str(s), + Err((r, g, b)) if !truecolor_support() => Self::TrueColor { r, g, b } + .closest_color_euclidean() + .to_fg_fmt(f), + Err((r, g, b)) => write!(f, "48;2;{r};{g};{b}"), } } #[must_use] pub fn to_bg_str(&self) -> Cow<'static, str> { - match *self { - Self::Black => "40".into(), - Self::Red => "41".into(), - Self::Green => "42".into(), - Self::Yellow => "43".into(), - Self::Blue => "44".into(), - Self::Magenta => "45".into(), - Self::Cyan => "46".into(), - Self::White => "47".into(), - Self::BrightBlack => "100".into(), - Self::BrightRed => "101".into(), - Self::BrightGreen => "102".into(), - Self::BrightYellow => "103".into(), - Self::BrightBlue => "104".into(), - Self::BrightMagenta => "105".into(), - Self::BrightCyan => "106".into(), - Self::BrightWhite => "107".into(), - Self::TrueColor { .. } if !truecolor_support() => { - self.closest_color_euclidean().to_bg_str() - } - Self::TrueColor { r, g, b } => format!("48;2;{r};{g};{b}").into(), + match self.to_bg_static_str() { + Ok(s) => s.into(), + Err((r, g, b)) if !truecolor_support() => Self::TrueColor { r, g, b } + .closest_color_euclidean() + .to_fg_str(), + Err((r, g, b)) => format!("48;2;{r};{g};{b}").into(), } } From 34eaf1a159541a78f5d99cb4bdb3b6b865de8fde Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 08:22:44 +0100 Subject: [PATCH 13/27] Remove alloc in closest_color_euclidean --- src/color.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/color.rs b/src/color.rs index 3a521c9..94e7e72 100644 --- a/src/color.rs +++ b/src/color.rs @@ -126,7 +126,7 @@ impl Color { g: g1, b: b1, } => { - let colors = vec![ + let colors = [ Black, Red, Green, From baf9453004b6e4598ac0eae94e8c13c1689fc663 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 08:27:19 +0100 Subject: [PATCH 14/27] Replace min_by with min_by_key --- src/color.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/color.rs b/src/color.rs index 94e7e72..98b2d7d 100644 --- a/src/color.rs +++ b/src/color.rs @@ -160,7 +160,7 @@ impl Color { unimplemented!("{:?} not a TrueColor", c) } }); - distances.min_by(|(_, d1), (_, d2)| d1.cmp(d2)).unwrap().0 + distances.min_by_key(|(_, distance)| *distance).unwrap().0 } c => c, } From 1e04c4d778fe5266cf5718326bb987bf860f6522 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 08:42:21 +0100 Subject: [PATCH 15/27] Add comments --- src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 70b2e92..bb4eb4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -537,14 +537,17 @@ impl Display for EscapeInnerResetSequencesHelper<'_> { if matches.peek().is_none() { return f.write_str(self.input); } + let mut start = 0; for offset in matches { - // shift the offset to the end of the reset sequence - + // Write the text up to the end reset sequence f.write_str(&self.input[start..offset])?; + // Remember where the next text starts start = offset; + // Write style ComputeStyleHelper::from(self).fmt(f)?; } + // Write rest f.write_str(&self.input[start..])?; Ok(()) From 962a9ed83ff6c6b108cfb00ed8227c05b6702612 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:07:51 +0100 Subject: [PATCH 16/27] Refactor distance calculation in color comparison --- src/color.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/color.rs b/src/color.rs index 98b2d7d..2c0df51 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,4 +1,4 @@ -use std::{borrow::Cow, cmp, env, str::FromStr}; +use std::{borrow::Cow, env, str::FromStr}; use Color::{ Black, Blue, BrightBlack, BrightBlue, BrightCyan, BrightGreen, BrightMagenta, BrightRed, BrightWhite, BrightYellow, Cyan, Green, Magenta, Red, TrueColor, White, Yellow, @@ -148,13 +148,10 @@ impl Color { .map(|c| (c, c.into_truecolor())); let distances = colors.map(|(c_original, c)| { if let TrueColor { r, g, b } = c { - let rd = cmp::max(r, r1) - cmp::min(r, r1); - let gd = cmp::max(g, g1) - cmp::min(g, g1); - let bd = cmp::max(b, b1) - cmp::min(b, b1); - let rd: u32 = rd.into(); - let gd: u32 = gd.into(); - let bd: u32 = bd.into(); - let distance = rd.pow(2) + gd.pow(2) + bd.pow(2); + fn distance(a: u8, b: u8) -> u32 { + u32::from(a.abs_diff(b)).pow(2) + } + let distance = distance(r, r1) + distance(g, g1) + distance(b, b1); (c_original, distance) } else { unimplemented!("{:?} not a TrueColor", c) From d5cd4209876ffe8cff0281cbe2d6b872277f0df5 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:08:15 +0100 Subject: [PATCH 17/27] Add tests to check if fmt and to_str are equal --- src/color.rs | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/color.rs b/src/color.rs index 2c0df51..15fc061 100644 --- a/src/color.rs +++ b/src/color.rs @@ -288,8 +288,82 @@ fn parse_hex(s: &str) -> Option { #[cfg(test)] mod tests { + use core::fmt::Display; + pub use super::*; + struct FmtFgWrapper(Color); + + impl Display for FmtFgWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.to_fg_fmt(f) + } + } + struct FmtBgWrapper(Color); + + impl Display for FmtBgWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.to_bg_fmt(f) + } + } + + #[test] + fn fmt_and_to_str_same() { + use Color::*; + let colors = &[ + Black, + Red, + Green, + Yellow, + Blue, + Magenta, + Cyan, + White, + BrightBlack, + BrightRed, + BrightGreen, + BrightYellow, + BrightBlue, + BrightMagenta, + BrightCyan, + BrightWhite, + TrueColor { r: 0, g: 0, b: 0 }, + TrueColor { + r: 255, + g: 255, + b: 255, + }, + TrueColor { + r: 126, + g: 127, + b: 128, + }, + TrueColor { r: 255, g: 0, b: 0 }, + TrueColor { + r: 255, + g: 255, + b: 0, + }, + TrueColor { r: 0, g: 255, b: 0 }, + TrueColor { + r: 0, + g: 255, + b: 255, + }, + TrueColor { r: 0, g: 0, b: 255 }, + TrueColor { + r: 255, + g: 0, + b: 255, + }, + ]; + + for color in colors { + assert_eq!(color.to_fg_str(), FmtFgWrapper(*color).to_string()); + assert_eq!(color.to_bg_str(), FmtBgWrapper(*color).to_string()); + } + } + mod from_str { pub use super::*; From 3e7932b1826155e7984d605db91eb0f9e4dda89e Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:42:46 +0100 Subject: [PATCH 18/27] Split up formatting test and added some --- src/lib.rs | 489 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 303 insertions(+), 186 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index bb4eb4a..9353f54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -826,198 +826,315 @@ mod tests { use super::*; use std::{error::Error, fmt::Write}; - #[test] - #[allow(clippy::cognitive_complexity)] - fn formatting() { - // respect the formatting. Escape sequence add some padding so >= 40 - - // Only padding - assert_eq!( - format!("{:40}", "".blue()) - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 40 - ); - assert_eq!( - format!("{:2}", "CS".blue()) - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0 - ); - - // Padding with precision - assert_eq!( - format!("{:1.2}", "CS".blue()) - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0 - ); - assert_eq!( - format!("{:1.2}", "CS".blue()) - .chars() - .filter(|c| *c == 'C' || *c == 'S') - .count(), - 2 - ); - - assert_eq!( - format!("{:1.1}", "CS".blue()) - .chars() - .filter(|c| *c == 'C') - .count(), - 1 - ); - assert_eq!( - format!("{:1.1}", "CS".blue()) - .chars() - .filter(|c| *c == 'S') - .count(), - 0 - ); - assert_eq!( - format!("{:1.1}", "CS".blue()) - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0 - ); - - assert_eq!( - format!("{:2.1}", "CS".blue()) - .chars() - .filter(|c| *c == 'C') - .count(), - 1 - ); - assert_eq!( - format!("{:2.1}", "CS".blue()) - .chars() - .filter(|c| *c == 'S') - .count(), - 0 - ); - assert_eq!( - format!("{:2.1}", "CS".blue()) - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 1 - ); - - // both should be truncated to 1 char before coloring - assert_eq!( - format!("{:1.1}", "toto".blue()).len(), - format!("{:1.1}", "1".blue()).len() - ); - - // Check handling of utf-8 characters - assert_ne!('πŸ¦€'.len_utf8(), 1); - let crab = format!("{:40.1}", "πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€".blue()); - assert_eq!(crab.chars().filter(|c| *c == 'πŸ¦€').count(), 1); - assert_eq!(crab.chars().filter(|c| c.is_whitespace()).count(), 39); - assert!(crab.len() >= 40); - let crab = format!("{:40.2}", "πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€".blue()); - assert_eq!(crab.chars().filter(|c| *c == 'πŸ¦€').count(), 2); - assert_eq!(crab.chars().filter(|c| c.is_whitespace()).count(), 38); - assert!(crab.len() >= 40); - - // Check equality to std string + /// Test the formatting + /// + /// Formatting *must* respect padding + /// + /// The added normal str in the input for comparison to rust-std + mod formmating { + use super::*; + + #[test] + fn empty_padding() { + let inputs: &[&dyn Display] = &[&"".custom_color((126, 194, 218)), &"".blue(), &""]; + + for input in inputs { + assert_eq!( + format!("{input:40}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 40 + ); + } + } + #[test] + fn no_padding() { + let inputs: &[&dyn Display] = &[&"".custom_color((126, 194, 218)), &"".blue(), &""]; + for input in inputs { + assert_eq!( + format!("{input}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0 + ); + } + } - // Only padding - assert_eq!( - format!("{:40}", "") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 40 - ); - assert_eq!( - format!("{:2}", "CS") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0 - ); + #[test] + fn not_enough_for_padding() { + let inputs: &[&dyn Display] = + &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CS".blue(), &"CS"]; + for input in inputs { + assert_eq!( + format!("{input:0}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0 + ); + } + for input in inputs { + assert_eq!( + format!("{input:1}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0 + ); + } + for input in inputs { + assert_eq!( + format!("{input:2}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0 + ); + } + } - // Padding with precision - assert_eq!( - format!("{:1.2}", "CS") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0 - ); - assert_eq!( - format!("{:1.2}", "CS") - .chars() - .filter(|c| *c == 'C' || *c == 'S') - .count(), - 2 - ); + #[test] + fn padding_with_input() { + let inputs: &[&dyn Display] = + &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CS".blue(), &"CS"]; + for input in inputs { + assert_eq!( + format!("{input:3}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 1 + ); + assert!(format!("{input:3}").contains(&input.to_string())); + + assert_eq!( + format!("{input:4}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 2 + ); + assert!(format!("{input:4}").contains(&input.to_string())); + } + } + } - assert_eq!( - format!("{:1.1}", "CS") - .chars() - .filter(|c| *c == 'C') - .count(), - 1 - ); - assert_eq!( - format!("{:1.1}", "CS") - .chars() - .filter(|c| *c == 'S') - .count(), - 0 - ); - assert_eq!( - format!("{:1.1}", "CS") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0 - ); + #[test] + fn precision_less() { + let inputs: &[&dyn Display] = &[ + &"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), + &"CC".blue(), + &"CC", + &"ColoredString".blue(), + ]; + for input in inputs { + assert_eq!( + format!("{input:.1}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 1, + "input: {input}" + ); + assert!(!format!("{input:.1}").contains(&input.to_string())); + } + } - assert_eq!( - format!("{:2.1}", "CS") - .chars() - .filter(|c| *c == 'C') - .count(), - 1 - ); - assert_eq!( - format!("{:2.1}", "CS") - .chars() - .filter(|c| *c == 'S') - .count(), - 0 - ); - assert_eq!( - format!("{:2.1}", "CS") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 1 - ); + #[test] + fn precision_eq() { + let inputs: &[&dyn Display] = &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CC".blue(), &"CC"]; + for input in inputs { + assert_eq!( + format!("{input:.2}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert!(format!("{input:.2}").contains(&input.to_string())); + } + } - // both should be truncated to 1 char before coloring - assert_eq!( - format!("{:1.1}", "toto".blue()).len(), - format!("{:1.1}", "1".blue()).len() - ); + #[test] + fn precision_more() { + let inputs: &[&dyn Display] = &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CC".blue(), &"CC"]; + for input in inputs { + assert_eq!( + format!("{input:.100}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert!(format!("{input:.100}").contains(&input.to_string())); + } + } - // Check handling of utf-8 characters - assert_ne!('πŸ¦€'.len_utf8(), 1); - let crab = format!("{:40.1}", "πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€"); - assert_eq!(crab.chars().filter(|c| *c == 'πŸ¦€').count(), 1); - assert_eq!(crab.chars().filter(|c| c.is_whitespace()).count(), 39); - assert!(crab.len() >= 40); - let crab = format!("{:40.2}", "πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€πŸ¦€"); - assert_eq!(crab.chars().filter(|c| *c == 'πŸ¦€').count(), 2); - assert_eq!(crab.chars().filter(|c| c.is_whitespace()).count(), 38); - assert!(crab.len() >= 40); + #[test] + fn precision_padding() { + let inputs: &[&dyn Display] = &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CC".blue(), &"CC"]; + for input in inputs { + // precision less, padding more + assert_eq!( + format!("{input:40.1}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 1, + "input: {input}" + ); + assert_eq!( + format!("{input:40.1}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 39, + "input: {input}" + ); + // precision less, padding less_or_equal + assert_eq!( + format!("{input:1.1}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 1, + "input: {input}" + ); + assert_eq!( + format!("{input:1.1}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert_eq!( + format!("{input:0.1}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 1, + "input: {input}" + ); + assert_eq!( + format!("{input:0.1}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert!(format!("{input:.100}").contains(&input.to_string())); + // precision eq, padding more + assert_eq!( + format!("{input:40.2}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:40.2}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 38, + "input: {input}" + ); + // precision eq, padding less_or_equal + assert_eq!( + format!("{input:1.2}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:1.2}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert!(format!("{input:1.2}").contains(&input.to_string())); + assert_eq!( + format!("{input:0.2}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:0.2}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert!(format!("{input:0.2}").contains(&input.to_string())); + + // precision more, padding more + assert_eq!( + format!("{input:40.4}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:40.4}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 38, + "input: {input}" + ); + // precision eq, padding less_or_equal + assert_eq!( + format!("{input:1.4}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:1.4}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert!(format!("{input:1.4}").contains(&input.to_string())); + assert_eq!( + format!("{input:0.4}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:0.4}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert!(format!("{input:0.4}").contains(&input.to_string())); + } } #[test] From d2d4d934349b8cf9897d5b0e80662361299a5b64 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:46:14 +0100 Subject: [PATCH 19/27] Cleanup --- src/color.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/color.rs b/src/color.rs index 15fc061..e14d6bb 100644 --- a/src/color.rs +++ b/src/color.rs @@ -288,28 +288,29 @@ fn parse_hex(s: &str) -> Option { #[cfg(test)] mod tests { - use core::fmt::Display; pub use super::*; - struct FmtFgWrapper(Color); + #[test] + fn fmt_and_to_str_same() { + use core::fmt::Display; + use Color::*; - impl Display for FmtFgWrapper { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.to_fg_fmt(f) + // Helper structs to call the method + struct FmtFgWrapper(Color); + impl Display for FmtFgWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.to_fg_fmt(f) + } } - } - struct FmtBgWrapper(Color); - - impl Display for FmtBgWrapper { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.to_bg_fmt(f) + struct FmtBgWrapper(Color); + impl Display for FmtBgWrapper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.to_bg_fmt(f) + } } - } - #[test] - fn fmt_and_to_str_same() { - use Color::*; + // Actual test let colors = &[ Black, Red, From 383397ea0f3b37b737eed194f844c1e0d97ba0ba Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:48:52 +0100 Subject: [PATCH 20/27] Move tests to their modul --- src/lib.rs | 419 +++++++++++++++++++++++++++-------------------------- 1 file changed, 211 insertions(+), 208 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9353f54..562790e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -919,221 +919,224 @@ mod tests { assert!(format!("{input:4}").contains(&input.to_string())); } } - } - #[test] - fn precision_less() { - let inputs: &[&dyn Display] = &[ - &"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), - &"CC".blue(), - &"CC", - &"ColoredString".blue(), - ]; - for input in inputs { - assert_eq!( - format!("{input:.1}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 1, - "input: {input}" - ); - assert!(!format!("{input:.1}").contains(&input.to_string())); + #[test] + fn precision_less() { + let inputs: &[&dyn Display] = &[ + &"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), + &"CC".blue(), + &"CC", + &"ColoredString".blue(), + ]; + for input in inputs { + assert_eq!( + format!("{input:.1}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 1, + "input: {input}" + ); + assert!(!format!("{input:.1}").contains(&input.to_string())); + } } - } - #[test] - fn precision_eq() { - let inputs: &[&dyn Display] = &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CC".blue(), &"CC"]; - for input in inputs { - assert_eq!( - format!("{input:.2}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 2, - "input: {input}" - ); - assert!(format!("{input:.2}").contains(&input.to_string())); + #[test] + fn precision_eq() { + let inputs: &[&dyn Display] = + &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CC".blue(), &"CC"]; + for input in inputs { + assert_eq!( + format!("{input:.2}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert!(format!("{input:.2}").contains(&input.to_string())); + } } - } - #[test] - fn precision_more() { - let inputs: &[&dyn Display] = &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CC".blue(), &"CC"]; - for input in inputs { - assert_eq!( - format!("{input:.100}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 2, - "input: {input}" - ); - assert!(format!("{input:.100}").contains(&input.to_string())); + #[test] + fn precision_more() { + let inputs: &[&dyn Display] = + &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CC".blue(), &"CC"]; + for input in inputs { + assert_eq!( + format!("{input:.100}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert!(format!("{input:.100}").contains(&input.to_string())); + } } - } - #[test] - fn precision_padding() { - let inputs: &[&dyn Display] = &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CC".blue(), &"CC"]; - for input in inputs { - // precision less, padding more - assert_eq!( - format!("{input:40.1}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 1, - "input: {input}" - ); - assert_eq!( - format!("{input:40.1}") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 39, - "input: {input}" - ); - // precision less, padding less_or_equal - assert_eq!( - format!("{input:1.1}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 1, - "input: {input}" - ); - assert_eq!( - format!("{input:1.1}") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0, - "input: {input}" - ); - assert_eq!( - format!("{input:0.1}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 1, - "input: {input}" - ); - assert_eq!( - format!("{input:0.1}") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0, - "input: {input}" - ); - assert!(format!("{input:.100}").contains(&input.to_string())); - // precision eq, padding more - assert_eq!( - format!("{input:40.2}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 2, - "input: {input}" - ); - assert_eq!( - format!("{input:40.2}") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 38, - "input: {input}" - ); - // precision eq, padding less_or_equal - assert_eq!( - format!("{input:1.2}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 2, - "input: {input}" - ); - assert_eq!( - format!("{input:1.2}") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0, - "input: {input}" - ); - assert!(format!("{input:1.2}").contains(&input.to_string())); - assert_eq!( - format!("{input:0.2}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 2, - "input: {input}" - ); - assert_eq!( - format!("{input:0.2}") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0, - "input: {input}" - ); - assert!(format!("{input:0.2}").contains(&input.to_string())); - - // precision more, padding more - assert_eq!( - format!("{input:40.4}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 2, - "input: {input}" - ); - assert_eq!( - format!("{input:40.4}") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 38, - "input: {input}" - ); - // precision eq, padding less_or_equal - assert_eq!( - format!("{input:1.4}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 2, - "input: {input}" - ); - assert_eq!( - format!("{input:1.4}") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0, - "input: {input}" - ); - assert!(format!("{input:1.4}").contains(&input.to_string())); - assert_eq!( - format!("{input:0.4}") - .chars() - .filter(|c| *c == 'πŸ¦€' || *c == 'C') - .count(), - 2, - "input: {input}" - ); - assert_eq!( - format!("{input:0.4}") - .chars() - .filter(|c| c.is_whitespace()) - .count(), - 0, - "input: {input}" - ); - assert!(format!("{input:0.4}").contains(&input.to_string())); + #[test] + fn precision_padding() { + let inputs: &[&dyn Display] = + &[&"πŸ¦€πŸ¦€".custom_color((126, 194, 218)), &"CC".blue(), &"CC"]; + for input in inputs { + // precision less, padding more + assert_eq!( + format!("{input:40.1}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 1, + "input: {input}" + ); + assert_eq!( + format!("{input:40.1}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 39, + "input: {input}" + ); + // precision less, padding less_or_equal + assert_eq!( + format!("{input:1.1}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 1, + "input: {input}" + ); + assert_eq!( + format!("{input:1.1}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert_eq!( + format!("{input:0.1}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 1, + "input: {input}" + ); + assert_eq!( + format!("{input:0.1}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert!(format!("{input:.100}").contains(&input.to_string())); + // precision eq, padding more + assert_eq!( + format!("{input:40.2}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:40.2}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 38, + "input: {input}" + ); + // precision eq, padding less_or_equal + assert_eq!( + format!("{input:1.2}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:1.2}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert!(format!("{input:1.2}").contains(&input.to_string())); + assert_eq!( + format!("{input:0.2}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:0.2}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert!(format!("{input:0.2}").contains(&input.to_string())); + + // precision more, padding more + assert_eq!( + format!("{input:40.4}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:40.4}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 38, + "input: {input}" + ); + // precision eq, padding less_or_equal + assert_eq!( + format!("{input:1.4}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:1.4}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert!(format!("{input:1.4}").contains(&input.to_string())); + assert_eq!( + format!("{input:0.4}") + .chars() + .filter(|c| *c == 'πŸ¦€' || *c == 'C') + .count(), + 2, + "input: {input}" + ); + assert_eq!( + format!("{input:0.4}") + .chars() + .filter(|c| c.is_whitespace()) + .count(), + 0, + "input: {input}" + ); + assert!(format!("{input:0.4}").contains(&input.to_string())); + } } } From 362f66749572d147c0b1c5ccc4ad01c1dd8a87e7 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:50:03 +0100 Subject: [PATCH 21/27] Typo --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 562790e..3fca551 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -830,7 +830,7 @@ mod tests { /// /// Formatting *must* respect padding /// - /// The added normal str in the input for comparison to rust-std + /// The added normal str in the input is for comparison to rust-std mod formmating { use super::*; From 2cd3492acf292442a78d85c3143893e1a465c776 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Fri, 28 Mar 2025 10:25:00 +0100 Subject: [PATCH 22/27] Enhance formatting, escape and color_fn tests --- src/lib.rs | 476 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 326 insertions(+), 150 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3fca551..7851fa1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1188,177 +1188,353 @@ mod tests { Ok(()) } - #[test] - fn compute_style_empty_string() { - assert_eq!("", ComputeStyleHelper::from(&"".clear()).to_string()); - } - - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn compute_style_simple_fg_blue() { - let blue = "\x1B[34m"; - - assert_eq!(blue, ComputeStyleHelper::from(&"".blue()).to_string()); - } - - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn compute_style_simple_bg_blue() { - let on_blue = "\x1B[44m"; - - assert_eq!(on_blue, ComputeStyleHelper::from(&"".on_blue()).to_string()); - } - - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn compute_style_blue_on_blue() { - let blue_on_blue = "\x1B[44;34m"; - - assert_eq!( - blue_on_blue, - ComputeStyleHelper::from(&"".blue().on_blue()).to_string() - ); - } + mod compute_style { + use crate::{ColoredString, Colorize, ComputeStyleHelper}; - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn compute_style_simple_fg_bright_blue() { - let blue = "\x1B[94m"; - - assert_eq!( - blue, - ComputeStyleHelper::from(&"".bright_blue()).to_string() - ); - } - - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn compute_style_simple_bg_bright_blue() { - let on_blue = "\x1B[104m"; - - assert_eq!( - on_blue, - ComputeStyleHelper::from(&"".on_bright_blue()).to_string() - ); - } - - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn compute_style_bright_blue_on_bright_blue() { - let blue_on_blue = "\x1B[104;94m"; - - assert_eq!( - blue_on_blue, - ComputeStyleHelper::from(&"".bright_blue().on_bright_blue()).to_string() - ); - } - - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn compute_style_simple_bold() { - let bold = "\x1B[1m"; + /// Into is not dyn compatible + trait IntoCS { + fn into(&self) -> ColoredString; + } + impl IntoCS for &str { + fn into(&self) -> ColoredString { + ColoredString::from(*self) + } + } + impl IntoCS for ColoredString { + fn into(&self) -> ColoredString { + self.clone() + } + } - assert_eq!(bold, ComputeStyleHelper::from(&"".bold()).to_string()); - } + /// Helper to use a Colorize function on str or `ColoredString` + type ColorMapper = &'static dyn Fn(&dyn IntoCS) -> ColoredString; + macro_rules! helper_color { + ($f:ident, $r:expr) => { + (&(|i| Colorize::$f(i.into())), $r) + }; + } + /// Helper to use a truecolor method of Colorize on str or `ColoredString` + macro_rules! helper_truecolor { + ($f:ident, $r:expr, $g:expr,$b:expr,$res:expr) => { + ( + &(|i| Colorize::$f(i.into(), $r, $g, $b)), + concat!( + $res, + stringify!($r), + ";", + stringify!($g), + ";", + stringify!($b) + ), + ) + }; + } + /// The input of a `ColoredString` + const INPUTS: &[&str; 5] = &["πŸ¦€πŸ¦€", "CS", "CC", "ColoredString", "πŸ¦€ColoredStringπŸ¦€CC"]; + /// All background function mappings to their expected result + const BG_MAPPINGS: &[(ColorMapper, &str)] = &[ + helper_color!(on_black, "40"), + helper_color!(on_red, "41"), + helper_color!(on_green, "42"), + helper_color!(on_yellow, "43"), + helper_color!(on_blue, "44"), + helper_color!(on_magenta, "45"), + helper_color!(on_cyan, "46"), + helper_color!(on_white, "47"), + helper_color!(on_bright_black, "100"), + helper_color!(on_bright_red, "101"), + helper_color!(on_bright_green, "102"), + helper_color!(on_bright_yellow, "103"), + helper_color!(on_bright_blue, "104"), + helper_color!(on_bright_magenta, "105"), + helper_color!(on_bright_cyan, "106"), + helper_color!(on_bright_white, "107"), + helper_truecolor!(on_truecolor, 0, 0, 0, "48;2;"), + helper_truecolor!(on_truecolor, 128, 0, 0, "48;2;"), + helper_truecolor!(on_truecolor, 0, 128, 0, "48;2;"), + helper_truecolor!(on_truecolor, 0, 0, 128, "48;2;"), + helper_truecolor!(on_truecolor, 128, 127, 0, "48;2;"), + helper_truecolor!(on_truecolor, 128, 0, 127, "48;2;"), + helper_truecolor!(on_truecolor, 0, 128, 127, "48;2;"), + helper_truecolor!(on_truecolor, 126, 128, 127, "48;2;"), + ]; + /// All foreground function mappings to their expected result + const FG_MAPPINGS: &[(ColorMapper, &str)] = &[ + helper_color!(black, "30"), + helper_color!(red, "31"), + helper_color!(green, "32"), + helper_color!(yellow, "33"), + helper_color!(blue, "34"), + helper_color!(magenta, "35"), + helper_color!(cyan, "36"), + helper_color!(white, "37"), + helper_color!(bright_black, "90"), + helper_color!(bright_red, "91"), + helper_color!(bright_green, "92"), + helper_color!(bright_yellow, "93"), + helper_color!(bright_blue, "94"), + helper_color!(bright_magenta, "95"), + helper_color!(bright_cyan, "96"), + helper_color!(bright_white, "97"), + helper_truecolor!(truecolor, 0, 0, 0, "38;2;"), + helper_truecolor!(truecolor, 128, 0, 0, "38;2;"), + helper_truecolor!(truecolor, 0, 128, 0, "38;2;"), + helper_truecolor!(truecolor, 0, 0, 128, "38;2;"), + helper_truecolor!(truecolor, 128, 127, 0, "38;2;"), + helper_truecolor!(truecolor, 128, 0, 127, "38;2;"), + helper_truecolor!(truecolor, 0, 128, 127, "38;2;"), + helper_truecolor!(truecolor, 126, 128, 127, "38;2;"), + ]; + + const STYLE_MAPPINGS: &[(ColorMapper, &str)] = &[ + helper_color!(bold, "1"), + helper_color!(dimmed, "2"), + helper_color!(italic, "3"), + helper_color!(underline, "4"), + helper_color!(blink, "5"), + helper_color!(reversed, "7"), + helper_color!(hidden, "8"), + helper_color!(strikethrough, "9"), + ]; - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn compute_style_blue_bold() { - let blue_bold = "\x1B[1;34m"; + #[test] + /// Check for clear + fn clear() { + for input in INPUTS { + assert_eq!(&format!("{}", input.clear()), input); + } + } - assert_eq!( - blue_bold, - ComputeStyleHelper::from(&"".blue().bold()).to_string() - ); - } + #[test] + #[cfg_attr(feature = "no-color", ignore)] + /// Check for only a set foreground color + fn simple_fg() { + for input in INPUTS { + for (fun, res) in FG_MAPPINGS { + assert_eq!( + format!("{}", ComputeStyleHelper::new(&fun(input))), + format!("\x1B[{res}m"), + "Input: '{input}'" + ); + } + } + } - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn compute_style_blue_bold_on_blue() { - let blue_bold_on_blue = "\x1B[1;44;34m"; + #[test] + #[cfg_attr(feature = "no-color", ignore)] + /// Check for only a set background color + fn simple_bg() { + for input in INPUTS { + for (fun, res) in BG_MAPPINGS { + assert_eq!( + format!("{}", ComputeStyleHelper::new(&fun(input))), + format!("\x1B[{res}m"), + "Input: '{input}'" + ); + } + } + } + #[test] + #[cfg_attr(feature = "no-color", ignore)] + /// Check for only a set background AND foreground color + fn fg_and_bg() { + for input in INPUTS { + for (fg_fun, fg_res) in FG_MAPPINGS { + for (bg_fun, bg_res) in BG_MAPPINGS { + let tmp = bg_fun(input); + assert_eq!( + format!("{}", ComputeStyleHelper::new(&fg_fun(&tmp))), + format!("\x1B[{bg_res};{fg_res}m"), + "Input: '{input}'" + ); + } + } + } + } - assert_eq!( - blue_bold_on_blue, - ComputeStyleHelper::from(&"".blue().bold().on_blue()).to_string() - ); + #[test] + #[cfg_attr(feature = "no-color", ignore)] + fn simple_style() { + for input in INPUTS { + for (fun, res) in STYLE_MAPPINGS { + assert_eq!( + format!("{}", ComputeStyleHelper::new(&fun(input))), + format!("\x1B[{res}m"), + "Input: '{input}'" + ); + } + } + } + #[test] + #[cfg_attr(feature = "no-color", ignore)] + #[ignore = "Not yet implemented"] + fn multiple_style() { + todo!() + } + #[test] + #[cfg_attr(feature = "no-color", ignore)] + fn style_fg() { + for input in INPUTS { + for (fg_fun, fg_res) in FG_MAPPINGS { + for (style_fun, style_res) in STYLE_MAPPINGS { + let tmp = style_fun(input); + assert_eq!( + format!("{}", ComputeStyleHelper::new(&fg_fun(&tmp))), + format!("\x1B[{style_res};{fg_res}m"), + "Input: '{input}'" + ); + } + } + } + } + #[test] + #[cfg_attr(feature = "no-color", ignore)] + fn style_bg() { + for input in INPUTS { + for (bg_fun, bg_res) in BG_MAPPINGS { + for (style_fun, style_res) in STYLE_MAPPINGS { + let tmp = style_fun(input); + assert_eq!( + format!("{}", ComputeStyleHelper::new(&bg_fun(&tmp))), + format!("\x1B[{style_res};{bg_res}m"), + "Input: '{input}'" + ); + } + } + } + } + #[test] + #[cfg_attr(feature = "no-color", ignore)] + fn style_fg_bg() { + for input in INPUTS { + for (fg_fun, fg_res) in FG_MAPPINGS { + for (bg_fun, bg_res) in BG_MAPPINGS { + for (style_fun, style_res) in STYLE_MAPPINGS { + let tmp = style_fun(input); + let tmp = fg_fun(&tmp); + assert_eq!( + format!("{}", ComputeStyleHelper::new(&bg_fun(&tmp))), + format!("\x1B[{style_res};{bg_res};{fg_res}m"), + "Input: '{input}'" + ); + } + } + } + } + } + #[test] + #[cfg_attr(feature = "no-color", ignore)] + #[ignore = "Not yet implemented"] + fn multiple_style_fg() { + todo!() + } + #[test] + #[cfg_attr(feature = "no-color", ignore)] + #[ignore = "Not yet implemented"] + fn multiple_style_bg() { + todo!() + } + #[test] + #[cfg_attr(feature = "no-color", ignore)] + #[ignore = "Not yet implemented"] + fn multiple_style_fg_bg() { + todo!() + } } - #[test] - fn escape_reset_sequence_spec_should_do_nothing_on_empty_strings() { - let style = ColoredString::default(); - let expected = String::new(); - - let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); + mod escape_rest { + use crate::{ColoredString, Colorize, EscapeInnerResetSequencesHelper}; - assert_eq!(expected, output); - } - - #[test] - fn escape_reset_sequence_spec_should_do_nothing_on_string_with_no_reset() { - let style = ColoredString { - input: String::from("hello world !"), - ..ColoredString::default() - }; + #[test] + fn do_nothing_on_empty_strings() { + let style = ColoredString::default(); + let expected = String::new(); - let expected = String::from("hello world !"); - let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); + let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); - assert_eq!(expected, output); - } + assert_eq!(expected, output); + } - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn escape_reset_sequence_spec_should_replace_inner_reset_sequence_with_current_style() { - let input = format!("start {} end", String::from("hello world !").red()); - let style = input.blue(); + #[test] + fn do_nothing_on_string_with_no_reset() { + let style = ColoredString { + input: String::from("hello world !"), + ..ColoredString::default() + }; - let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); - let blue = "\x1B[34m"; - let red = "\x1B[31m"; - let reset = "\x1B[0m"; - let expected = format!("start {red}hello world !{reset}{blue} end"); - assert_eq!(expected, output); - } + let expected = String::from("hello world !"); + let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); - #[cfg_attr(feature = "no-color", ignore)] - #[test] - fn escape_reset_sequence_spec_should_replace_multiple_inner_reset_sequences_with_current_style() - { - let italic_str = String::from("yo").italic(); - let input = format!("start 1:{italic_str} 2:{italic_str} 3:{italic_str} end"); - let style = input.blue(); + assert_eq!(expected, output); + } - let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); - let blue = "\x1B[34m"; - let italic = "\x1B[3m"; - let reset = "\x1B[0m"; - let expected = format!( - "start 1:{italic}yo{reset}{blue} 2:{italic}yo{reset}{blue} 3:{italic}yo{reset}{blue} end" - ); + #[cfg_attr(feature = "no-color", ignore)] + #[test] + fn replace_inner_reset_sequence_with_current_style() { + let input = format!("start {} end", String::from("hello world !").red()); + let style = input.blue(); + + let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); + let blue = "\x1B[34m"; + let red = "\x1B[31m"; + let reset = "\x1B[0m"; + let expected = format!("start {red}hello world !{reset}{blue} end"); + assert_eq!(expected, output); + } - println!("first: {expected}\nsecond: {output}"); + #[cfg_attr(feature = "no-color", ignore)] + #[test] + fn replace_multiple_inner_reset_sequences_with_current_style() { + let italic_str = String::from("yo").italic(); + let input = format!("start 1:{italic_str} 2:{italic_str} 3:{italic_str} end"); + let style = input.blue(); + + let output = EscapeInnerResetSequencesHelper::new(&style, &style).to_string(); + let blue = "\x1B[34m"; + let italic = "\x1B[3m"; + let reset = "\x1B[0m"; + let expected = format!( + "start 1:{italic}yo{reset}{blue} 2:{italic}yo{reset}{blue} 3:{italic}yo{reset}{blue} end" + ); - assert_eq!(expected, output); + assert_eq!(expected, output, "first: {expected}\nsecond: {output}"); + } } #[test] fn color_fn() { - assert_eq!("blue".blue(), "blue".color("blue")); - } - - #[test] - fn on_color_fn() { - assert_eq!("blue".on_blue(), "blue".on_color("blue")); - } - - #[test] - fn bright_color_fn() { - assert_eq!("blue".bright_blue(), "blue".color("bright blue")); - } - - #[test] - fn on_bright_color_fn() { - assert_eq!("blue".on_bright_blue(), "blue".on_color("bright blue")); + macro_rules! helper { + ($f:ident) => { + ("blue".$f(), "blue".color(stringify!($f))) + }; + } + macro_rules! helper_bright { + ($f:ident, $e:expr) => { + ("blue".$f(), "blue".color(concat!("bright ", $e))) + }; + } + let mappings = &[ + helper!(black), + helper!(red), + helper!(green), + helper!(yellow), + helper!(blue), + helper!(magenta), + helper!(cyan), + helper!(white), + helper_bright!(bright_black, "black"), + helper_bright!(bright_red, "red"), + helper_bright!(bright_green, "green"), + helper_bright!(bright_yellow, "yellow"), + helper_bright!(bright_blue, "blue"), + helper_bright!(bright_magenta, "magenta"), + helper_bright!(bright_cyan, "cyan"), + helper_bright!(bright_white, "white"), + ]; + + for (l, r) in mappings { + assert_eq!(l, r); + } } #[test] From 432ebe7477c2c7f3e8151d9df7788e6bc314ed14 Mon Sep 17 00:00:00 2001 From: tanculau <117458673+tanculau@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:02:06 +0200 Subject: [PATCH 23/27] Remove allocations in Style --- Cargo.toml | 1 + src/color.rs | 58 ++++++------- src/lib.rs | 2 +- src/style.rs | 238 ++++++++++++++++++++++++++++----------------------- 4 files changed, 159 insertions(+), 140 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d6c3d7c..c7f9976 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ features = ["Win32_Foundation", "Win32_System_Console"] [dev-dependencies] ansi_term = "0.12" insta = "1" +itertools = "0.14.0" rspec = "1" [lints.rust] diff --git a/src/color.rs b/src/color.rs index e14d6bb..96b5f7e 100644 --- a/src/color.rs +++ b/src/color.rs @@ -293,6 +293,7 @@ mod tests { #[test] fn fmt_and_to_str_same() { + use itertools::Itertools; use core::fmt::Display; use Color::*; @@ -311,7 +312,8 @@ mod tests { } // Actual test - let colors = &[ + + let colors = [ Black, Red, Green, @@ -328,40 +330,30 @@ mod tests { BrightMagenta, BrightCyan, BrightWhite, - TrueColor { r: 0, g: 0, b: 0 }, - TrueColor { - r: 255, - g: 255, - b: 255, - }, - TrueColor { - r: 126, - g: 127, - b: 128, - }, - TrueColor { r: 255, g: 0, b: 0 }, - TrueColor { - r: 255, - g: 255, - b: 0, - }, - TrueColor { r: 0, g: 255, b: 0 }, - TrueColor { - r: 0, - g: 255, - b: 255, - }, - TrueColor { r: 0, g: 0, b: 255 }, - TrueColor { - r: 255, - g: 0, - b: 255, - }, - ]; + ] + .into_iter() + .chain( + // Iterator over TrueColors + // r g b + // 0 0 0 + // 0 0 1 + // 0 0 2 + // 0 0 3 + // 0 1 0 + // .. + // 3 3 3 + (0..4) + .combinations_with_replacement(3) + .map(|rgb| Color::TrueColor { + r: rgb[0], + g: rgb[1], + b: rgb[2], + }), + ); for color in colors { - assert_eq!(color.to_fg_str(), FmtFgWrapper(*color).to_string()); - assert_eq!(color.to_bg_str(), FmtBgWrapper(*color).to_string()); + assert_eq!(color.to_fg_str(), FmtFgWrapper(color).to_string()); + assert_eq!(color.to_bg_str(), FmtBgWrapper(color).to_string()); } } diff --git a/src/lib.rs b/src/lib.rs index 7851fa1..0ab7713 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -599,7 +599,7 @@ impl Display for ComputeStyleHelper { let mut has_wrote = if self.style == style::CLEAR { false } else { - f.write_str(&self.style.to_str())?; + self.style.private_fmt(f)?; true }; diff --git a/src/style.rs b/src/style.rs index 557dce0..adb928b 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,4 +1,7 @@ -use core::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; +use core::{ + fmt::Write, + ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}, +}; macro_rules! auto_impl_ref_binop_trait { (impl $trait_name:ident, $method:ident for $t:ty, $u:ty) => { @@ -207,21 +210,22 @@ pub enum Styles { } impl Styles { - fn to_str<'a>(self) -> &'a str { + fn private_fmt(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use core::fmt::Write; match self { - Self::Clear => "", // unreachable, but we don't want to panic - Self::Bold => "1", - Self::Dimmed => "2", - Self::Italic => "3", - Self::Underline => "4", - Self::Blink => "5", - Self::Reversed => "7", - Self::Hidden => "8", - Self::Strikethrough => "9", + Self::Clear => Ok(()), // unreachable, but we don't want to panic + Self::Bold => f.write_char('1'), + Self::Dimmed => f.write_char('2'), + Self::Italic => f.write_char('3'), + Self::Underline => f.write_char('4'), + Self::Blink => f.write_char('5'), + Self::Reversed => f.write_char('7'), + Self::Hidden => f.write_char('8'), + Self::Strikethrough => f.write_char('9'), } } - fn to_u8(self) -> u8 { + fn to_u8_mask(self) -> u8 { match self { Self::Clear => CLEARV, Self::Bold => BOLD, @@ -235,21 +239,11 @@ impl Styles { } } - fn from_u8(u: u8) -> Option> { - if u == CLEARV { - return None; - } - - let res: Vec = STYLES + fn from_u8(u: u8) -> impl Iterator + Clone { + STYLES .iter() - .filter(|&(mask, _)| (0 != (u & mask))) + .filter(move |&(mask, _)| (0 != (u & mask))) .map(|&(_, value)| value) - .collect(); - if res.is_empty() { - None - } else { - Some(res) - } } } @@ -257,7 +251,7 @@ impl BitAnd for Styles { type Output = Style; fn bitand(self, rhs: Self) -> Self::Output { - Style(self.to_u8() & rhs.to_u8()) + Style(self.to_u8_mask() & rhs.to_u8_mask()) } } @@ -267,7 +261,7 @@ impl BitAnd