1use std::{borrow::Cow, cmp::min, fmt, ops::Deref};
2
3#[derive(PartialEq)]
4enum AnsiToken {
5 Escape,
6 Opening,
7 Code,
8}
9
10#[derive(Debug)]
11pub struct AnsiSlice<'a> {
12 pub slice: Cow<'a, str>,
13 pub len: usize,
14 pub needs_rst: bool,
15}
16
17impl<'a> Deref for AnsiSlice<'a> {
18 type Target = str;
19
20 fn deref(&self) -> &Self::Target {
21 self.slice.as_ref()
22 }
23}
24
25impl<'a> PartialEq<&str> for AnsiSlice<'a> {
26 fn eq(&self, other: &&str) -> bool {
27 self.slice.as_ref() == *other
28 }
29}
30
31impl<'a> PartialEq<String> for AnsiSlice<'a> {
32 fn eq(&self, other: &String) -> bool {
33 self.slice.as_ref() == other.as_str()
34 }
35}
36
37impl<'a> AnsiSlice<'a> {
38 pub fn tupled(&'a self) -> (&'a str, usize) {
39 (self.slice.as_ref(), self.len)
40 }
41
42 pub fn owned(self) -> String {
43 match self.slice {
44 Cow::Owned(text) => text,
45 Cow::Borrowed(text) => text.to_string(),
46 }
47 }
48}
49
50#[derive(Debug)]
51pub struct AnsiSegment<'a> {
56 pub sgr_code: Option<Cow<'a, str>>,
57 pub rst_code: Option<Cow<'a, str>>,
58 pub text: &'a str,
59
60 is_initial: bool,
62}
63
64#[derive(Debug, Default)]
65pub struct AnsiString<'a> {
69 len: usize,
70 segments: Vec<AnsiSegment<'a>>,
71}
72
73impl<'a> fmt::Display for AnsiSegment<'a> {
74 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75 let sgr = self.sgr_code.as_deref().unwrap_or("");
76 let rst = self.rst_code.as_deref().unwrap_or("");
77 write!(f, "{}{}{}", sgr, self.text, rst)
78 }
79}
80
81impl<'a> AnsiSegment<'a> {
82 pub fn len(&self) -> usize {
83 self.text.chars().count() + self.is_initial as usize
84 }
85
86 fn size(&self) -> usize {
92 self.text.len() + self.sgr_size() + self.rst_size() + self.is_initial as usize
93 }
94 fn has_sgr_code(&self) -> bool {
95 self.sgr_code.is_some()
96 }
97 fn has_rst_code(&self) -> bool {
98 self.rst_code.is_some()
99 }
100 fn sgr_size(&self) -> usize {
103 if self.is_sgr_owned() {
104 return 0;
105 }
106 self.sgr_code.as_ref().map(|c| c.len()).unwrap_or(0)
107 }
108 fn rst_size(&self) -> usize {
109 self.rst_code.as_ref().map(|c| c.len()).unwrap_or(0)
110 }
111 fn is_sgr_owned(&self) -> bool {
112 matches!(self.sgr_code, Some(Cow::Owned(_)))
113 }
114}
115
116impl<'a> AnsiString<'a> {
117 pub fn new(input: &'a str) -> Self {
118 build_ansi_string(input)
119 }
120 pub fn with_sgr(mut self, codes: Option<String>) -> Self {
121 if let Some(codes) = codes
122 && !codes.is_empty()
123 && let Some(seg) = self.segments.first_mut()
124 {
125 seg.sgr_code = Some(Cow::Owned(codes))
126 }
127 self
128 }
129 pub fn segments(&self) -> &[AnsiSegment<'a>] {
130 &self.segments
131 }
132 pub fn is_empty(&self) -> bool {
133 self.len == 0
134 }
135 pub fn append(&mut self, mut str: AnsiString<'a>) {
136 if !self.segments.is_empty()
137 && let Some(seg) = str.segments.first_mut()
138 {
139 assert!(!seg.is_sgr_owned());
141
142 seg.is_initial = true;
143 self.len += 1;
144 }
145 self.len += str.len();
146 self.segments.extend(str.segments);
147 }
148
149 pub fn len(&self) -> usize {
152 self.len
153 }
154
155 pub fn codes_to_continue(&self) -> String {
159 let codes = self
160 .segments
161 .iter()
162 .rev()
163 .take_while(|seg| !seg.has_rst_code())
164 .filter_map(|seg| seg.sgr_code.as_ref().map(|c| c.as_ref()))
165 .collect::<Vec<_>>();
166
167 codes.join("")
168 }
169
170 pub fn get(&self, len: usize) -> AnsiSlice<'a> {
176 let (text_len, bytes, is_terminating_rst) =
177 self.segments
178 .iter()
179 .fold((0, 0, true), |(text_len, byte_size, is_reset), segment| {
180 if text_len >= len {
181 (text_len, byte_size, is_reset)
182 } else {
183 let slen = text_len + segment.len();
184 let diff = slen.saturating_sub(len);
185 (
186 min(slen, len),
187 byte_size
188 + if diff == 0 {
189 segment.size()
190 } else {
191 segment.sgr_size()
192 + segment
193 .text
194 .char_indices()
195 .nth(segment.len() - diff)
196 .map_or(segment.text.len(), |(byte_idx, _)| byte_idx)
197 },
198 (segment.has_rst_code() && diff == 0)
199 || (is_reset && !segment.has_sgr_code()),
200 )
201 }
202 });
203
204 if let Some((ptr, seg)) = self.slice_ptr() {
205 let str =
206 unsafe { std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, bytes)) };
207
208 return AnsiSlice {
209 slice: if let Some(sgr) = seg.sgr_code.as_ref()
210 && seg.is_sgr_owned()
211 {
212 Cow::Owned(format!("{sgr}{str}"))
213 } else {
214 Cow::Borrowed(str)
215 },
216 len: text_len,
217 needs_rst: !is_terminating_rst,
218 };
219 }
220 AnsiSlice {
221 slice: Cow::Borrowed(""),
222 len: 0,
223 needs_rst: false,
224 }
225 }
226
227 fn push_segment(&mut self, segment: AnsiSegment<'a>) {
228 self.len += segment.len() + !self.is_empty() as usize;
229 self.segments.push(segment);
230 }
231
232 fn slice_ptr(&self) -> Option<(*const u8, &AnsiSegment<'a>)> {
233 if let Some(seg) = self.segments.first() {
234 return if !seg.has_sgr_code() || seg.is_sgr_owned() {
235 Some((seg.text.as_ptr(), seg))
236 } else {
237 seg.sgr_code.as_ref().map(|c| (c.as_ptr(), seg))
238 };
239 }
240 None
241 }
242}
243
244pub fn build_ansi_string<'a>(input: &'a str) -> AnsiString<'a> {
245 let mut result = AnsiString::default();
246 let mut expected = AnsiToken::Escape;
247 let mut last_code = (0, 0, false); let mut sequence;
249
250 let mut current_code_start = 0;
251 let mut text_byte_size: usize = 0;
252
253 for (pos, ch) in input.char_indices() {
254 text_byte_size += ch.len_utf8();
255 match ch {
256 '\x1b' if expected == AnsiToken::Escape => {
257 expected = AnsiToken::Opening;
258 current_code_start = pos;
259 }
260 '[' if expected == AnsiToken::Opening => expected = AnsiToken::Code,
261 'm' if expected == AnsiToken::Code => {
262 sequence = &input[current_code_start..pos + 1];
264 text_byte_size = text_byte_size.saturating_sub(sequence.len());
265
266 let is_reset = sequence == "\x1b[0m";
267 let is_text = text_byte_size > 0;
268 let (last_code_start, last_code_end, was_reset) = last_code;
269
270 if is_text {
272 result.push_segment(AnsiSegment {
273 text: &input[last_code_end..last_code_end + text_byte_size],
274 sgr_code: if !was_reset && last_code_end > last_code_start {
275 Some(Cow::Borrowed(&input[last_code_start..last_code_end]))
276 } else {
277 None
278 },
279 rst_code: if is_reset {
280 Some(Cow::Borrowed(sequence))
281 } else {
282 None
283 },
284 is_initial: false,
285 });
286 }
287 last_code = (
288 if is_text || was_reset {
289 current_code_start
290 } else {
291 last_code_start
292 },
293 pos + 1,
294 is_reset,
295 );
296 text_byte_size = 0;
297 expected = AnsiToken::Escape
298 }
299 '0'..='9' | ';' | ':' if expected == AnsiToken::Code => {
300 continue;
301 }
302 _ => {
303 }
305 }
306 }
307
308 if text_byte_size > 0 || input.is_empty() {
311 let seg = AnsiSegment {
312 sgr_code: if !last_code.2 && last_code.0 != last_code.1 {
313 Some(Cow::Borrowed(&input[last_code.0..last_code.1]))
314 } else {
315 None
316 },
317 rst_code: None,
318 text: &input[last_code.1..],
319 is_initial: false,
320 };
321 result.push_segment(seg)
322 }
323 result
324}
325
326#[macro_export]
327macro_rules! assert_segments {
328 ($string:expr, [$($segment:tt),+]) => {
329 {
330 let str = format!($string);
331 let ansi = $crate::AnsiString::new(&str);
332 let segments = ansi.segments();
333 let mut segment_index = 0;
334
335 $(
336 assert_segments!(@verify_segment segments[segment_index], $segment);
337 segment_index += 1;
338 )+
339 assert_eq!(segments.len(), segment_index, "Expected {} segments, found {}", segment_index, segments.len());
340 }
341 };
342 (@verify_segment $seg:expr, { $($field:ident => $value:tt),* }) => {
343 let seg = &$seg;
344 $(
345 assert_segments!(@check_field seg, $field, $value);
346 )*
347 };
348 (@check_field $seg:expr, len, $expected:expr) => {
349 assert_eq!($seg.len(), $expected);
350 };
351 (@check_field $seg:expr, txt, $expected:expr) => {
352 assert_eq!($seg.text, $expected);
353 };
354 (@check_field $seg:expr, sgr, $expected:literal) => {
355 let formatted = format!($expected);
356 assert_eq!($seg.sgr_code, Some(std::borrow::Cow::Borrowed(formatted.as_str())));
357 };
358 (@check_field $seg:expr, sgr, None) => {
359 assert_eq!($seg.sgr_code, None)
360 };
361 (@check_field $seg:expr, rst, $expected:literal) => {
362 let formatted = format!($expected);
363 assert_eq!($seg.rst_code, Some(std::borrow::Cow::Borrowed(formatted.as_str())));
364 };
365 (@check_field $seg:expr, rst, None) => {
366 assert_eq!($seg.rst_code, None)
367 };
368 }