1use std::borrow::Cow;
2
3/// The function cuts the string to a specific width.
4/// Preserving colors with `ansi` feature on.
5pub(crate) fn split_str(s: &str, width: usize) -> (Cow<'_, str>, Cow<'_, str>) {
6 #[cfg(feature = "ansi")]
7 {
8 const REPLACEMENT: char = '\u{FFFD}';
9
10 let stripped = ansi_str::AnsiStr::ansi_strip(s);
11 let (length, cutwidth, csize) = split_at_width(&stripped, width);
12 let (mut lhs, mut rhs) = ansi_str::AnsiStr::ansi_split_at(s, length);
13
14 if csize > 0 {
15 let mut buf = lhs.into_owned();
16 let count_unknowns = width - cutwidth;
17 buf.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns));
18 lhs = Cow::Owned(buf);
19 rhs = Cow::Owned(ansi_str::AnsiStr::ansi_cut(rhs.as_ref(), csize..).into_owned());
20 }
21
22 (lhs, rhs)
23 }
24
25 #[cfg(not(feature = "ansi"))]
26 {
27 const REPLACEMENT: char = '\u{FFFD}';
28
29 let (length, cutwidth, csize) = split_at_width(s, width);
30 let (lhs, rhs) = s.split_at(length);
31
32 if csize == 0 {
33 return (Cow::Borrowed(lhs), Cow::Borrowed(rhs));
34 }
35
36 let count_unknowns = width - cutwidth;
37 let mut buf = lhs.to_owned();
38 buf.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns));
39
40 (Cow::Owned(buf), Cow::Borrowed(&rhs[csize..]))
41 }
42}
43
44/// The function cuts the string to a specific width.
45/// Preserving colors with `ansi` feature on.
46pub(crate) fn cut_str(s: &str, width: usize) -> Cow<'_, str> {
47 #[cfg(feature = "ansi")]
48 {
49 const REPLACEMENT: char = '\u{FFFD}';
50
51 let stripped = ansi_str::AnsiStr::ansi_strip(s);
52 let (length, cutwidth, csize) = split_at_width(&stripped, width);
53 let mut buf = ansi_str::AnsiStr::ansi_cut(s, ..length);
54 if csize != 0 {
55 let mut b = buf.into_owned();
56 let count_unknowns = width - cutwidth;
57 b.extend(std::iter::repeat(REPLACEMENT).take(count_unknowns));
58 buf = Cow::Owned(b);
59 }
60
61 buf
62 }
63
64 #[cfg(not(feature = "ansi"))]
65 {
66 cut_str2(text:s, width)
67 }
68}
69
70/// The function cuts the string to a specific width.
71/// While not preserving ansi sequences.
72pub(crate) fn cut_str2(text: &str, width: usize) -> Cow<'_, str> {
73 const REPLACEMENT: char = '\u{FFFD}';
74
75 let (length: usize, cutwidth: usize, csize: usize) = split_at_width(s:text, width);
76 if csize == 0 {
77 let buf: &str = &text[..length];
78 return Cow::Borrowed(buf);
79 }
80
81 let buf: &str = &text[..length];
82 let mut buf: String = buf.to_owned();
83 let count_unknowns: usize = width - cutwidth;
84 buf.extend(iter:std::iter::repeat(REPLACEMENT).take(count_unknowns));
85
86 Cow::Owned(buf)
87}
88
89/// The function splits a string in the position and
90/// returns a exact number of bytes before the position and in case of a split in an unicode grapheme
91/// a width of a character which was tried to be splited in.
92pub(crate) fn split_at_width(s: &str, at_width: usize) -> (usize, usize, usize) {
93 let mut length: usize = 0;
94 let mut width: usize = 0;
95 for c: char in s.chars() {
96 if width == at_width {
97 break;
98 };
99
100 let c_width: usize = unicode_width::UnicodeWidthChar::width(self:c).unwrap_or_default();
101 let c_length: usize = c.len_utf8();
102
103 // We cut the chars which takes more then 1 symbol to display,
104 // in order to archive the necessary width.
105 if width + c_width > at_width {
106 return (length, width, c_length);
107 }
108
109 width += c_width;
110 length += c_length;
111 }
112
113 (length, width, 0)
114}
115
116/// Strip OSC codes from `s`. If `s` is a single OSC8 hyperlink, with no other text, then return
117/// (s_with_all_hyperlinks_removed, Some(url)). If `s` does not meet this description, then return
118/// (s_with_all_hyperlinks_removed, None). Any ANSI color sequences in `s` will be retained. See
119/// <https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda>
120///
121/// The function is based on Dan Davison <https://github.com/dandavison> delta <https://github.com/dandavison/delta> ansi library.
122#[cfg(feature = "ansi")]
123pub(crate) fn strip_osc(text: &str) -> (String, Option<String>) {
124 #[derive(Debug)]
125 enum ExtractOsc8HyperlinkState {
126 ExpectOsc8Url,
127 ExpectFirstText,
128 ExpectMoreTextOrTerminator,
129 SeenOneHyperlink,
130 WillNotReturnUrl,
131 }
132
133 use ExtractOsc8HyperlinkState::*;
134
135 let mut url = None;
136 let mut state = ExpectOsc8Url;
137 let mut buf = String::with_capacity(text.len());
138
139 for el in ansitok::parse_ansi(text) {
140 match el.kind() {
141 ansitok::ElementKind::Osc => match state {
142 ExpectOsc8Url => {
143 url = Some(&text[el.start()..el.end()]);
144 state = ExpectFirstText;
145 }
146 ExpectMoreTextOrTerminator => state = SeenOneHyperlink,
147 _ => state = WillNotReturnUrl,
148 },
149 ansitok::ElementKind::Sgr => buf.push_str(&text[el.start()..el.end()]),
150 ansitok::ElementKind::Csi => buf.push_str(&text[el.start()..el.end()]),
151 ansitok::ElementKind::Esc => {}
152 ansitok::ElementKind::Text => {
153 buf.push_str(&text[el.start()..el.end()]);
154 match state {
155 ExpectFirstText => state = ExpectMoreTextOrTerminator,
156 ExpectMoreTextOrTerminator => {}
157 _ => state = WillNotReturnUrl,
158 }
159 }
160 }
161 }
162
163 match state {
164 WillNotReturnUrl => (buf, None),
165 _ => {
166 let url = url.and_then(|s| {
167 s.strip_prefix("\x1b]8;;")
168 .and_then(|s| s.strip_suffix('\x1b'))
169 });
170 if let Some(url) = url {
171 (buf, Some(url.to_string()))
172 } else {
173 (buf, None)
174 }
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 use crate::grid::util::string::string_width;
184
185 #[cfg(feature = "ansi")]
186 use owo_colors::{colors::Yellow, OwoColorize};
187
188 #[test]
189 fn strip_test() {
190 assert_eq!(cut_str("123456", 0), "");
191 assert_eq!(cut_str("123456", 3), "123");
192 assert_eq!(cut_str("123456", 10), "123456");
193
194 assert_eq!(cut_str("a week ago", 4), "a we");
195
196 assert_eq!(cut_str("😳😳😳😳😳", 0), "");
197 assert_eq!(cut_str("😳😳😳😳😳", 3), "😳�");
198 assert_eq!(cut_str("😳😳😳😳😳", 4), "😳😳");
199 assert_eq!(cut_str("😳😳😳😳😳", 20), "😳😳😳😳😳");
200
201 assert_eq!(cut_str("🏳️🏳️", 0), "");
202 assert_eq!(cut_str("🏳️🏳️", 1), "🏳");
203 assert_eq!(cut_str("🏳️🏳️", 2), "🏳\u{fe0f}🏳");
204 assert_eq!(string_width("🏳️🏳️"), string_width("🏳\u{fe0f}🏳"));
205
206 assert_eq!(cut_str("🎓", 1), "�");
207 assert_eq!(cut_str("🎓", 2), "🎓");
208
209 assert_eq!(cut_str("🥿", 1), "�");
210 assert_eq!(cut_str("🥿", 2), "🥿");
211
212 assert_eq!(cut_str("🩰", 1), "�");
213 assert_eq!(cut_str("🩰", 2), "🩰");
214
215 assert_eq!(cut_str("👍🏿", 1), "�");
216 assert_eq!(cut_str("👍🏿", 2), "👍");
217 assert_eq!(cut_str("👍🏿", 3), "👍�");
218 assert_eq!(cut_str("👍🏿", 4), "👍🏿");
219
220 assert_eq!(cut_str("🇻🇬", 1), "🇻");
221 assert_eq!(cut_str("🇻🇬", 2), "🇻🇬");
222 assert_eq!(cut_str("🇻🇬", 3), "🇻🇬");
223 assert_eq!(cut_str("🇻🇬", 4), "🇻🇬");
224 }
225
226 #[cfg(feature = "ansi")]
227 #[test]
228 fn strip_color_test() {
229 let numbers = "123456".red().on_bright_black().to_string();
230
231 assert_eq!(cut_str(&numbers, 0), "\u{1b}[31;100m\u{1b}[39m\u{1b}[49m");
232 assert_eq!(
233 cut_str(&numbers, 3),
234 "\u{1b}[31;100m123\u{1b}[39m\u{1b}[49m"
235 );
236 assert_eq!(cut_str(&numbers, 10), "\u{1b}[31;100m123456\u{1b}[0m");
237
238 let emojies = "😳😳😳😳😳".red().on_bright_black().to_string();
239
240 assert_eq!(cut_str(&emojies, 0), "\u{1b}[31;100m\u{1b}[39m\u{1b}[49m");
241 assert_eq!(
242 cut_str(&emojies, 3),
243 "\u{1b}[31;100m😳\u{1b}[39m\u{1b}[49m�"
244 );
245 assert_eq!(
246 cut_str(&emojies, 4),
247 "\u{1b}[31;100m😳😳\u{1b}[39m\u{1b}[49m"
248 );
249 assert_eq!(cut_str(&emojies, 20), "\u{1b}[31;100m😳😳😳😳😳\u{1b}[0m");
250
251 let emojies = "🏳️🏳️".red().on_bright_black().to_string();
252
253 assert_eq!(cut_str(&emojies, 0), "\u{1b}[31;100m\u{1b}[39m\u{1b}[49m");
254 assert_eq!(cut_str(&emojies, 1), "\u{1b}[31;100m🏳\u{1b}[39m\u{1b}[49m");
255 assert_eq!(
256 cut_str(&emojies, 2),
257 "\u{1b}[31;100m🏳\u{fe0f}🏳\u{1b}[39m\u{1b}[49m"
258 );
259 assert_eq!(
260 string_width(&emojies),
261 string_width("\u{1b}[31;100m🏳\u{fe0f}🏳\u{1b}[39m\u{1b}[49m")
262 );
263 }
264
265 #[test]
266 #[cfg(feature = "ansi")]
267 fn test_color_strip() {
268 let s = "Collored string"
269 .fg::<Yellow>()
270 .on_truecolor(12, 200, 100)
271 .blink()
272 .to_string();
273 assert_eq!(
274 cut_str(&s, 1),
275 "\u{1b}[5m\u{1b}[48;2;12;200;100m\u{1b}[33mC\u{1b}[25m\u{1b}[39m\u{1b}[49m"
276 )
277 }
278
279 #[test]
280 #[cfg(feature = "ansi")]
281 fn test_srip_osc() {
282 assert_eq!(
283 strip_osc("just a string here"),
284 (String::from("just a string here"), None)
285 );
286 assert_eq!(
287 strip_osc("/etc/rc.conf"),
288 (String::from("/etc/rc.conf"), None)
289 );
290 assert_eq!(
291 strip_osc(
292 "https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982"
293 ),
294 (String::from("https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982"), None)
295 );
296
297 assert_eq!(
298 strip_osc(&build_link_prefix_suffix("just a string here")),
299 (String::default(), Some(String::from("just a string here")))
300 );
301 assert_eq!(
302 strip_osc(&build_link_prefix_suffix("/etc/rc.conf")),
303 (String::default(), Some(String::from("/etc/rc.conf")))
304 );
305 assert_eq!(
306 strip_osc(
307 &build_link_prefix_suffix("https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982")
308 ),
309 (String::default(), Some(String::from("https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982")))
310 );
311
312 #[cfg(feature = "ansi")]
313 fn build_link_prefix_suffix(url: &str) -> String {
314 // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
315 let osc8 = "\x1b]8;;";
316 let st = "\x1b\\";
317 format!("{osc8}{url}{st}")
318 }
319 }
320}
321