1mod console_tests;
2mod iterator;
3
4use std::borrow::Cow;
5
6use ansi_term::Style;
7use itertools::Itertools;
8use unicode_segmentation::UnicodeSegmentation;
9use unicode_width::UnicodeWidthStr;
10
11use iterator::{AnsiElementIterator, Element};
12
13pub const ANSI_CSI_CLEAR_TO_EOL: &str = "\x1b[0K";
14pub const ANSI_CSI_CLEAR_TO_BOL: &str = "\x1b[1K";
15pub const ANSI_SGR_RESET: &str = "\x1b[0m";
16pub const ANSI_SGR_REVERSE: &str = "\x1b[7m";
17
18pub fn strip_ansi_codes(s: &str) -> String {
19 strip_ansi_codes_from_strings_iterator(ansi_strings_iterator(s))
20}
21
22pub fn measure_text_width(s: &str) -> usize {
23 strip_ansi_codes(s).width()
25}
26
27pub fn truncate_str<'a, 'b>(s: &'a str, display_width: usize, tail: &'b str) -> Cow<'a, str> {
38 let items = ansi_strings_iterator(s).collect::<Vec<(&str, bool)>>();
39 let width = strip_ansi_codes_from_strings_iterator(items.iter().copied()).width();
40 if width <= display_width {
41 return Cow::from(s);
42 }
43 let result_tail = if !tail.is_empty() {
44 truncate_str(tail, display_width, "").to_string()
45 } else {
46 String::new()
47 };
48 let mut used = measure_text_width(&result_tail);
49 let mut result = String::new();
50 for (t, is_ansi) in items {
51 if !is_ansi {
52 for g in t.graphemes(true) {
53 let w = g.width();
54 if used + w > display_width {
55 break;
56 }
57 result.push_str(g);
58 used += w;
59 }
60 } else {
61 result.push_str(t);
62 }
63 }
64
65 Cow::from(format!("{}{}", result, result_tail))
66}
67
68pub fn parse_style_sections(s: &str) -> Vec<(ansi_term::Style, &str)> {
69 let mut sections = Vec::new();
70 let mut curr_style = Style::default();
71 for element in AnsiElementIterator::new(s) {
72 match element {
73 Element::Text(start, end) => sections.push((curr_style, &s[start..end])),
74 Element::Sgr(style, _, _) => curr_style = style,
75 _ => {}
76 }
77 }
78 sections
79}
80
81pub fn parse_first_style(s: &str) -> Option<ansi_term::Style> {
83 AnsiElementIterator::new(s).find_map(|el| match el {
84 Element::Sgr(style, _, _) => Some(style),
85 _ => None,
86 })
87}
88
89pub fn string_starts_with_ansi_style_sequence(s: &str) -> bool {
90 AnsiElementIterator::new(s)
91 .next()
92 .map(|el| matches!(el, Element::Sgr(_, _, _)))
93 .unwrap_or(false)
94}
95
96pub fn ansi_preserving_slice(s: &str, start: usize) -> String {
100 AnsiElementIterator::new(s)
101 .scan(0, |index, element| {
102 Some(match element {
104 Element::Sgr(_, a, b) => &s[a..b],
105 Element::Csi(a, b) => &s[a..b],
106 Element::Esc(a, b) => &s[a..b],
107 Element::Osc(a, b) => &s[a..b],
108 Element::Text(a, b) => {
109 let i = *index;
110 *index += b - a;
111 if *index <= start {
112 ""
114 } else if i > start {
115 &s[a..b]
117 } else {
118 &s[(a + start - i)..b]
120 }
121 }
122 })
123 })
124 .join("")
125}
126
127pub fn ansi_preserving_index(s: &str, i: usize) -> Option<usize> {
130 let mut index = 0;
131 for element in AnsiElementIterator::new(s) {
132 if let Element::Text(a, b) = element {
133 index += b - a;
134 if index > i {
135 return Some(b - (index - i));
136 }
137 }
138 }
139 None
140}
141
142fn ansi_strings_iterator(s: &str) -> impl Iterator<Item = (&str, bool)> {
143 AnsiElementIterator::new(s).map(move |el| match el {
144 Element::Sgr(_, i, j) => (&s[i..j], true),
145 Element::Csi(i, j) => (&s[i..j], true),
146 Element::Esc(i, j) => (&s[i..j], true),
147 Element::Osc(i, j) => (&s[i..j], true),
148 Element::Text(i, j) => (&s[i..j], false),
149 })
150}
151
152fn strip_ansi_codes_from_strings_iterator<'a>(
153 strings: impl Iterator<Item = (&'a str, bool)>,
154) -> String {
155 strings
156 .filter_map(|(el, is_ansi)| if !is_ansi { Some(el) } else { None })
157 .join("")
158}
159
160pub fn explain_ansi(line: &str, colorful: bool) -> String {
161 use crate::style::Style;
162
163 parse_style_sections(line)
164 .into_iter()
165 .map(|(ansi_term_style, s)| {
166 let style = Style {
167 ansi_term_style,
168 ..Style::default()
169 };
170 if colorful {
171 format!("({}){}", style.to_painted_string(), style.paint(s))
172 } else {
173 format!("({}){}", style, s)
174 }
175 })
176 .collect()
177}
178
179#[cfg(test)]
180mod tests {
181 use crate::ansi::ansi_preserving_index;
182
183 use super::{
185 ansi_preserving_slice, measure_text_width, parse_first_style,
186 string_starts_with_ansi_style_sequence, strip_ansi_codes, truncate_str,
187 };
188
189 #[test]
190 fn test_strip_ansi_codes() {
191 for s in &["src/ansi/mod.rs", "バー", "src/ansi/modバー.rs"] {
192 assert_eq!(strip_ansi_codes(s), *s);
193 }
194 assert_eq!(strip_ansi_codes("\x1b[31mバー\x1b[0m"), "バー");
195 }
196
197 #[test]
198 fn test_measure_text_width() {
199 assert_eq!(measure_text_width("src/ansi/mod.rs"), 15);
200 assert_eq!(measure_text_width("バー"), 4);
201 assert_eq!(measure_text_width("src/ansi/modバー.rs"), 19);
202 assert_eq!(measure_text_width("\x1b[31mバー\x1b[0m"), 4);
203 assert_eq!(measure_text_width("a\nb\n"), 2);
204 }
205
206 #[test]
207 fn test_strip_ansi_codes_osc_hyperlink() {
208 assert_eq!(strip_ansi_codes("\x1b[38;5;4m\x1b]8;;file:///Users/dan/src/delta/src/ansi/mod.rs\x1b\\src/ansi/mod.rs\x1b]8;;\x1b\\\x1b[0m\n"),
209 "src/ansi/mod.rs\n");
210 }
211
212 #[test]
213 fn test_measure_text_width_osc_hyperlink() {
214 assert_eq!(measure_text_width("\x1b[38;5;4m\x1b]8;;file:///Users/dan/src/delta/src/ansi/mod.rs\x1b\\src/ansi/mod.rs\x1b]8;;\x1b\\\x1b[0m"),
215 measure_text_width("src/ansi/mod.rs"));
216 }
217
218 #[test]
219 fn test_measure_text_width_osc_hyperlink_non_ascii() {
220 assert_eq!(measure_text_width("\x1b[38;5;4m\x1b]8;;file:///Users/dan/src/delta/src/ansi/mod.rs\x1b\\src/ansi/modバー.rs\x1b]8;;\x1b\\\x1b[0m"),
221 measure_text_width("src/ansi/modバー.rs"));
222 }
223
224 #[test]
225 fn test_parse_first_style() {
226 let minus_line_from_unconfigured_git = "\x1b[31m-____\x1b[m\n";
227 let style = parse_first_style(minus_line_from_unconfigured_git);
228 let expected_style = ansi_term::Style {
229 foreground: Some(ansi_term::Color::Red),
230 ..ansi_term::Style::default()
231 };
232 assert_eq!(Some(expected_style), style);
233 }
234
235 #[test]
236 fn test_string_starts_with_ansi_escape_sequence() {
237 assert!(!string_starts_with_ansi_style_sequence(""));
238 assert!(!string_starts_with_ansi_style_sequence("-"));
239 assert!(string_starts_with_ansi_style_sequence(
240 "\x1b[31m-XXX\x1b[m\n"
241 ));
242 assert!(string_starts_with_ansi_style_sequence("\x1b[32m+XXX"));
243 }
244
245 #[test]
246 fn test_ansi_preserving_slice_and_index() {
247 assert_eq!(ansi_preserving_slice("", 0), "");
248 assert_eq!(ansi_preserving_index("", 0), None);
249
250 assert_eq!(ansi_preserving_slice("0", 0), "0");
251 assert_eq!(ansi_preserving_index("0", 0), Some(0));
252
253 assert_eq!(ansi_preserving_slice("0", 1), "");
254 assert_eq!(ansi_preserving_index("0", 1), None);
255
256 let raw_string = "\x1b[1;35m0123456789\x1b[0m";
257 assert_eq!(
258 ansi_preserving_slice(raw_string, 1),
259 "\x1b[1;35m123456789\x1b[0m"
260 );
261 assert_eq!(ansi_preserving_slice(raw_string, 7), "\x1b[1;35m789\x1b[0m");
262 assert_eq!(ansi_preserving_index(raw_string, 0), Some(7));
263 assert_eq!(ansi_preserving_index(raw_string, 1), Some(8));
264 assert_eq!(ansi_preserving_index(raw_string, 7), Some(14));
265
266 let raw_string = "\x1b[1;36m0\x1b[m\x1b[1;36m123456789\x1b[m\n";
267 assert_eq!(
268 ansi_preserving_slice(raw_string, 1),
269 "\x1b[1;36m\x1b[m\x1b[1;36m123456789\x1b[m\n"
270 );
271 assert_eq!(ansi_preserving_index(raw_string, 0), Some(7));
272 assert_eq!(ansi_preserving_index(raw_string, 1), Some(18));
273 assert_eq!(ansi_preserving_index(raw_string, 7), Some(24));
274
275 let raw_string = "\x1b[1;36m012345\x1b[m\x1b[1;36m6789\x1b[m\n";
276 assert_eq!(
277 ansi_preserving_slice(raw_string, 3),
278 "\x1b[1;36m345\x1b[m\x1b[1;36m6789\x1b[m\n"
279 );
280 assert_eq!(ansi_preserving_index(raw_string, 0), Some(7));
281 assert_eq!(ansi_preserving_index(raw_string, 1), Some(8));
282 assert_eq!(ansi_preserving_index(raw_string, 7), Some(24));
283 }
284
285 #[test]
286 fn test_truncate_str() {
287 assert_eq!(truncate_str("1", 1, ""), "1");
288 assert_eq!(truncate_str("12", 1, ""), "1");
289 assert_eq!(truncate_str("123", 2, "s"), "1s");
290 assert_eq!(truncate_str("123", 2, "→"), "1→");
291 assert_eq!(truncate_str("12ݶ", 1, "ݶ"), "ݶ");
292 }
293}