use std::cmp::min;
use smallvec::SmallVec;
pub const RST_CODE: &str = "\x1b[0m";
type AnsiSegment<'a> = (SmallVec<[&'a str; 4]>, &'a str, usize, bool, bool);
#[derive(Default)]
pub struct AnsiString<'a> {
pub slice: &'a str,
pub c2c: Option<String>,
pub len: usize,
pub needs_rst: bool,
}
#[derive(PartialEq)]
enum AnsiToken {
Escape,
Opening,
Code,
}
#[derive(PartialEq)]
pub enum Overflow {
WordWrap,
Truncate,
}
#[derive(Debug)]
enum Segment<'a> {
Word(&'a str, usize),
Term(&'a str, usize),
}
#[derive(Debug, Default)]
struct CodeQueue<'a> {
codes_to_continue: SmallVec<[&'a str; 4]>, codes_to_collect: SmallVec<[&'a str; 4]>,
reset_after_get: bool,
}
impl<'a> CodeQueue<'a> {
pub fn codes_to_continue(&mut self) -> Option<String> {
let code_seq = if self.codes_to_continue.is_empty() {
None
} else {
let capacity = self.codes_to_continue.iter().map(|s| s.len()).sum();
let mut codes = String::with_capacity(capacity);
for s in &self.codes_to_continue {
codes.push_str(s);
}
Some(codes)
};
if self.reset_after_get {
self.codes_to_continue.clear();
self.reset_after_get = false;
}
self.codes_to_continue
.extend_from_slice(&self.codes_to_collect);
self.codes_to_collect.clear();
code_seq
}
pub fn collect(&mut self, codes: SmallVec<[&'a str; 4]>) {
self.codes_to_collect.extend(codes);
}
pub fn clear(&mut self) {
self.reset_after_get = true;
self.codes_to_collect.clear();
}
pub fn has_codes_to_continue(&self) -> bool {
!self.codes_to_continue.is_empty()
}
}
impl<'a> Segment<'a> {
fn text(&self) -> &'a str {
match self {
Segment::Word(txt, _) | Segment::Term(txt, _) => txt,
}
}
fn pos(&self) -> usize {
match self {
Segment::Word(_, pos) | Segment::Term(_, pos) => *pos,
}
}
}
impl<'a> AnsiString<'a> {
pub fn build(c2c: Option<String>, slice: &'a str, len: usize, needs_rst: bool) -> Self {
Self {
slice,
len,
c2c,
needs_rst,
}
}
}
pub fn build_string<'a>(
input: &'a str,
hspace: usize,
vspace: usize,
overflow: &Overflow,
) -> Vec<AnsiString<'a>> {
if hspace == 0 {
return vec![];
}
let mut line_reset = false;
let mut queue = CodeQueue::default();
let mut result = Vec::with_capacity(vspace);
let mut str_pos = 0;
let mut end_pos = 0;
let mut txt_len = 0;
let mut segments = build_segments_iter(input, overflow).peekable();
while let Some(seg) = segments.next() {
let is_last_str = segments.peek().is_none();
let is_init_str = txt_len == 0;
let is_term_str = matches!(seg, Segment::Term(_, _));
let eol = is_last_str || is_term_str;
let pos = seg.pos();
let (new_codes, txt, total_len, has_rst, needs_rst) = parse_segment(seg, hspace);
let len = min(total_len, hspace);
let sep_len = (txt_len > 0) as usize;
if txt_len == 0 {
str_pos = pos;
end_pos = pos;
line_reset = queue.has_codes_to_continue();
}
if !is_init_str && txt_len + total_len + sep_len > hspace {
result.push(AnsiString::build(
queue.codes_to_continue(),
&input[str_pos..end_pos],
txt_len,
line_reset,
));
str_pos = pos;
end_pos = pos + txt.len();
txt_len = len;
} else {
end_pos += txt.len() + sep_len;
txt_len += len + sep_len;
}
if has_rst {
queue.clear();
line_reset = needs_rst;
} else {
line_reset = line_reset || needs_rst;
}
queue.collect(new_codes);
if result.len() < vspace && (txt_len == hspace || eol) {
result.push(AnsiString::build(
queue.codes_to_continue(),
&input[str_pos..end_pos],
txt_len,
line_reset,
));
txt_len = 0;
}
if result.len() == vspace {
return result;
}
}
result
}
fn build_segments_iter<'a>(
input: &'a str,
overflow: &Overflow,
) -> Box<dyn Iterator<Item = Segment<'a>> + 'a> {
let input_ptr = input.as_ptr();
match overflow {
Overflow::Truncate => Box::new(
input
.lines()
.map(move |s| Segment::Term(s, s.as_ptr() as usize - input_ptr as usize)),
),
Overflow::WordWrap => Box::new(input.lines().flat_map(move |s| {
let mut iter = s.split(' ').peekable();
std::iter::from_fn(move || {
iter.next().map(|slice| {
let pos = slice.as_ptr() as usize - input_ptr as usize;
if iter.peek().is_none() {
Segment::Term(slice, pos)
} else {
Segment::Word(slice, pos)
}
})
})
})),
}
}
fn parse_segment<'a>(segment: Segment<'a>, len: usize) -> AnsiSegment<'a> {
let mut codes = SmallVec::new();
let mut expected = AnsiToken::Escape;
let mut current_code_start = 0;
let mut txt_len: usize = 0;
let mut end_pos = None;
let mut has_rst = false;
let mut needs_rst = false;
let mut stop_collecting = false;
let input = segment.text();
if input.is_empty() {
return (codes, input, 0, false, false);
}
for (pos, ch) in input.char_indices() {
match ch {
'\x1b' if expected == AnsiToken::Escape => {
expected = AnsiToken::Opening;
current_code_start = pos;
}
'[' if expected == AnsiToken::Opening => expected = AnsiToken::Code,
'm' if expected == AnsiToken::Code => {
let sequence = &input[current_code_start..pos + 1];
let seq_rst = sequence == RST_CODE;
has_rst = seq_rst;
if seq_rst {
codes.clear();
} else {
codes.push(sequence);
}
if !stop_collecting {
needs_rst = !has_rst;
if end_pos.is_some() {
end_pos = Some(pos + 1);
}
}
expected = AnsiToken::Escape
}
'0'..='9' | ';' | ':' if expected == AnsiToken::Code => {
continue;
}
_ if end_pos.is_none() => {
txt_len += 1;
if txt_len == len {
end_pos = Some(pos + ch.len_utf8());
}
expected = AnsiToken::Escape;
}
_ => {
stop_collecting = true;
expected = AnsiToken::Escape;
}
}
}
let slice = &input[0..end_pos.unwrap_or(input.len())];
(codes, slice, txt_len, has_rst, needs_rst)
}
#[macro_export]
macro_rules! assert_ansi_string {
($string:expr, $hspace:expr, $vspace:expr, $overflow:expr, []) => {
let str = format!($string);
let segments = $crate::ansi::build_string(&str, $hspace, $vspace, &$overflow);
assert!(segments.is_empty());
};
($string:expr, $hspace:expr, $vspace:expr, $overflow:expr, [$($segment:tt),*]) => {
{
let str = format!($string);
let segments = $crate::ansi::build_string(&str, $hspace, $vspace, &$overflow);
let mut segment_index = 0;
$(
assert_ansi_string!(@verify_segment segments[segment_index], $segment);
segment_index += 1;
)+
assert_eq!(segments.len(), segment_index, "Expected {} segments, found {}", segment_index, segments.len());
}
};
(@verify_segment $seg:expr, { $($field:ident => $value:tt),* }) => {
let seg = &$seg;
$(
assert_ansi_string!(@check_field seg, $field, $value);
)*
};
(@check_field $seg:expr, len, $expected:expr) => {
assert_eq!($seg.len, $expected);
};
(@check_field $seg:expr, txt, $expected:literal) => {
let formatted = format!($expected);
assert_eq!($seg.slice.as_ref(), formatted);
};
(@check_field $seg:expr, rst, $expected:expr) => {
assert_eq!($seg.needs_rst, $expected);
};
}
#[cfg(test)]
mod tests {
use super::*;
use smallvec::smallvec;
#[test]
fn test_codes_queue_clear() {
let mut queue = CodeQueue::default();
queue.collect(smallvec!["\x1b[31m", "\x1b[1m"]);
assert_eq!(queue.codes_to_continue(), None);
assert!(queue.has_codes_to_continue());
queue.clear();
assert_eq!(
queue.codes_to_continue(),
Some(String::from("\x1b[31m\x1b[1m"))
);
assert!(!queue.has_codes_to_continue());
}
#[test]
fn test_codes_queue_multiple_codes_collected() {
let mut queue = CodeQueue::default();
queue.collect(smallvec!["\x1b[31m"]);
assert_eq!(queue.codes_to_continue(), None);
queue.collect(smallvec!["\x1b[1m"]);
assert_eq!(queue.codes_to_continue(), Some(String::from("\x1b[31m")));
assert_eq!(
queue.codes_to_continue(),
Some(String::from("\x1b[31m\x1b[1m"))
);
}
#[test]
fn test_codes_queue_clear_with_new_codes() {
let mut queue = CodeQueue::default();
queue.collect(smallvec!["\x1b[31m", "\x1b[1m"]);
queue.codes_to_continue();
queue.collect(smallvec!["\x1b[32m"]);
queue.clear();
assert_eq!(
queue.codes_to_continue(),
Some(String::from("\x1b[31m\x1b[1m"))
);
assert!(!queue.has_codes_to_continue());
}
#[test]
fn test_codes_queue_empty_states() {
let mut queue = CodeQueue::default();
assert!(!queue.has_codes_to_continue());
assert_eq!(queue.codes_to_continue(), None);
queue.collect(smallvec![]);
assert_eq!(queue.codes_to_continue(), None);
assert!(!queue.has_codes_to_continue());
queue.clear();
assert_eq!(queue.codes_to_continue(), None);
assert!(!queue.has_codes_to_continue());
}
}