1use regex::Regex;
2
3pub const ANSI_REGEX: &'static str = r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]";
5
6pub const ANSI_PAIR: [[&'static str; 2]; 24] = [
8 ["\x1B[0m", "\x1B[0m"], ["\x1B[1m", "\x1B[22m"], ["\x1B[2m", "\x1B[22m"], ["\x1B[3m", "\x1B[23m"], ["\x1B[4m", "\x1B[24m"], ["\x1B[5m", "\x1B[25m"], ["\x1B[7m", "\x1B[27m"], ["\x1B[8m", "\x1B[28m"], ["\x1B[30m", "\x1B[39m"], ["\x1B[31m", "\x1B[39m"], ["\x1B[32m", "\x1B[39m"], ["\x1B[33m", "\x1B[39m"], ["\x1B[34m", "\x1B[39m"], ["\x1B[35m", "\x1B[39m"], ["\x1B[36m", "\x1B[39m"], ["\x1B[37m", "\x1B[39m"], ["\x1B[40m", "\x1B[49m"], ["\x1B[41m", "\x1B[49m"], ["\x1B[42m", "\x1B[49m"], ["\x1B[43m", "\x1B[49m"], ["\x1B[44m", "\x1B[49m"], ["\x1B[45m", "\x1B[49m"], ["\x1B[46m", "\x1B[49m"], ["\x1B[47m", "\x1B[49m"], ];
33
34#[derive(Debug, Clone, PartialEq)]
36pub enum Alignment {
37 Left,
38 Center,
39 Right,
40}
41
42pub fn ansi_pair<'a>(code: &'a str) -> Option<&[&str; 2]> {
44 ANSI_PAIR.iter().find(|&pair| pair.iter().any(|&v| v == code))
45}
46
47pub fn strip_codes(txt: &str) -> String {
49 let regex = Regex::new(ANSI_REGEX).unwrap();
50 let clean = String::from_utf8(regex.replace_all(txt, "").as_bytes().to_vec());
51 if clean.is_ok() {
52 clean.unwrap()
53 } else {
54 txt.to_string()
55 }
56}
57
58pub fn match_indices(txt: &str) -> Vec<String> {
61 let regex = Regex::new(ANSI_REGEX).unwrap();
62 let mut result = Vec::new();
63 let mut data: String = txt.to_string();
64
65 loop {
66 let mat = regex.find(data.as_str());
67 if mat.is_some() {
68 let mat = mat.unwrap();
69 let start = mat.start();
70 let end = mat.end();
71 result.push(data[0..start].to_string());
72 result.push(data[start..end].to_string());
73
74 let size = data.chars().count();
75 if size == 0 {
76 break;
77 } else {
78 data = data[end..].to_string();
79 }
80 } else {
81 result.push(data);
82 break;
83 }
84 }
85
86 result
87}
88
89pub fn slice_text(txt: &str, start: usize, end: usize) -> String {
91 let mut u_start = None;
92 let mut u_end = None;
93 let mut offset = 0;
94 let mut u_offset = 0;
95
96 for chunk in match_indices(txt).iter() {
97 let size = strip_codes(chunk).len();
98
99 if u_start.is_none() && offset + size >= start {
100 u_start = Some(u_offset + start - offset);
101 }
102 if u_end.is_none() && offset + size >= end {
103 u_end = Some(u_offset + end - offset);
104 break;
105 }
106 offset += size;
107 u_offset += chunk.len();
108 }
109
110 let u_start = match u_start {
111 Some(v) => v,
112 None => 0,
113 };
114 let u_end = match u_end {
115 Some(v) => v,
116 None => txt.len(),
117 };
118 txt[u_start..u_end].to_string()
119}
120
121pub fn size_text(txt: &str) -> usize {
124 unicode_width::UnicodeWidthStr::width(strip_codes(txt).as_str())
125}
126
127pub fn pad_text(
129 txt: &str,
130 width: usize,
131 align: &Alignment,
132 chr: char,
133) -> String {
134 let size = size_text(txt);
135 if size >= width {
136 return txt.to_string();
137 }
138
139 let diff = width - size;
140 let (left_pad, right_pad) = match align {
141 Alignment::Left => (0, diff),
142 Alignment::Right => (diff, 0),
143 Alignment::Center => (diff / 2, diff - diff / 2),
144 };
145
146 let mut result = String::new();
147 for _ in 0..left_pad {
148 result.push(chr);
149 }
150 result.push_str(txt);
151 for _ in 0..right_pad {
152 result.push(chr);
153 }
154 result
155}
156
157pub fn trucate_text(
159 txt: &str,
160 width: usize,
161 align: &Alignment,
162 tail: &str
163) -> String {
164
165 let size = size_text(txt);
166 if width >= size {
167 return txt.to_string();
168 }
169
170 let t_size = size_text(tail);
171 match align {
172 Alignment::Left => {
173 let text = slice_text(txt, 0, width - t_size).trim().to_string();
174 format!("{}{}", text, tail)
175 },
176 Alignment::Right => {
177 let text = slice_text(txt, size - width + t_size, size).trim().to_string();
178 format!("{}{}", tail, text)
179 },
180 Alignment::Center => {
181 let dim = (width - t_size) / 2;
182 let left = slice_text(txt, 0, dim).trim().to_string();
183 let right = slice_text(txt, size - width + t_size + dim, size).trim().to_string();
184 format!("{}{}{}", left, tail, right)
185 },
186 }
187}
188
189pub fn wrap_text(txt: &str, width: usize) -> Vec<String> {
191 let mut result: Vec<String> = Vec::new();
192
193 for line in txt.lines() {
194 let mut words: Vec<String> = Vec::new();
195 let mut length = 0;
196
197 for (wcount, word) in line.split(" ").enumerate() {
198 let word_size = size_text(word);
199 if length + word_size >= width && words.len() > 0 {
200 result.push(words.join(" "));
201 words = Vec::new();
202 length = 0;
203 }
204 length += word_size + if wcount > 0 { 1 } else { 0 }; words.push(word.to_string());
206 }
207
208 if words.len() > 0 {
209 result.push(words.join(" "));
210 }
211 }
212
213 result
214}
215
216pub fn repaire_text(lines: Vec<String>) -> Vec<String> {
218 let mut ansis: Vec<Vec<String>> = Vec::new();
219
220 lines.iter().map(|line| {
221 let parts = match_indices(line.as_str());
222
223 let mut result: Vec<String> = Vec::new();
224 let ansiiter = &ansis;
225 for ansi in ansiiter.into_iter() {
226 result.push(ansi[0].to_string());
227 }
228 for part in parts.into_iter() {
229 let pair = ansi_pair(part.as_str());
230 if pair.is_some() {
231 let pair = pair.unwrap();
232 let opentag = pair[0].to_string();
233 let closetag = pair[1].to_string();
234 if part == opentag {
235 ansis.push(vec![opentag, closetag]);
236 } else if part == closetag {
237 let index = ansis.iter().position(|a| a[1].to_string() == closetag);
238 if index.is_some() {
239 ansis.remove(index.unwrap());
240 }
241 }
242 }
243 result.push(part.to_string());
244 }
245 let ansiiter = &ansis;
246 for ansi in ansiiter.into_iter() {
247 result.push(ansi[1].to_string());
248 }
249 result.join("")
250 }).collect()
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn finds_ansi_pair() {
259 assert_eq!(ansi_pair(&ANSI_PAIR[0][1]), Some(&ANSI_PAIR[0]));
260 assert_eq!(ansi_pair("foo"), None);
261 }
262
263 #[test]
264 fn strips_ansi_codes() {
265 assert_eq!(strip_codes("aaa\x1B[0mbbb\x1B[0mccc"), "aaabbbccc");
266 }
267
268 #[test]
269 fn matches_ansi_indices() {
270 assert_eq!(match_indices("This is\x1B[39m long"), vec!["This is", "\x1B[39m", " long"]);
271 assert_eq!(match_indices("This is\x1B[39m long \x1B[46mtext for test"), vec!["This is", "\x1B[39m", " long ", "\x1B[46m", "text for test"]);
272 }
273
274 #[test]
275 fn slices_ansi_text() {
276 assert_eq!(slice_text("a\x1B[32maa\x1B[32mb\x1B[32mbb\x1B[32mcccdddeeefff", 5, 10), "b\x1B[32mcccd");
277 }
278
279 #[test]
280 fn sizes_ansi_text() {
281 assert_eq!(size_text("aaa\x1B[0mbbb\x1B[0mccc"), 9);
282 }
283
284 #[test]
285 fn pads_ansi_text() {
286 assert_eq!(pad_text("fo\x1B[39mobar", 10, &Alignment::Left, '+'), "fo\x1B[39mobar++++");
287 assert_eq!(pad_text("fo\x1B[39mobar", 10, &Alignment::Right, '+'), "++++fo\x1B[39mobar");
288 assert_eq!(pad_text("fo\x1B[39mobar", 10, &Alignment::Center, '+'), "++fo\x1B[39mobar++");
289 }
290
291 #[test]
292 fn truncates_ansi_text() {
293 assert_eq!(trucate_text("fo\x1B[39mobarbaz", 5, &Alignment::Left, "+"), "fo\x1B[39mob+");
294 assert_eq!(trucate_text("fo\x1B[39mobarbaz", 5, &Alignment::Right, "+++"), "+++az");
295 assert_eq!(trucate_text("fo\x1B[39mobarbaz", 5, &Alignment::Center, "+++"), "f+++z");
296 }
297
298 #[test]
299 fn wraps_ansi_text() {
300 assert_eq!(wrap_text("This is \x1B[39ma very long tekst for testing\x1B[39m only.", 10), vec![
301 "This is \x1B[39ma",
302 "very long",
303 "tekst for",
304 "testing\x1B[39m",
305 "only."
306 ]);
307 }
308
309 #[test]
310 fn repairs_multiline_ansi_text() {
311 assert_eq!(repaire_text(vec![
312 "This is \x1B[31mlong".to_string(),
313 "string 利干 sample".to_string(),
314 "this is 利干 sample\x1B[39m long code".to_string(),
315 ]), vec![
316 "This is \x1B[31mlong\x1B[39m",
317 "\x1B[31mstring 利干 sample\x1B[39m",
318 "\x1B[31mthis is 利干 sample\x1B[39m long code",
319 ]);
320 }
321}