use std::{borrow::Cow, cmp::min, fmt, ops::Deref};
#[derive(PartialEq)]
enum AnsiToken {
Escape,
Opening,
Code,
}
#[derive(Debug)]
pub struct AnsiSlice<'a> {
pub slice: Cow<'a, str>,
pub len: usize,
pub needs_rst: bool,
}
impl<'a> Deref for AnsiSlice<'a> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.slice.as_ref()
}
}
impl<'a> PartialEq<&str> for AnsiSlice<'a> {
fn eq(&self, other: &&str) -> bool {
self.slice.as_ref() == *other
}
}
impl<'a> PartialEq<String> for AnsiSlice<'a> {
fn eq(&self, other: &String) -> bool {
self.slice.as_ref() == other.as_str()
}
}
impl<'a> AnsiSlice<'a> {
pub fn tupled(&'a self) -> (&'a str, usize) {
(self.slice.as_ref(), self.len)
}
pub fn owned(self) -> String {
match self.slice {
Cow::Owned(text) => text,
Cow::Borrowed(text) => text.to_string(),
}
}
}
#[derive(Debug)]
pub struct AnsiSegment<'a> {
pub sgr_code: Option<Cow<'a, str>>,
pub rst_code: Option<Cow<'a, str>>,
pub text: &'a str,
is_initial: bool,
}
#[derive(Debug, Default)]
pub struct AnsiString<'a> {
len: usize,
segments: Vec<AnsiSegment<'a>>,
}
impl<'a> fmt::Display for AnsiSegment<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let sgr = self.sgr_code.as_deref().unwrap_or("");
let rst = self.rst_code.as_deref().unwrap_or("");
write!(f, "{}{}{}", sgr, self.text, rst)
}
}
impl<'a> AnsiSegment<'a> {
pub fn len(&self) -> usize {
self.text.chars().count() + self.is_initial as usize
}
fn size(&self) -> usize {
self.text.len() + self.sgr_size() + self.rst_size() + self.is_initial as usize
}
fn has_sgr_code(&self) -> bool {
self.sgr_code.is_some()
}
fn has_rst_code(&self) -> bool {
self.rst_code.is_some()
}
fn sgr_size(&self) -> usize {
if self.is_sgr_owned() {
return 0;
}
self.sgr_code.as_ref().map(|c| c.len()).unwrap_or(0)
}
fn rst_size(&self) -> usize {
self.rst_code.as_ref().map(|c| c.len()).unwrap_or(0)
}
fn is_sgr_owned(&self) -> bool {
matches!(self.sgr_code, Some(Cow::Owned(_)))
}
}
impl<'a> AnsiString<'a> {
pub fn new(input: &'a str) -> Self {
build_ansi_string(input)
}
pub fn with_sgr(mut self, codes: Option<String>) -> Self {
if let Some(codes) = codes
&& !codes.is_empty()
&& let Some(seg) = self.segments.first_mut()
{
seg.sgr_code = Some(Cow::Owned(codes))
}
self
}
pub fn segments(&self) -> &[AnsiSegment<'a>] {
&self.segments
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn append(&mut self, mut str: AnsiString<'a>) {
if !self.segments.is_empty()
&& let Some(seg) = str.segments.first_mut()
{
assert!(!seg.is_sgr_owned());
seg.is_initial = true;
self.len += 1;
}
self.len += str.len();
self.segments.extend(str.segments);
}
pub fn len(&self) -> usize {
self.len
}
pub fn codes_to_continue(&self) -> String {
let codes = self
.segments
.iter()
.rev()
.take_while(|seg| !seg.has_rst_code())
.filter_map(|seg| seg.sgr_code.as_ref().map(|c| c.as_ref()))
.collect::<Vec<_>>();
codes.join("")
}
pub fn get(&self, len: usize) -> AnsiSlice<'a> {
let (text_len, bytes, is_terminating_rst) =
self.segments
.iter()
.fold((0, 0, true), |(text_len, byte_size, is_reset), segment| {
if text_len >= len {
(text_len, byte_size, is_reset)
} else {
let slen = text_len + segment.len();
let diff = slen.saturating_sub(len);
(
min(slen, len),
byte_size
+ if diff == 0 {
segment.size()
} else {
segment.sgr_size()
+ segment
.text
.char_indices()
.nth(segment.len() - diff)
.map_or(segment.text.len(), |(byte_idx, _)| byte_idx)
},
(segment.has_rst_code() && diff == 0)
|| (is_reset && !segment.has_sgr_code()),
)
}
});
if let Some((ptr, seg)) = self.slice_ptr() {
let str =
unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, bytes)) };
return AnsiSlice {
slice: if let Some(sgr) = seg.sgr_code.as_ref()
&& seg.is_sgr_owned()
{
Cow::Owned(format!("{sgr}{str}"))
} else {
Cow::Borrowed(str)
},
len: text_len,
needs_rst: !is_terminating_rst,
};
}
AnsiSlice {
slice: Cow::Borrowed(""),
len: 0,
needs_rst: false,
}
}
fn push_segment(&mut self, segment: AnsiSegment<'a>) {
self.len += segment.len() + !self.is_empty() as usize;
self.segments.push(segment);
}
fn slice_ptr(&self) -> Option<(*const u8, &AnsiSegment<'a>)> {
if let Some(seg) = self.segments.first() {
return if !seg.has_sgr_code() || seg.is_sgr_owned() {
Some((seg.text.as_ptr(), seg))
} else {
seg.sgr_code.as_ref().map(|c| (c.as_ptr(), seg))
};
}
None
}
}
pub fn build_ansi_string<'a>(input: &'a str) -> AnsiString<'a> {
let mut result = AnsiString::default();
let mut expected = AnsiToken::Escape;
let mut last_code = (0, 0, false); let mut sequence;
let mut current_code_start = 0;
let mut text_byte_size: usize = 0;
for (pos, ch) in input.char_indices() {
text_byte_size += ch.len_utf8();
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 => {
sequence = &input[current_code_start..pos + 1];
text_byte_size = text_byte_size.saturating_sub(sequence.len());
let is_reset = sequence == "\x1b[0m";
let is_text = text_byte_size > 0;
let (last_code_start, last_code_end, was_reset) = last_code;
if is_text {
result.push_segment(AnsiSegment {
text: &input[last_code_end..last_code_end + text_byte_size],
sgr_code: if !was_reset && last_code_end > last_code_start {
Some(Cow::Borrowed(&input[last_code_start..last_code_end]))
} else {
None
},
rst_code: if is_reset {
Some(Cow::Borrowed(sequence))
} else {
None
},
is_initial: false,
});
}
last_code = (
if is_text || was_reset {
current_code_start
} else {
last_code_start
},
pos + 1,
is_reset,
);
text_byte_size = 0;
expected = AnsiToken::Escape
}
'0'..='9' | ';' | ':' if expected == AnsiToken::Code => {
continue;
}
_ => {
}
}
}
if text_byte_size > 0 || input.is_empty() {
let seg = AnsiSegment {
sgr_code: if !last_code.2 && last_code.0 != last_code.1 {
Some(Cow::Borrowed(&input[last_code.0..last_code.1]))
} else {
None
},
rst_code: None,
text: &input[last_code.1..],
is_initial: false,
};
result.push_segment(seg)
}
result
}
#[macro_export]
macro_rules! assert_segments {
($string:expr, [$($segment:tt),+]) => {
{
let str = format!($string);
let ansi = $crate::AnsiString::new(&str);
let segments = ansi.segments();
let mut segment_index = 0;
$(
assert_segments!(@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_segments!(@check_field seg, $field, $value);
)*
};
(@check_field $seg:expr, len, $expected:expr) => {
assert_eq!($seg.len(), $expected);
};
(@check_field $seg:expr, txt, $expected:expr) => {
assert_eq!($seg.text, $expected);
};
(@check_field $seg:expr, sgr, $expected:literal) => {
let formatted = format!($expected);
assert_eq!($seg.sgr_code, Some(std::borrow::Cow::Borrowed(formatted.as_str())));
};
(@check_field $seg:expr, sgr, None) => {
assert_eq!($seg.sgr_code, None)
};
(@check_field $seg:expr, rst, $expected:literal) => {
let formatted = format!($expected);
assert_eq!($seg.rst_code, Some(std::borrow::Cow::Borrowed(formatted.as_str())));
};
(@check_field $seg:expr, rst, None) => {
assert_eq!($seg.rst_code, None)
};
}