diff --git a/crates/anstream/src/strip.rs b/crates/anstream/src/strip.rs index ef828e59..f55b6656 100644 --- a/crates/anstream/src/strip.rs +++ b/crates/anstream/src/strip.rs @@ -50,23 +50,11 @@ impl std::io::Write for StripStream where S: RawStream, { + #[inline] fn write(&mut self, buf: &[u8]) -> std::io::Result { - let initial_state = self.state.clone(); - - for printable in self.state.strip_next(buf) { - let possible = printable.len(); - let written = self.raw.write(printable)?; - if possible != written { - let divergence = &printable[written..]; - let offset = offset_to(buf, divergence); - let consumed = &buf[offset..]; - self.state = initial_state; - self.state.strip_next(consumed).last(); - return Ok(offset); - } - } - Ok(buf.len()) + write(&mut self.raw, &mut self.state, buf) } + #[inline] fn flush(&mut self) -> std::io::Result<()> { self.raw.flush() @@ -87,6 +75,28 @@ where // Not bothering with `write_fmt` as it just calls `write_all` } +fn write( + raw: &mut dyn std::io::Write, + state: &mut StripBytes, + buf: &[u8], +) -> std::io::Result { + let initial_state = state.clone(); + + for printable in state.strip_next(buf) { + let possible = printable.len(); + let written = raw.write(printable)?; + if possible != written { + let divergence = &printable[written..]; + let offset = offset_to(buf, divergence); + let consumed = &buf[offset..]; + *state = initial_state; + state.strip_next(consumed).last(); + return Ok(offset); + } + } + Ok(buf.len()) +} + #[inline] fn offset_to(total: &[u8], subslice: &[u8]) -> usize { let total = total.as_ptr(); diff --git a/crates/anstyle-roff/tests/test_roff.rs b/crates/anstyle-roff/tests/test_roff.rs index 96fbf537..a2b919e8 100644 --- a/crates/anstyle-roff/tests/test_roff.rs +++ b/crates/anstyle-roff/tests/test_roff.rs @@ -1,11 +1,11 @@ -use anstyle::{AnsiColor, Color, Style}; +use anstyle::Style; #[test] fn test_ansi_color_output() { - let style = Style::new() - .fg_color(Some(Color::Ansi(AnsiColor::Red))) - .bg_color(Some(Color::Ansi(AnsiColor::Blue))); - let text = format!("{}{}", style.render(), "test"); + // HACK: cansi doesn't properly merge separate sequences + // let style = AnsiColor::Red.on(AnsiColor::Blue); + // let text = format!("{}{}", style.render(), "test"); + let text = "\u{1b}[31;44mtest".to_owned(); let roff_doc = anstyle_roff::to_roff(&text); snapbox::assert_eq_path("tests/roffs/ansi_color.roff", roff_doc.to_roff()); } @@ -14,6 +14,7 @@ fn test_ansi_color_output() { fn test_bold_output() { let style = Style::new().bold(); let text = format!("{}{}", style.render(), "test"); + dbg!(&text); let roff_doc = anstyle_roff::to_roff(&text); snapbox::assert_eq_path("tests/roffs/bold.roff", roff_doc.to_roff()); @@ -23,6 +24,7 @@ fn test_bold_output() { fn test_italic_output() { let style = Style::new().italic(); let text = format!("{}{}", style.render(), "test"); + dbg!(&text); let roff_doc = anstyle_roff::to_roff(&text); snapbox::assert_eq_path("tests/roffs/italic.roff", roff_doc.to_roff()); @@ -30,10 +32,11 @@ fn test_italic_output() { #[test] fn test_bright_color_output_as_bold() { - let style = Style::new() - .fg_color(Some(Color::Ansi(AnsiColor::BrightRed))) - .bg_color(Some(Color::Ansi(AnsiColor::Blue))); - let text = format!("{}{}", style.render(), "test"); + // HACK: cansi doesn't properly merge separate sequences + // let style = AnsiColor::BrightRed.on(AnsiColor::Blue); + // let text = format!("{}{}", style.render(), "test"); + let text = "\u{1b}[91;44mtest".to_owned(); + dbg!(&text); let roff_doc = anstyle_roff::to_roff(&text); snapbox::assert_eq_path("tests/roffs/bright_ansi_colors.roff", roff_doc.to_roff()); } diff --git a/crates/anstyle/src/color.rs b/crates/anstyle/src/color.rs index c15a84c2..c75e236e 100644 --- a/crates/anstyle/src/color.rs +++ b/crates/anstyle/src/color.rs @@ -24,39 +24,63 @@ impl Color { /// Render the ANSI code for a foreground color #[inline] pub fn render_fg(self) -> impl core::fmt::Display { - DisplayColor { - color: self, - context: ColorContext::Foreground, + match self { + Self::Ansi(color) => DisplayBuffer::default().write_str(color.as_fg_str()), + Self::Ansi256(color) => color.as_fg_buffer(), + Self::Rgb(color) => color.as_fg_buffer(), } } + #[inline] + #[cfg(feature = "std")] + pub(crate) fn write_fg_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + let buffer = match self { + Self::Ansi(color) => DisplayBuffer::default().write_str(color.as_fg_str()), + Self::Ansi256(color) => color.as_fg_buffer(), + Self::Rgb(color) => color.as_fg_buffer(), + }; + buffer.write_to(write) + } + /// Render the ANSI code for a background color #[inline] pub fn render_bg(self) -> impl core::fmt::Display { - DisplayColor { - color: self, - context: ColorContext::Background, + match self { + Self::Ansi(color) => DisplayBuffer::default().write_str(color.as_bg_str()), + Self::Ansi256(color) => color.as_bg_buffer(), + Self::Rgb(color) => color.as_bg_buffer(), } } #[inline] - pub(crate) fn ansi_fmt( - &self, - f: &mut dyn core::fmt::Write, - context: ColorContext, - ) -> core::fmt::Result { + #[cfg(feature = "std")] + pub(crate) fn write_bg_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + let buffer = match self { + Self::Ansi(color) => DisplayBuffer::default().write_str(color.as_bg_str()), + Self::Ansi256(color) => color.as_bg_buffer(), + Self::Rgb(color) => color.as_bg_buffer(), + }; + buffer.write_to(write) + } + + #[inline] + pub(crate) fn render_underline(self) -> impl core::fmt::Display { match self { - Self::Ansi(color) => color.ansi_fmt(f, context), - Self::Ansi256(color) => color.ansi_fmt(f, context), - Self::Rgb(color) => color.ansi_fmt(f, context), + Self::Ansi(color) => color.as_underline_buffer(), + Self::Ansi256(color) => color.as_underline_buffer(), + Self::Rgb(color) => color.as_underline_buffer(), } } -} -impl AnsiColorFmt for Color { #[inline] - fn ansi_fmt(&self, f: &mut dyn core::fmt::Write, context: ColorContext) -> core::fmt::Result { - self.ansi_fmt(f, context) + #[cfg(feature = "std")] + pub(crate) fn write_underline_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + let buffer = match self { + Self::Ansi(color) => color.as_underline_buffer(), + Self::Ansi256(color) => color.as_underline_buffer(), + Self::Rgb(color) => color.as_underline_buffer(), + }; + buffer.write_to(write) } } @@ -185,21 +209,65 @@ impl AnsiColor { /// Render the ANSI code for a foreground color #[inline] pub fn render_fg(self) -> impl core::fmt::Display { - DisplayColor { - color: self, - context: ColorContext::Foreground, + self.as_fg_str() + } + + #[inline] + fn as_fg_str(&self) -> &'static str { + match self { + Self::Black => escape!("3", "0"), + Self::Red => escape!("3", "1"), + Self::Green => escape!("3", "2"), + Self::Yellow => escape!("3", "3"), + Self::Blue => escape!("3", "4"), + Self::Magenta => escape!("3", "5"), + Self::Cyan => escape!("3", "6"), + Self::White => escape!("3", "7"), + Self::BrightBlack => escape!("9", "0"), + Self::BrightRed => escape!("9", "1"), + Self::BrightGreen => escape!("9", "2"), + Self::BrightYellow => escape!("9", "3"), + Self::BrightBlue => escape!("9", "4"), + Self::BrightMagenta => escape!("9", "5"), + Self::BrightCyan => escape!("9", "6"), + Self::BrightWhite => escape!("9", "7"), } } /// Render the ANSI code for a background color #[inline] pub fn render_bg(self) -> impl core::fmt::Display { - DisplayColor { - color: self, - context: ColorContext::Background, + self.as_bg_str() + } + + #[inline] + fn as_bg_str(&self) -> &'static str { + match self { + Self::Black => escape!("4", "0"), + Self::Red => escape!("4", "1"), + Self::Green => escape!("4", "2"), + Self::Yellow => escape!("4", "3"), + Self::Blue => escape!("4", "4"), + Self::Magenta => escape!("4", "5"), + Self::Cyan => escape!("4", "6"), + Self::White => escape!("4", "7"), + Self::BrightBlack => escape!("10", "0"), + Self::BrightRed => escape!("10", "1"), + Self::BrightGreen => escape!("10", "2"), + Self::BrightYellow => escape!("10", "3"), + Self::BrightBlue => escape!("10", "4"), + Self::BrightMagenta => escape!("10", "5"), + Self::BrightCyan => escape!("10", "6"), + Self::BrightWhite => escape!("10", "7"), } } + #[inline] + fn as_underline_buffer(&self) -> DisplayBuffer { + // No per-color codes; must delegate to `Ansi256Color` + Ansi256Color::from(*self).as_underline_buffer() + } + /// Change the color to/from bright #[must_use] #[inline] @@ -267,41 +335,6 @@ impl AnsiColor { Self::BrightWhite => true, } } - - #[inline] - fn suffix(self) -> &'static str { - match self { - Self::Black => "0", - Self::Red => "1", - Self::Green => "2", - Self::Yellow => "3", - Self::Blue => "4", - Self::Magenta => "5", - Self::Cyan => "6", - Self::White => "7", - Self::BrightBlack => "0", - Self::BrightRed => "1", - Self::BrightGreen => "2", - Self::BrightYellow => "3", - Self::BrightBlue => "4", - Self::BrightMagenta => "5", - Self::BrightCyan => "6", - Self::BrightWhite => "7", - } - } -} - -impl AnsiColorFmt for AnsiColor { - fn ansi_fmt(&self, f: &mut dyn core::fmt::Write, context: ColorContext) -> core::fmt::Result { - match (context, self.is_bright()) { - (ColorContext::Background, true) => write!(f, "10{}", self.suffix()), - (ColorContext::Foreground, true) => write!(f, "9{}", self.suffix()), - (ColorContext::Background, false) => write!(f, "4{}", self.suffix()), - (ColorContext::Foreground, false) => write!(f, "3{}", self.suffix()), - // No per-color codes; must delegate to `Ansi256Color` - (ColorContext::Underline, _) => Ansi256Color::from(*self).ansi_fmt(f, context), - } - } } /// Define style with specified foreground color and effects @@ -398,36 +431,37 @@ impl Ansi256Color { /// Render the ANSI code for a foreground color #[inline] pub fn render_fg(self) -> impl core::fmt::Display { - DisplayColor { - color: self, - context: ColorContext::Foreground, - } + self.as_fg_buffer() + } + + #[inline] + fn as_fg_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[38;5;") + .write_code(self.index()) + .write_str("m") } /// Render the ANSI code for a background color #[inline] pub fn render_bg(self) -> impl core::fmt::Display { - DisplayColor { - color: self, - context: ColorContext::Background, - } + self.as_bg_buffer() } -} -impl AnsiColorFmt for Ansi256Color { - fn ansi_fmt(&self, f: &mut dyn core::fmt::Write, context: ColorContext) -> core::fmt::Result { - match context { - ColorContext::Background => { - write!(f, "48;")?; - } - ColorContext::Foreground => { - write!(f, "38;")?; - } - ColorContext::Underline => { - write!(f, "58;")?; - } - } - write!(f, "5;{}", self.index()) + #[inline] + fn as_bg_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[48;5;") + .write_code(self.index()) + .write_str("m") + } + + #[inline] + fn as_underline_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[58;5;") + .write_code(self.index()) + .write_str("m") } } @@ -499,36 +533,49 @@ impl RgbColor { /// Render the ANSI code for a foreground color #[inline] pub fn render_fg(self) -> impl core::fmt::Display { - DisplayColor { - color: self, - context: ColorContext::Foreground, - } + self.as_fg_buffer() + } + + #[inline] + fn as_fg_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[38;2;") + .write_code(self.r()) + .write_str(";") + .write_code(self.g()) + .write_str(";") + .write_code(self.b()) + .write_str("m") } /// Render the ANSI code for a background color #[inline] pub fn render_bg(self) -> impl core::fmt::Display { - DisplayColor { - color: self, - context: ColorContext::Background, - } + self.as_bg_buffer() } -} -impl AnsiColorFmt for RgbColor { - fn ansi_fmt(&self, f: &mut dyn core::fmt::Write, context: ColorContext) -> core::fmt::Result { - match context { - ColorContext::Background => { - write!(f, "48;")?; - } - ColorContext::Foreground => { - write!(f, "38;")?; - } - ColorContext::Underline => { - write!(f, "58;")?; - } - } - write!(f, "2;{};{};{}", self.r(), self.g(), self.b()) + #[inline] + fn as_bg_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[48;2;") + .write_code(self.r()) + .write_str(";") + .write_code(self.g()) + .write_str(";") + .write_code(self.b()) + .write_str("m") + } + + #[inline] + fn as_underline_buffer(&self) -> DisplayBuffer { + DisplayBuffer::default() + .write_str("\x1B[58;2;") + .write_code(self.r()) + .write_str(";") + .write_code(self.g()) + .write_str(";") + .write_code(self.b()) + .write_str("m") } } @@ -557,27 +604,75 @@ impl core::ops::BitOr for RgbColor { } } -#[derive(Copy, Clone)] -pub(crate) enum ColorContext { - Background, - Foreground, - Underline, +#[derive(Default, Debug)] +struct DisplayBuffer { + buffer: [u8; 19], + len: usize, } -trait AnsiColorFmt { - fn ansi_fmt(&self, f: &mut dyn core::fmt::Write, context: ColorContext) -> core::fmt::Result; -} +impl DisplayBuffer { + #[inline] + #[must_use] + fn write_str(mut self, part: &'static str) -> Self { + for (i, b) in part.as_bytes().iter().enumerate() { + self.buffer[self.len + i] = *b; + } + self.len += part.len(); + self + } + + #[inline] + #[must_use] + fn write_code(mut self, code: u8) -> Self { + let c1: u8 = (code / 100) % 10; + let c2: u8 = (code / 10) % 10; + let c3: u8 = code % 10; + + let mut printed = true; + if c1 != 0 { + printed = true; + self.buffer[self.len] = b'0' + c1; + self.len += 1; + } + if c2 != 0 || printed { + self.buffer[self.len] = b'0' + c2; + self.len += 1; + } + // If we received a zero value we must still print a value. + self.buffer[self.len] = b'0' + c3; + self.len += 1; -struct DisplayColor { - color: C, - context: ColorContext, + self + } + + #[inline] + fn as_str(&self) -> &str { + // SAFETY: Only `&str` can be written to the buffer + unsafe { core::str::from_utf8_unchecked(&self.buffer[0..self.len]) } + } + + #[inline] + #[cfg(feature = "std")] + fn write_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + write.write_all(self.as_str().as_bytes()) + } } -impl core::fmt::Display for DisplayColor { +impl core::fmt::Display for DisplayBuffer { + #[inline] fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "\x1B[")?; - self.color.ansi_fmt(f, self.context)?; - write!(f, "m")?; - Ok(()) + self.as_str().fmt(f) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn max_display_buffer() { + let c = RgbColor(255, 255, 255); + let actual = c.render_fg().to_string(); + assert_eq!(actual, "\u{1b}[38;2;255;255;255m"); } } diff --git a/crates/anstyle/src/effect.rs b/crates/anstyle/src/effect.rs index ad3b4889..d31dafe8 100644 --- a/crates/anstyle/src/effect.rs +++ b/crates/anstyle/src/effect.rs @@ -159,6 +159,15 @@ impl Effects { pub fn render(self) -> impl core::fmt::Display { EffectsDisplay(self) } + + #[inline] + #[cfg(feature = "std")] + pub(crate) fn write_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + for index in self.index_iter() { + write.write_all(METADATA[index].escape.as_bytes())?; + } + Ok(()) + } } /// # Examples @@ -244,92 +253,68 @@ impl core::ops::SubAssign for Effects { pub(crate) struct Metadata { pub(crate) name: &'static str, - pub(crate) primary: usize, - pub(crate) secondary: Option, + pub(crate) escape: &'static str, } pub(crate) const METADATA: [Metadata; 12] = [ Metadata { name: "BOLD", - primary: 1, - secondary: None, + escape: escape!("1"), }, Metadata { name: "DIMMED", - primary: 2, - secondary: None, + escape: escape!("2"), }, Metadata { name: "ITALIC", - primary: 3, - secondary: None, + escape: escape!("3"), }, Metadata { name: "UNDERLINE", - primary: 4, - secondary: None, + escape: escape!("4"), }, Metadata { name: "DOUBLE_UNDERLINE", - primary: 21, - secondary: None, + escape: escape!("21"), }, Metadata { name: "CURLY_UNDERLINE", - primary: 4, - secondary: Some(3), + escape: escape!("4:3"), }, Metadata { name: "DOTTED_UNDERLINE", - primary: 4, - secondary: Some(4), + escape: escape!("4:4"), }, Metadata { name: "DASHED_UNDERLINE", - primary: 4, - secondary: Some(5), + escape: escape!("4:5"), }, Metadata { name: "BLINK", - primary: 5, - secondary: None, + escape: escape!("5"), }, Metadata { name: "INVERT", - primary: 7, - secondary: None, + escape: escape!("7"), }, Metadata { name: "HIDDEN", - primary: 8, - secondary: None, + escape: escape!("8"), }, Metadata { name: "STRIKETHROUGH", - primary: 9, - secondary: None, + escape: escape!("9"), }, ]; struct EffectsDisplay(Effects); impl core::fmt::Display for EffectsDisplay { + #[inline] fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - if self.0.is_plain() { - return Ok(()); - } - - write!(f, "\x1B[")?; - for (i, index) in self.0.index_iter().enumerate() { - if i != 0 { - write!(f, ";")?; - } - write!(f, "{}", METADATA[index].primary)?; - if let Some(secondary) = METADATA[index].secondary { - write!(f, ":{}", secondary)?; - } + for index in self.0.index_iter() { + METADATA[index].escape.fmt(f)?; } - write!(f, "m")?; Ok(()) } } diff --git a/crates/anstyle/src/lib.rs b/crates/anstyle/src/lib.rs index c0173ec5..41b7174e 100644 --- a/crates/anstyle/src/lib.rs +++ b/crates/anstyle/src/lib.rs @@ -45,6 +45,9 @@ #![cfg_attr(not(feature = "std"), no_std)] +#[macro_use] +mod macros; + mod color; mod effect; mod reset; diff --git a/crates/anstyle/src/macros.rs b/crates/anstyle/src/macros.rs new file mode 100644 index 00000000..f19666e6 --- /dev/null +++ b/crates/anstyle/src/macros.rs @@ -0,0 +1,5 @@ +macro_rules! escape { + ($($inner:expr),*) => { + concat!("\x1B[", $($inner),*, "m") + }; +} diff --git a/crates/anstyle/src/reset.rs b/crates/anstyle/src/reset.rs index 2729eba1..5f5f2b46 100644 --- a/crates/anstyle/src/reset.rs +++ b/crates/anstyle/src/reset.rs @@ -14,6 +14,8 @@ struct ResetDisplay; impl core::fmt::Display for ResetDisplay { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "\x1B[0m") + RESET.fmt(f) } } + +pub(crate) const RESET: &str = "\x1B[0m"; diff --git a/crates/anstyle/src/style.rs b/crates/anstyle/src/style.rs index dfd25169..4861d896 100644 --- a/crates/anstyle/src/style.rs +++ b/crates/anstyle/src/style.rs @@ -1,3 +1,5 @@ +use crate::reset::RESET; + /// ANSI Text styling /// /// # Examples @@ -94,6 +96,27 @@ impl Style { StyleDisplay(self) } + /// Write the ANSI code + #[inline] + #[cfg(feature = "std")] + pub fn write_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + self.effects.write_to(write)?; + + if let Some(fg) = self.fg { + fg.write_fg_to(write)?; + } + + if let Some(bg) = self.bg { + bg.write_bg_to(write)?; + } + + if let Some(underline) = self.underline { + underline.write_underline_to(write)?; + } + + Ok(()) + } + /// Renders the relevant [`Reset`][crate::Reset] code /// /// Unlike [`Reset::render`][crate::Reset::render], this will elide the code if there is nothing to reset. @@ -101,6 +124,19 @@ impl Style { pub fn render_reset(self) -> impl core::fmt::Display { ResetDisplay(self != Self::new()) } + + /// Write the relevant [`Reset`][crate::Reset] code + /// + /// Unlike [`Reset::render`][crate::Reset::render], this will elide the code if there is nothing to reset. + #[inline] + #[cfg(feature = "std")] + pub fn write_reset_to(self, write: &mut dyn std::io::Write) -> std::io::Result<()> { + if self != Self::new() { + write.write_all(RESET.as_bytes()) + } else { + Ok(()) + } + } } /// # Convenience @@ -410,49 +446,21 @@ struct StyleDisplay(Style); impl core::fmt::Display for StyleDisplay { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - if self.0.is_plain() { - return Ok(()); - } - - write!(f, "\x1B[")?; - - let mut first = true; - - for index in self.0.effects.index_iter() { - separator(f, &mut first)?; - write!(f, "{}", crate::effect::METADATA[index].primary)?; - if let Some(secondary) = crate::effect::METADATA[index].secondary { - write!(f, ":{}", secondary)?; - } - } + self.0.effects.render().fmt(f)?; if let Some(fg) = self.0.fg { - separator(f, &mut first)?; - fg.ansi_fmt(f, crate::ColorContext::Foreground)?; + fg.render_fg().fmt(f)?; } if let Some(bg) = self.0.bg { - separator(f, &mut first)?; - bg.ansi_fmt(f, crate::ColorContext::Background)?; + bg.render_bg().fmt(f)?; } if let Some(underline) = self.0.underline { - separator(f, &mut first)?; - underline.ansi_fmt(f, crate::ColorContext::Underline)?; + underline.render_underline().fmt(f)?; } - write!(f, "m")?; - Ok(()) - } -} - -#[inline] -fn separator(f: &mut core::fmt::Formatter<'_>, first: &mut bool) -> core::fmt::Result { - if *first { - *first = false; Ok(()) - } else { - write!(f, ";") } } @@ -461,7 +469,7 @@ struct ResetDisplay(bool); impl core::fmt::Display for ResetDisplay { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { if self.0 { - write!(f, "\x1B[0m") + RESET.fmt(f) } else { Ok(()) }