| //! Write colored strings with ANSI escape code into a `termcolor` terminal. |
| //! |
| //! This package provides a single function, [`write_ansi`], which parses ANSI |
| //! escape codes in the provided byte string and transforms them into the |
| //! corresponding `termcolor` commands. The colors will be supported even on a |
| //! Windows console. |
| //! |
| //! The main purpose of this package is to forward colored output from a child |
| //! process. |
| //! |
| //! ```rust |
| // #![doc(include = "../examples/rustc.rs")] // still unstable, see issue 44732 |
| //! extern crate termcolor; |
| //! extern crate fwdansi; |
| //! |
| //! use termcolor::*; |
| //! use std::io; |
| //! use std::process::Command; |
| //! use fwdansi::write_ansi; |
| //! |
| //! fn main() -> io::Result<()> { |
| //! let output = Command::new("rustc").args(&["--color", "always"]).output()?; |
| //! |
| //! let mut stderr = StandardStream::stderr(ColorChoice::Always); |
| //! write_ansi(&mut stderr, &output.stderr)?; |
| //! //^ should print "error: no input filename given" with appropriate color everywhere. |
| //! |
| //! Ok(()) |
| //! } |
| //! ``` |
| |
| extern crate memchr; |
| extern crate termcolor; |
| |
| use memchr::memchr; |
| use termcolor::{Color, ColorSpec, WriteColor}; |
| |
| use std::io; |
| |
| /// Writes a string with ANSI escape code into the colored output stream. |
| /// |
| /// Only SGR (`\x1b[…m`) is supported. Other input will be printed as-is. |
| pub fn write_ansi<W: WriteColor>(mut writer: W, mut ansi: &[u8]) -> io::Result<()> { |
| while let Some(index) = memchr(0x1b, ansi) { |
| let (left, right) = ansi.split_at(index); |
| writer.write_all(left)?; |
| if right.is_empty() { |
| return Ok(()); |
| } |
| |
| let mut parser = ColorSpecParser::new(right); |
| parser.parse(); |
| if parser.ansi.as_ptr() == right.as_ptr() { |
| writer.write_all(&right[..1])?; |
| ansi = &right[1..]; |
| } else { |
| if parser.reset { |
| writer.reset()?; |
| } else { |
| writer.set_color(&parser.spec)?; |
| } |
| ansi = parser.ansi; |
| } |
| } |
| writer.write_all(ansi) |
| } |
| |
| #[derive(Copy, Clone, PartialEq, Eq, Debug)] |
| enum State { |
| Normal, |
| PrepareCustomColor, |
| Ansi256, |
| Rgb, |
| } |
| #[derive(Debug)] |
| struct ColorSpecParser<'a> { |
| spec: ColorSpec, |
| ansi: &'a [u8], |
| reset: bool, |
| state: State, |
| is_bg: bool, |
| red: Option<u8>, |
| green: Option<u8>, |
| } |
| impl<'a> ColorSpecParser<'a> { |
| fn new(ansi: &'a [u8]) -> Self { |
| Self { |
| spec: ColorSpec::new(), |
| ansi, |
| reset: false, |
| state: State::Normal, |
| is_bg: false, |
| red: None, |
| green: None, |
| } |
| } |
| |
| fn parse(&mut self) { |
| #[derive(PartialEq, Eq, Debug)] |
| enum Expected { |
| Escape, |
| OpenBracket, |
| Number(u8), |
| } |
| |
| while !self.ansi.is_empty() { |
| let mut expected = Expected::Escape; |
| let mut it = self.ansi.iter(); |
| for b in &mut it { |
| match (*b, expected) { |
| (0x1b, Expected::Escape) => { |
| expected = Expected::OpenBracket; |
| continue; |
| } |
| (b'[', Expected::OpenBracket) => { |
| expected = Expected::Number(0); |
| continue; |
| } |
| (b'0'..=b'9', Expected::Number(number)) => { |
| if let Some(n) = number.checked_mul(10).and_then(|n| n.checked_add(b - b'0')) { |
| expected = Expected::Number(n); |
| continue; |
| } |
| } |
| (b':', Expected::Number(number)) |
| | (b';', Expected::Number(number)) |
| | (b'm', Expected::Number(number)) => { |
| self.apply_number(number); |
| if *b == b'm' { |
| expected = Expected::Escape; |
| break; |
| } else { |
| expected = Expected::Number(0); |
| continue; |
| } |
| } |
| _ => {} |
| } |
| return; |
| } |
| if let Expected::Escape = expected { |
| self.ansi = it.as_slice(); |
| } else { |
| break; |
| } |
| } |
| } |
| |
| fn set_color(&mut self, color: Color) { |
| if self.is_bg { |
| self.spec.set_bg(Some(color)); |
| } else { |
| self.spec.set_fg(Some(color)); |
| } |
| } |
| |
| fn apply_number(&mut self, number: u8) { |
| self.reset = false; |
| match (number, self.state) { |
| (0, State::Normal) => { |
| self.reset = true; |
| } |
| (1, State::Normal) => { |
| self.spec.set_bold(true); |
| } |
| (4, State::Normal) => { |
| self.spec.set_underline(true); |
| } |
| (21, State::Normal) => { |
| self.spec.set_bold(false); |
| } |
| (24, State::Normal) => { |
| self.spec.set_underline(false); |
| } |
| (38, State::Normal) | (48, State::Normal) => { |
| self.is_bg = number == 48; |
| self.state = State::PrepareCustomColor; |
| } |
| (30..=39, State::Normal) => { |
| self.spec.set_fg(parse_color(number - 30)); |
| } |
| (40..=49, State::Normal) => { |
| self.spec.set_bg(parse_color(number - 40)); |
| } |
| (90..=97, State::Normal) => { |
| self.spec.set_intense(true).set_fg(parse_color(number - 90)); |
| } |
| (100..=107, State::Normal) => { |
| self.spec.set_intense(true).set_bg(parse_color(number - 100)); |
| } |
| (5, State::PrepareCustomColor) => { |
| self.state = State::Ansi256; |
| } |
| (2, State::PrepareCustomColor) => { |
| self.state = State::Rgb; |
| self.red = None; |
| self.green = None; |
| } |
| (n, State::Ansi256) => { |
| self.set_color(Color::Ansi256(n)); |
| self.state = State::Normal; |
| } |
| (b, State::Rgb) => match (self.red, self.green) { |
| (None, _) => { |
| self.red = Some(b); |
| } |
| (Some(_), None) => { |
| self.green = Some(b); |
| } |
| (Some(r), Some(g)) => { |
| self.set_color(Color::Rgb(r, g, b)); |
| self.state = State::Normal; |
| } |
| }, |
| _ => { |
| self.state = State::Normal; |
| } |
| } |
| } |
| } |
| |
| fn parse_color(digit: u8) -> Option<Color> { |
| match digit { |
| 0 => Some(Color::Black), |
| 1 => Some(Color::Red), |
| 2 => Some(Color::Green), |
| 3 => Some(Color::Yellow), |
| 4 => Some(Color::Blue), |
| 5 => Some(Color::Magenta), |
| 6 => Some(Color::Cyan), |
| 7 => Some(Color::White), |
| _ => None, |
| } |
| } |