rawcmd_utils/
lib.rs

1use regex::Regex;
2
3/// Regex pattern for maching ANSI codes.
4pub const ANSI_REGEX: &'static str = r"[\x1b\x9b][\[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PRZcf-nqry=><]";
5
6/// List of supported ANSI codes.
7pub const ANSI_PAIR: [[&'static str; 2]; 24] = [
8  ["\x1B[0m", "\x1B[0m"], // reset
9  ["\x1B[1m", "\x1B[22m"], // bold
10  ["\x1B[2m", "\x1B[22m"], // dim
11  ["\x1B[3m", "\x1B[23m"], // italic
12  ["\x1B[4m", "\x1B[24m"], // underline
13  ["\x1B[5m", "\x1B[25m"], // blink
14  ["\x1B[7m", "\x1B[27m"], // inverse
15  ["\x1B[8m", "\x1B[28m"], // hidden
16  ["\x1B[30m", "\x1B[39m"], // black
17  ["\x1B[31m", "\x1B[39m"], // red
18  ["\x1B[32m", "\x1B[39m"], // green
19  ["\x1B[33m", "\x1B[39m"], // yellow
20  ["\x1B[34m", "\x1B[39m"], // blue
21  ["\x1B[35m", "\x1B[39m"], // magenta
22  ["\x1B[36m", "\x1B[39m"], // cyan
23  ["\x1B[37m", "\x1B[39m"], // white
24  ["\x1B[40m", "\x1B[49m"], // bgblack
25  ["\x1B[41m", "\x1B[49m"], // bgred
26  ["\x1B[42m", "\x1B[49m"], // bggreen
27  ["\x1B[43m", "\x1B[49m"], // bgyellow
28  ["\x1B[44m", "\x1B[49m"], // bgblue
29  ["\x1B[45m", "\x1B[49m"], // bgmagenta
30  ["\x1B[46m", "\x1B[49m"], // bgcyan
31  ["\x1B[47m", "\x1B[49m"], // bgwhite
32];
33
34/// Text alignement options.
35#[derive(Debug, Clone, PartialEq)]
36pub enum Alignment {
37    Left,
38    Center,
39    Right,
40}
41
42/// Returns opening and closing code for the provided ANSI code.
43pub fn ansi_pair<'a>(code: &'a str) -> Option<&[&str; 2]> {
44    ANSI_PAIR.iter().find(|&pair| pair.iter().any(|&v| v == code))
45}
46
47/// Removes ANSI codes from the provided text and return a new string.
48pub 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
58/// Uses regex to split the provided text on ANSI codes. It returned list
59/// includes text chinks and ANSI delimiters.
60pub 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
89/// Returns a slice of a string containing ANSI codes.
90pub 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
121/// Calculates the length of a unicode string containing ANSI codes. The length
122/// returned represents the visible string length.
123pub fn size_text(txt: &str) -> usize {
124    unicode_width::UnicodeWidthStr::width(strip_codes(txt).as_str())
125}
126
127/// Pads a string to fill a certain number of characters.
128pub 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
157/// Truncates a string to a certain number of characters. 
158pub 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
189/// Splits long string into multiple lines.
190pub 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 }; // include spaces
205            words.push(word.to_string());
206        }
207
208        if words.len() > 0 {
209            result.push(words.join(" "));
210        }
211    }
212
213    result
214}
215
216/// Adds missing closing ANSI codes to the multi-line string.
217pub 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}