From 2b14654552f18a99e636a4124efdd50d2cf79464 Mon Sep 17 00:00:00 2001 From: Lovecraftian Horror Date: Thu, 12 Jan 2023 18:44:15 -0700 Subject: [PATCH 1/2] Drop regex for parsing windows console colors --- Cargo.toml | 3 +- src/windows_term/colors.rs | 153 ++++++++++++++++++++++++++++--------- 2 files changed, 119 insertions(+), 37 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9db13571..537fc655 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,11 @@ rust-version = "1.48.0" [features] default = ["unicode-width", "ansi-parsing"] -windows-console-colors = ["ansi-parsing", "regex"] +windows-console-colors = ["ansi-parsing"] ansi-parsing = [] [dependencies] libc = "0.2.30" -regex = { version = "1.4.2", optional = true, default-features = false, features = ["std"] } unicode-width = { version = "0.1", optional = true } lazy_static = "1.4.0" diff --git a/src/windows_term/colors.rs b/src/windows_term/colors.rs index 5efd4f15..ad41eed0 100644 --- a/src/windows_term/colors.rs +++ b/src/windows_term/colors.rs @@ -1,8 +1,10 @@ +// TODO: add tests + use std::io; use std::mem; use std::os::windows::io::AsRawHandle; +use std::str::Bytes; -use regex::Regex; use windows_sys::Win32::Foundation::HANDLE; use windows_sys::Win32::System::Console::{ GetConsoleScreenBufferInfo, SetConsoleTextAttribute, CONSOLE_SCREEN_BUFFER_INFO, @@ -12,12 +14,6 @@ use windows_sys::Win32::System::Console::{ use crate::Term; -lazy_static::lazy_static! { - static ref INTENSE_COLOR_RE: Regex = Regex::new(r"\x1b\[(3|4)8;5;(8|9|1[0-5])m").unwrap(); - static ref NORMAL_COLOR_RE: Regex = Regex::new(r"\x1b\[(3|4)([0-7])m").unwrap(); - static ref ATTR_RE: Regex = Regex::new(r"\x1b\[([1-8])m").unwrap(); -} - type WORD = u16; const FG_CYAN: WORD = FG_BLUE | FG_GREEN; @@ -306,23 +302,12 @@ pub fn console_colors(out: &Term, mut con: Console, bytes: &[u8]) -> io::Result< out.write_through_common(part.as_bytes())?; } else if part == "\x1b[0m" { con.reset()?; - } else if let Some(cap) = INTENSE_COLOR_RE.captures(part) { - let color = get_color_from_ansi(cap.get(2).unwrap().as_str()); - - match cap.get(1).unwrap().as_str() { - "3" => con.fg(Intense::Yes, color)?, - "4" => con.bg(Intense::Yes, color)?, - _ => unreachable!(), - }; - } else if let Some(cap) = NORMAL_COLOR_RE.captures(part) { - let color = get_color_from_ansi(cap.get(2).unwrap().as_str()); - - match cap.get(1).unwrap().as_str() { - "3" => con.fg(Intense::No, color)?, - "4" => con.bg(Intense::No, color)?, - _ => unreachable!(), - }; - } else if !ATTR_RE.is_match(part) { + } else if let Some((intense, color, fg_bg)) = driver(parse_color, part) { + match fg_bg { + FgBg::Foreground => con.fg(intense, color), + FgBg::Background => con.bg(intense, color), + }?; + } else if driver(parse_attr, part).is_none() { out.write_through_common(part.as_bytes())?; } } @@ -331,16 +316,114 @@ pub fn console_colors(out: &Term, mut con: Console, bytes: &[u8]) -> io::Result< Ok(()) } -fn get_color_from_ansi(ansi: &str) -> Color { - match ansi { - "0" | "8" => Color::Black, - "1" | "9" => Color::Red, - "2" | "10" => Color::Green, - "3" | "11" => Color::Yellow, - "4" | "12" => Color::Blue, - "5" | "13" => Color::Magenta, - "6" | "14" => Color::Cyan, - "7" | "15" => Color::White, - _ => unreachable!(), +enum FgBg { + Foreground, + Background, +} + +impl FgBg { + fn new(byte: u8) -> Option { + match byte { + b'3' => Some(Self::Foreground), + b'4' => Some(Self::Background), + _ => None, + } + } +} + +fn driver(parse: fn(Bytes<'_>) -> Option, part: &str) -> Option { + let mut bytes = part.bytes(); + + loop { + while bytes.next()? != b'\x1b' {} + + if let ret @ Some(_) = (parse)(bytes.clone()) { + return ret; + } + } +} + +// Parses the equivalent of the regex +// \x1b\[(3|4)8;5;(8|9|1[0-5])m +// for intense or +// \x1b\[(3|4)([0-7])m +// for normal +fn parse_color(mut bytes: Bytes<'_>) -> Option<(Intense, Color, FgBg)> { + parse_prefix(&mut bytes)?; + + let fg_bg = FgBg::new(bytes.next()?)?; + let (intense, color) = match bytes.next()? { + b @ b'0'..=b'7' => (Intense::No, normal_color_ansi_from_byte(b)?), + b'8' => { + if &[bytes.next()?, bytes.next()?, bytes.next()?] != b";5;" { + return None; + } + (Intense::Yes, parse_intense_color_ansi(&mut bytes)?) + } + _ => return None, + }; + + parse_suffix(&mut bytes)?; + Some((intense, color, fg_bg)) +} + +// Parses the equivalent of the regex +// \x1b\[([1-8])m +fn parse_attr(mut bytes: Bytes<'_>) -> Option { + parse_prefix(&mut bytes)?; + let attr = match bytes.next()? { + attr @ b'1'..=b'8' => attr, + _ => return None, + }; + parse_suffix(&mut bytes)?; + Some(attr) +} + +fn parse_prefix(bytes: &mut Bytes<'_>) -> Option<()> { + if bytes.next()? == b'[' { + Some(()) + } else { + None + } +} + +fn parse_intense_color_ansi(bytes: &mut Bytes<'_>) -> Option { + let color = match bytes.next()? { + b'8' => Color::Black, + b'9' => Color::Red, + b'1' => match bytes.next()? { + b'0' => Color::Green, + b'1' => Color::Yellow, + b'2' => Color::Blue, + b'3' => Color::Magenta, + b'4' => Color::Cyan, + b'5' => Color::White, + _ => return None, + }, + _ => return None, + }; + Some(color) +} + +fn normal_color_ansi_from_byte(b: u8) -> Option { + let color = match b { + b'0' => Color::Black, + b'1' => Color::Red, + b'2' => Color::Green, + b'3' => Color::Yellow, + b'4' => Color::Blue, + b'5' => Color::Magenta, + b'6' => Color::Cyan, + b'7' => Color::White, + _ => return None, + }; + Some(color) +} + +fn parse_suffix(bytes: &mut Bytes<'_>) -> Option<()> { + if bytes.next()? == b'm' { + Some(()) + } else { + None } } From a2ad42fca984658c6833b4c9d991c052213f3898 Mon Sep 17 00:00:00 2001 From: Lovecraftian Horror Date: Thu, 12 Jan 2023 19:23:27 -0700 Subject: [PATCH 2/2] Add in a couple of basic unit tests --- src/windows_term/colors.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/windows_term/colors.rs b/src/windows_term/colors.rs index ad41eed0..dc8209de 100644 --- a/src/windows_term/colors.rs +++ b/src/windows_term/colors.rs @@ -1,5 +1,3 @@ -// TODO: add tests - use std::io; use std::mem; use std::os::windows::io::AsRawHandle; @@ -316,6 +314,7 @@ pub fn console_colors(out: &Term, mut con: Console, bytes: &[u8]) -> io::Result< Ok(()) } +#[derive(Debug, PartialEq, Eq)] enum FgBg { Foreground, Background, @@ -343,7 +342,7 @@ fn driver(parse: fn(Bytes<'_>) -> Option, part: &str) -> Option { } } -// Parses the equivalent of the regex +// `driver(parse_color, s)` parses the equivalent of the regex // \x1b\[(3|4)8;5;(8|9|1[0-5])m // for intense or // \x1b\[(3|4)([0-7])m @@ -367,7 +366,7 @@ fn parse_color(mut bytes: Bytes<'_>) -> Option<(Intense, Color, FgBg)> { Some((intense, color, fg_bg)) } -// Parses the equivalent of the regex +// `driver(parse_attr, s)` parses the equivalent of the regex // \x1b\[([1-8])m fn parse_attr(mut bytes: Bytes<'_>) -> Option { parse_prefix(&mut bytes)?; @@ -427,3 +426,26 @@ fn parse_suffix(bytes: &mut Bytes<'_>) -> Option<()> { None } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn color_parsing() { + let intense_color = "leading bytes \x1b[38;5;10m trailing bytes"; + let parsed = driver(parse_color, intense_color).unwrap(); + assert_eq!(parsed, (Intense::Yes, Color::Green, FgBg::Foreground)); + + let normal_color = "leading bytes \x1b[40m trailing bytes"; + let parsed = driver(parse_color, normal_color).unwrap(); + assert_eq!(parsed, (Intense::No, Color::Black, FgBg::Background)); + } + + #[test] + fn attr_parsing() { + let attr = "leading bytes \x1b[1m trailing bytes"; + let parsed = driver(parse_attr, attr).unwrap(); + assert_eq!(parsed, b'1'); + } +}