use crate::ansi::code::AnsiCode;
use nom::{
branch::alt,
bytes::complete::{tag, take, take_till, take_while},
character::{
complete::{char, i64, not_line_ending, u8},
is_alphabetic,
},
combinator::{map_res, opt, recognize, value},
error::{self, FromExternalError},
multi::fold_many0,
sequence::{delimited, preceded, terminated, tuple},
IResult, Parser,
};
use smallvec::{SmallVec, ToSmallVec};
use std::str::FromStr;
use tui::{
style::{Color, Modifier, Style, Stylize},
text::{Line, Span, Text},
};
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
enum ColorType {
EightBit,
TrueColor,
}
#[derive(Debug, Clone, PartialEq)]
struct AnsiItem {
code: AnsiCode,
color: Option<Color>,
}
#[derive(Debug, Clone, PartialEq)]
struct AnsiStates {
pub items: smallvec::SmallVec<[AnsiItem; 2]>,
pub style: Style,
}
impl From<AnsiStates> for tui::style::Style {
fn from(states: AnsiStates) -> Self {
let mut style = states.style;
for item in states.items {
match item.code {
AnsiCode::Bold => style = style.add_modifier(Modifier::BOLD),
AnsiCode::Faint => style = style.add_modifier(Modifier::DIM),
AnsiCode::Normal => {
style = style
.remove_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM);
}
AnsiCode::Italic => {
style = style.add_modifier(Modifier::ITALIC);
}
AnsiCode::Underline => {
style = style.add_modifier(Modifier::UNDERLINED);
}
AnsiCode::SlowBlink => {
style = style.add_modifier(Modifier::SLOW_BLINK);
}
AnsiCode::RapidBlink => {
style = style.add_modifier(Modifier::RAPID_BLINK);
}
AnsiCode::Reverse => {
style = style.add_modifier(Modifier::REVERSED);
}
AnsiCode::Conceal => {
style = style.add_modifier(Modifier::HIDDEN);
}
AnsiCode::CrossedOut => {
style = style.add_modifier(Modifier::CROSSED_OUT);
}
AnsiCode::DefaultForegroundColor => {
style = style.fg(Color::Reset);
}
AnsiCode::SetForegroundColor => {
if let Some(color) = item.color {
style = style.fg(color);
}
}
AnsiCode::ForegroundColor(color) => style = style.fg(color),
AnsiCode::Reset => style = style.fg(Color::Reset),
_ => (),
}
}
style
}
}
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn text(mut s: &[u8]) -> IResult<&[u8], Text<'static>> {
let mut lines = Vec::new();
let mut last_style = Style::new();
while let Ok((remaining, (line, style))) = line(last_style)(s) {
lines.push(line);
last_style = style;
s = remaining;
if s.is_empty() {
break;
}
}
Ok((s, Text::from(lines)))
}
#[cfg(feature = "zero-copy")]
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn text_fast(mut s: &[u8]) -> IResult<&[u8], Text<'_>> {
let mut lines = Vec::new();
let mut last = Style::new();
while let Ok((c, (line, style))) = line_fast(last)(s) {
lines.push(line);
last = style;
s = c;
if s.is_empty() {
break;
}
}
Ok((s, Text::from(lines)))
}
fn line(
style: Style,
) -> impl Fn(&[u8]) -> IResult<&[u8], (Line<'static>, Style)> {
move |s: &[u8]| -> IResult<&[u8], (Line<'static>, Style)> {
let (s, mut text) = not_line_ending(s)?;
let (s, _) = opt(alt((tag("\r\n"), tag("\n"))))(s)?;
let mut spans = Vec::new();
let mut last_style = style;
while let Ok((remaining, span)) = span(last_style)(text) {
last_style = last_style.patch(span.style);
if !span.content.is_empty() {
spans.push(span);
}
text = remaining;
if text.is_empty() {
break;
}
}
Ok((s, (Line::from(spans), last_style)))
}
}
#[cfg(feature = "zero-copy")]
fn line_fast(
style: Style,
) -> impl Fn(&[u8]) -> IResult<&[u8], (Line<'_>, Style)> {
move |s: &[u8]| -> IResult<&[u8], (Line<'_>, Style)> {
let (s, mut text) = not_line_ending(s)?;
let (s, _) = opt(alt((tag("\r\n"), tag("\n"))))(s)?;
let mut spans = Vec::new();
let mut last = style;
while let Ok((s, span)) = span_fast(last)(text) {
last = last.patch(span.style);
if !span.content.is_empty() {
spans.push(span);
}
text = s;
if text.is_empty() {
break;
}
}
Ok((s, (Line::from(spans), last)))
}
}
fn span(
last: Style,
) -> impl Fn(&[u8]) -> IResult<&[u8], Span<'static>, nom::error::Error<&[u8]>>
{
move |s: &[u8]| -> IResult<&[u8], Span<'static>> {
let mut last_style = last;
let (s, maybe_style) = opt(style(last_style))(s)?;
#[cfg(feature = "simd")]
let (s, text) = map_res(take_while(|c| c != b'\x1b'), |t| {
simdutf8::basic::from_utf8(t)
})(s)?;
#[cfg(not(feature = "simd"))]
let (s, text) =
map_res(take_while(|c| c != b'\x1b'), |t| std::str::from_utf8(t))(
s,
)?;
if let Some(st) = maybe_style.flatten() {
last_style = last_style.patch(st);
}
Ok((s, Span::styled(text.to_owned(), last_style)))
}
}
#[cfg(feature = "zero-copy")]
fn span_fast(
last: Style,
) -> impl Fn(&[u8]) -> IResult<&[u8], Span<'_>, nom::error::Error<&[u8]>> {
move |s: &[u8]| -> IResult<&[u8], Span<'_>> {
let mut last = last;
let (s, style) = opt(style(last))(s)?;
#[cfg(feature = "simd")]
let (s, text) = map_res(take_while(|c| c != b'\x1b'), |t| {
simdutf8::basic::from_utf8(t)
})(s)?;
#[cfg(not(feature = "simd"))]
let (s, text) =
map_res(take_while(|c| c != b'\x1b'), |t| std::str::from_utf8(t))(
s,
)?;
if let Some(style) = style.flatten() {
last = last.patch(style);
}
Ok((s, Span::styled(text, last)))
}
}
fn style(
style: Style,
) -> impl Fn(&[u8]) -> IResult<&[u8], Option<Style>, nom::error::Error<&[u8]>>
{
move |s: &[u8]| -> IResult<&[u8], Option<Style>> {
let (s, r) = match opt(ansi_sgr_code)(s)? {
(s, Some(r)) => {
if r.is_empty() {
let mut sv = SmallVec::<[AnsiItem; 2]>::new();
sv.push(AnsiItem {
code: AnsiCode::Reset,
color: None,
});
(s, Some(sv))
} else {
(s, Some(r))
}
}
(s, None) => {
let (s, _) = any_escape_sequence(s)?;
(s, None)
}
};
Ok((s, r.map(|r| Style::from(AnsiStates { style, items: r }))))
}
}
fn ansi_sgr_code(
s: &[u8],
) -> IResult<&[u8], smallvec::SmallVec<[AnsiItem; 2]>, nom::error::Error<&[u8]>>
{
delimited(
tag("\x1b["),
fold_many0(
ansi_sgr_item,
smallvec::SmallVec::new,
|mut items, item| {
items.push(item);
items
},
),
char('m'),
)(s)
}
fn any_escape_sequence(s: &[u8]) -> IResult<&[u8], Option<&[u8]>> {
let (input, garbage) = preceded(
char('\x1b'),
opt(alt((
delimited(char('['), take_till(is_alphabetic), opt(take(1u8))),
delimited(char(']'), take_till(|c| c == b'\x07'), opt(take(1u8))),
))),
)(s)?;
Ok((input, garbage))
}
fn ansi_sgr_item(s: &[u8]) -> IResult<&[u8], AnsiItem> {
let (s, c) = u8(s)?;
let code = AnsiCode::from(c);
let (s, color) = match code {
AnsiCode::SetForegroundColor | AnsiCode::SetBackgroundColor => {
let (s, _) = opt(tag(";"))(s)?;
let (s, color) = color(s)?;
(s, Some(color))
}
_ => (s, None),
};
let (s, _) = opt(tag(";"))(s)?;
Ok((s, AnsiItem { code, color }))
}
fn color(s: &[u8]) -> IResult<&[u8], Color> {
let (s, c_type) = color_type(s)?;
let (s, _) = opt(tag(";"))(s)?;
match c_type {
ColorType::TrueColor => {
let (s, (r, _, g, _, b)) =
tuple((u8, tag(";"), u8, tag(";"), u8))(s)?;
Ok((s, Color::Rgb(r, g, b)))
}
ColorType::EightBit => {
let (s, index) = u8(s)?;
Ok((s, Color::Indexed(index)))
}
}
}
fn color_type(s: &[u8]) -> IResult<&[u8], ColorType> {
let (s, t) = i64(s)?;
let (s, _) = tag(";")(s)?;
match t {
2 => Ok((s, ColorType::TrueColor)),
5 => Ok((s, ColorType::EightBit)),
_ => Err(nom::Err::Error(nom::error::Error::new(
s,
nom::error::ErrorKind::Alt,
))),
}
}
#[test]
fn color_test() {
let c = color(b"2;255;255;255").unwrap();
assert_eq!(c.1, Color::Rgb(255, 255, 255));
let c = color(b"5;255").unwrap();
assert_eq!(c.1, Color::Indexed(255));
let err = color(b"10;255");
assert_ne!(err, Ok(c));
}
#[test]
fn test_color_reset() {
let t = text(b"\x1b[33msome arbitrary text\x1b[0m\nmore text")
.unwrap()
.1;
assert_eq!(
t,
Text::from(vec![
Line::from(vec![Span::styled(
"some arbitrary text",
Style::default().fg(Color::Yellow)
),]),
Line::from(Span::from("more text").fg(Color::Reset)),
])
);
}
#[test]
fn test_color_reset_implicit_escape() {
let t = text(b"\x1b[33msome arbitrary text\x1b[m\nmore text")
.unwrap()
.1;
assert_eq!(
t,
Text::from(vec![
Line::from(vec![Span::styled(
"some arbitrary text",
Style::default().fg(Color::Yellow)
),]),
Line::from(Span::from("more text").fg(Color::Reset)),
])
);
}
#[test]
fn ansi_items_test() {
let sc = Style::default();
let t = style(sc)(b"\x1b[38;2;3;3;3m").unwrap().1.unwrap();
assert_eq!(
t,
Style::from(AnsiStates {
style: sc,
items: vec![AnsiItem {
code: AnsiCode::SetForegroundColor,
color: Some(Color::Rgb(3, 3, 3))
}]
.into()
})
);
assert_eq!(
style(sc)(b"\x1b[38;5;3m").unwrap().1.unwrap(),
Style::from(AnsiStates {
style: sc,
items: vec![AnsiItem {
code: AnsiCode::SetForegroundColor,
color: Some(Color::Indexed(3))
}]
.into()
})
);
assert_eq!(
style(sc)(b"\x1b[38;5;3;48;5;3m").unwrap().1.unwrap(),
Style::from(AnsiStates {
style: sc,
items: vec![
AnsiItem {
code: AnsiCode::SetForegroundColor,
color: Some(Color::Indexed(3))
},
AnsiItem {
code: AnsiCode::SetBackgroundColor,
color: Some(Color::Indexed(3))
}
]
.into()
})
);
assert_eq!(
style(sc)(b"\x1b[38;5;3;48;5;3;1m").unwrap().1.unwrap(),
Style::from(AnsiStates {
style: sc,
items: vec![
AnsiItem {
code: AnsiCode::SetForegroundColor,
color: Some(Color::Indexed(3))
},
AnsiItem {
code: AnsiCode::SetBackgroundColor,
color: Some(Color::Indexed(3))
},
AnsiItem {
code: AnsiCode::Bold,
color: None
}
]
.into()
})
);
}