/rust/registry/src/index.crates.io-1949cf8c6b5b557f/nu-ansi-term-0.49.0/src/display.rs
Line | Count | Source |
1 | | use crate::ansi::RESET; |
2 | | use crate::difference::Difference; |
3 | | use crate::style::{Color, Style}; |
4 | | use crate::write::AnyWrite; |
5 | | use std::borrow::Cow; |
6 | | use std::fmt; |
7 | | use std::io; |
8 | | |
9 | | #[derive(Eq, PartialEq, Debug)] |
10 | | enum OSControl<'a, S: 'a + ToOwned + ?Sized> |
11 | | where |
12 | | <S as ToOwned>::Owned: fmt::Debug, |
13 | | { |
14 | | Title, |
15 | | Link { url: Cow<'a, S> }, |
16 | | } |
17 | | |
18 | | impl<'a, S: 'a + ToOwned + ?Sized> Clone for OSControl<'a, S> |
19 | | where |
20 | | <S as ToOwned>::Owned: fmt::Debug, |
21 | | { |
22 | 0 | fn clone(&self) -> Self { |
23 | 0 | match self { |
24 | 0 | Self::Link { url: u } => Self::Link { url: u.clone() }, |
25 | 0 | Self::Title => Self::Title, |
26 | | } |
27 | 0 | } |
28 | | } |
29 | | |
30 | | /// An `AnsiGenericString` includes a generic string type and a `Style` to |
31 | | /// display that string. `AnsiString` and `AnsiByteString` are aliases for |
32 | | /// this type on `str` and `\[u8]`, respectively. |
33 | | #[derive(Eq, PartialEq, Debug)] |
34 | | pub struct AnsiGenericString<'a, S: 'a + ToOwned + ?Sized> |
35 | | where |
36 | | <S as ToOwned>::Owned: fmt::Debug, |
37 | | { |
38 | | pub(crate) style: Style, |
39 | | pub(crate) string: Cow<'a, S>, |
40 | | oscontrol: Option<OSControl<'a, S>>, |
41 | | } |
42 | | |
43 | | /// Cloning an `AnsiGenericString` will clone its underlying string. |
44 | | /// |
45 | | /// # Examples |
46 | | /// |
47 | | /// ``` |
48 | | /// use nu_ansi_term::AnsiString; |
49 | | /// |
50 | | /// let plain_string = AnsiString::from("a plain string"); |
51 | | /// let clone_string = plain_string.clone(); |
52 | | /// assert_eq!(clone_string, plain_string); |
53 | | /// ``` |
54 | | impl<'a, S: 'a + ToOwned + ?Sized> Clone for AnsiGenericString<'a, S> |
55 | | where |
56 | | <S as ToOwned>::Owned: fmt::Debug, |
57 | | { |
58 | 0 | fn clone(&self) -> AnsiGenericString<'a, S> { |
59 | 0 | AnsiGenericString { |
60 | 0 | style: self.style, |
61 | 0 | string: self.string.clone(), |
62 | 0 | oscontrol: self.oscontrol.clone(), |
63 | 0 | } |
64 | 0 | } |
65 | | } |
66 | | |
67 | | // You might think that the hand-written Clone impl above is the same as the |
68 | | // one that gets generated with #[derive]. But it’s not *quite* the same! |
69 | | // |
70 | | // `str` is not Clone, and the derived Clone implementation puts a Clone |
71 | | // constraint on the S type parameter (generated using --pretty=expanded): |
72 | | // |
73 | | // ↓_________________↓ |
74 | | // impl <'a, S: ::std::clone::Clone + 'a + ToOwned + ?Sized> ::std::clone::Clone |
75 | | // for ANSIGenericString<'a, S> where |
76 | | // <S as ToOwned>::Owned: fmt::Debug { ... } |
77 | | // |
78 | | // This resulted in compile errors when you tried to derive Clone on a type |
79 | | // that used it: |
80 | | // |
81 | | // #[derive(PartialEq, Debug, Clone, Default)] |
82 | | // pub struct TextCellContents(Vec<AnsiString<'static>>); |
83 | | // ^^^^^^^^^^^^^^^^^^^^^^^^^ |
84 | | // error[E0277]: the trait `std::clone::Clone` is not implemented for `str` |
85 | | // |
86 | | // The hand-written impl above can ignore that constraint and still compile. |
87 | | |
88 | | /// An ANSI String is a string coupled with the `Style` to display it |
89 | | /// in a terminal. |
90 | | /// |
91 | | /// Although not technically a string itself, it can be turned into |
92 | | /// one with the `to_string` method. |
93 | | /// |
94 | | /// # Examples |
95 | | /// |
96 | | /// ``` |
97 | | /// use nu_ansi_term::AnsiString; |
98 | | /// use nu_ansi_term::Color::Red; |
99 | | /// |
100 | | /// let red_string = Red.paint("a red string"); |
101 | | /// println!("{}", red_string); |
102 | | /// ``` |
103 | | /// |
104 | | /// ``` |
105 | | /// use nu_ansi_term::AnsiString; |
106 | | /// |
107 | | /// let plain_string = AnsiString::from("a plain string"); |
108 | | /// ``` |
109 | | pub type AnsiString<'a> = AnsiGenericString<'a, str>; |
110 | | |
111 | | /// An `AnsiByteString` represents a formatted series of bytes. Use |
112 | | /// `AnsiByteString` when styling text with an unknown encoding. |
113 | | pub type AnsiByteString<'a> = AnsiGenericString<'a, [u8]>; |
114 | | |
115 | | impl<'a, I, S: 'a + ToOwned + ?Sized> From<I> for AnsiGenericString<'a, S> |
116 | | where |
117 | | I: Into<Cow<'a, S>>, |
118 | | <S as ToOwned>::Owned: fmt::Debug, |
119 | | { |
120 | 0 | fn from(input: I) -> AnsiGenericString<'a, S> { |
121 | 0 | AnsiGenericString { |
122 | 0 | string: input.into(), |
123 | 0 | style: Style::default(), |
124 | 0 | oscontrol: None, |
125 | 0 | } |
126 | 0 | } |
127 | | } |
128 | | |
129 | | impl<'a, S: 'a + ToOwned + ?Sized> AnsiGenericString<'a, S> |
130 | | where |
131 | | <S as ToOwned>::Owned: fmt::Debug, |
132 | | { |
133 | | /// Directly access the style |
134 | 0 | pub const fn style_ref(&self) -> &Style { |
135 | 0 | &self.style |
136 | 0 | } |
137 | | |
138 | | /// Directly access the style mutably |
139 | 0 | pub fn style_ref_mut(&mut self) -> &mut Style { |
140 | 0 | &mut self.style |
141 | 0 | } |
142 | | |
143 | | /// Directly access the underlying string |
144 | 0 | pub fn as_str(&self) -> &S { |
145 | 0 | self.string.as_ref() |
146 | 0 | } |
147 | | |
148 | | // Instances that imply wrapping in OSC sequences |
149 | | // and do not get displayed in the terminal text |
150 | | // area. |
151 | | // |
152 | | /// Produce an ANSI string that changes the title shown |
153 | | /// by the terminal emulator. |
154 | | /// |
155 | | /// # Examples |
156 | | /// |
157 | | /// ``` |
158 | | /// use nu_ansi_term::AnsiGenericString; |
159 | | /// let title_string = AnsiGenericString::title("My Title"); |
160 | | /// println!("{}", title_string); |
161 | | /// ``` |
162 | | /// Should produce an empty line but set the terminal title. |
163 | 0 | pub fn title<I>(s: I) -> Self |
164 | 0 | where |
165 | 0 | I: Into<Cow<'a, S>>, |
166 | | { |
167 | 0 | Self { |
168 | 0 | style: Style::default(), |
169 | 0 | string: s.into(), |
170 | 0 | oscontrol: Some(OSControl::<'a, S>::Title), |
171 | 0 | } |
172 | 0 | } |
173 | | |
174 | | // |
175 | | // Annotations (OSC sequences that do more than wrap) |
176 | | // |
177 | | |
178 | | /// Cause the styled ANSI string to link to the given URL |
179 | | /// |
180 | | /// # Examples |
181 | | /// |
182 | | /// ``` |
183 | | /// use nu_ansi_term::Color::Red; |
184 | | /// |
185 | | /// let link_string = Red.paint("a red string").hyperlink("https://www.example.com"); |
186 | | /// println!("{}", link_string); |
187 | | /// ``` |
188 | | /// Should show a red-painted string which, on terminals |
189 | | /// that support it, is a clickable hyperlink. |
190 | 0 | pub fn hyperlink<I>(mut self, url: I) -> Self |
191 | 0 | where |
192 | 0 | I: Into<Cow<'a, S>>, |
193 | | { |
194 | 0 | self.oscontrol = Some(OSControl::Link { url: url.into() }); |
195 | 0 | self |
196 | 0 | } |
197 | | |
198 | | /// Get any URL associated with the string |
199 | 0 | pub fn url_string(&self) -> Option<&S> { |
200 | 0 | match &self.oscontrol { |
201 | 0 | Some(OSControl::Link { url: u }) => Some(u.as_ref()), |
202 | 0 | _ => None, |
203 | | } |
204 | 0 | } |
205 | | } |
206 | | |
207 | | /// A set of `AnsiGenericStrings`s collected together, in order to be |
208 | | /// written with a minimum of control characters. |
209 | | #[derive(Debug, Eq, PartialEq)] |
210 | | pub struct AnsiGenericStrings<'a, S: 'a + ToOwned + ?Sized>(pub &'a [AnsiGenericString<'a, S>]) |
211 | | where |
212 | | <S as ToOwned>::Owned: fmt::Debug, |
213 | | S: PartialEq; |
214 | | |
215 | | /// A set of `AnsiString`s collected together, in order to be written with a |
216 | | /// minimum of control characters. |
217 | | pub type AnsiStrings<'a> = AnsiGenericStrings<'a, str>; |
218 | | |
219 | | /// A function to construct an `AnsiStrings` instance. |
220 | | #[allow(non_snake_case)] |
221 | 0 | pub const fn AnsiStrings<'a>(arg: &'a [AnsiString<'a>]) -> AnsiStrings<'a> { |
222 | 0 | AnsiGenericStrings(arg) |
223 | 0 | } |
224 | | |
225 | | /// A set of `AnsiByteString`s collected together, in order to be |
226 | | /// written with a minimum of control characters. |
227 | | pub type AnsiByteStrings<'a> = AnsiGenericStrings<'a, [u8]>; |
228 | | |
229 | | /// A function to construct an `AnsiByteStrings` instance. |
230 | | #[allow(non_snake_case)] |
231 | 0 | pub const fn AnsiByteStrings<'a>(arg: &'a [AnsiByteString<'a>]) -> AnsiByteStrings<'a> { |
232 | 0 | AnsiGenericStrings(arg) |
233 | 0 | } |
234 | | |
235 | | // ---- paint functions ---- |
236 | | |
237 | | impl Style { |
238 | | /// Paints the given text with this color, returning an ANSI string. |
239 | | #[must_use] |
240 | 0 | pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>(self, input: I) -> AnsiGenericString<'a, S> |
241 | 0 | where |
242 | 0 | I: Into<Cow<'a, S>>, |
243 | 0 | <S as ToOwned>::Owned: fmt::Debug, |
244 | | { |
245 | 0 | AnsiGenericString { |
246 | 0 | string: input.into(), |
247 | 0 | style: self, |
248 | 0 | oscontrol: None, |
249 | 0 | } |
250 | 0 | } Unexecuted instantiation: <nu_ansi_term::style::Style>::paint::<&alloc::string::String, str> Unexecuted instantiation: <nu_ansi_term::style::Style>::paint::<&str, str> Unexecuted instantiation: <nu_ansi_term::style::Style>::paint::<alloc::string::String, str> |
251 | | } |
252 | | |
253 | | impl Color { |
254 | | /// Paints the given text with this color, returning an ANSI string. |
255 | | /// This is a short-cut so you don’t have to use `Blue.normal()` just |
256 | | /// to get blue text. |
257 | | /// |
258 | | /// ``` |
259 | | /// use nu_ansi_term::Color::Blue; |
260 | | /// println!("{}", Blue.paint("da ba dee")); |
261 | | /// ``` |
262 | | #[must_use] |
263 | 0 | pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>(self, input: I) -> AnsiGenericString<'a, S> |
264 | 0 | where |
265 | 0 | I: Into<Cow<'a, S>>, |
266 | 0 | <S as ToOwned>::Owned: fmt::Debug, |
267 | | { |
268 | 0 | AnsiGenericString { |
269 | 0 | string: input.into(), |
270 | 0 | style: self.normal(), |
271 | 0 | oscontrol: None, |
272 | 0 | } |
273 | 0 | } |
274 | | } |
275 | | |
276 | | // ---- writers for individual ANSI strings ---- |
277 | | |
278 | | impl<'a> fmt::Display for AnsiString<'a> { |
279 | 0 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
280 | 0 | let w: &mut dyn fmt::Write = f; |
281 | 0 | self.write_to_any(w) |
282 | 0 | } |
283 | | } |
284 | | |
285 | | impl<'a> AnsiByteString<'a> { |
286 | | /// Write an `AnsiByteString` to an `io::Write`. This writes the escape |
287 | | /// sequences for the associated `Style` around the bytes. |
288 | 0 | pub fn write_to<W: io::Write>(&self, w: &mut W) -> io::Result<()> { |
289 | 0 | let w: &mut dyn io::Write = w; |
290 | 0 | self.write_to_any(w) |
291 | 0 | } |
292 | | } |
293 | | |
294 | | impl<'a, S: 'a + ToOwned + ?Sized> AnsiGenericString<'a, S> |
295 | | where |
296 | | <S as ToOwned>::Owned: fmt::Debug, |
297 | | &'a S: AsRef<[u8]>, |
298 | | { |
299 | | // write the part within the styling prefix and suffix |
300 | 0 | fn write_inner<W: AnyWrite<Wstr = S> + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> { |
301 | 0 | match &self.oscontrol { |
302 | 0 | Some(OSControl::Link { url: u }) => { |
303 | 0 | write!(w, "\x1B]8;;")?; |
304 | 0 | w.write_str(u.as_ref())?; |
305 | 0 | write!(w, "\x1B\x5C")?; |
306 | 0 | w.write_str(self.string.as_ref())?; |
307 | 0 | write!(w, "\x1B]8;;\x1B\x5C") |
308 | | } |
309 | | Some(OSControl::Title) => { |
310 | 0 | write!(w, "\x1B]2;")?; |
311 | 0 | w.write_str(self.string.as_ref())?; |
312 | 0 | write!(w, "\x1B\x5C") |
313 | | } |
314 | 0 | None => w.write_str(self.string.as_ref()), |
315 | | } |
316 | 0 | } |
317 | | |
318 | 0 | fn write_to_any<W: AnyWrite<Wstr = S> + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> { |
319 | 0 | write!(w, "{}", self.style.prefix())?; |
320 | 0 | self.write_inner(w)?; |
321 | 0 | write!(w, "{}", self.style.suffix()) |
322 | 0 | } |
323 | | } |
324 | | |
325 | | // ---- writers for combined ANSI strings ---- |
326 | | |
327 | | impl<'a> fmt::Display for AnsiStrings<'a> { |
328 | 0 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
329 | 0 | let f: &mut dyn fmt::Write = f; |
330 | 0 | self.write_to_any(f) |
331 | 0 | } |
332 | | } |
333 | | |
334 | | impl<'a> AnsiByteStrings<'a> { |
335 | | /// Write `AnsiByteStrings` to an `io::Write`. This writes the minimal |
336 | | /// escape sequences for the associated `Style`s around each set of |
337 | | /// bytes. |
338 | 0 | pub fn write_to<W: io::Write>(&self, w: &mut W) -> io::Result<()> { |
339 | 0 | let w: &mut dyn io::Write = w; |
340 | 0 | self.write_to_any(w) |
341 | 0 | } |
342 | | } |
343 | | |
344 | | impl<'a, S: 'a + ToOwned + ?Sized + PartialEq> AnsiGenericStrings<'a, S> |
345 | | where |
346 | | <S as ToOwned>::Owned: fmt::Debug, |
347 | | &'a S: AsRef<[u8]>, |
348 | | { |
349 | 0 | fn write_to_any<W: AnyWrite<Wstr = S> + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> { |
350 | | use self::Difference::*; |
351 | | |
352 | 0 | let first = match self.0.first() { |
353 | 0 | None => return Ok(()), |
354 | 0 | Some(f) => f, |
355 | | }; |
356 | | |
357 | 0 | write!(w, "{}", first.style.prefix())?; |
358 | 0 | first.write_inner(w)?; |
359 | | |
360 | 0 | for window in self.0.windows(2) { |
361 | 0 | match Difference::between(&window[0].style, &window[1].style) { |
362 | 0 | ExtraStyles(style) => write!(w, "{}", style.prefix())?, |
363 | 0 | Reset => write!(w, "{}{}", RESET, window[1].style.prefix())?, |
364 | 0 | Empty => { /* Do nothing! */ } |
365 | | } |
366 | | |
367 | 0 | window[1].write_inner(w)?; |
368 | | } |
369 | | |
370 | | // Write the final reset string after all of the AnsiStrings have been |
371 | | // written, *except* if the last one has no styles, because it would |
372 | | // have already been written by this point. |
373 | 0 | if let Some(last) = self.0.last() { |
374 | 0 | if !last.style.is_plain() { |
375 | 0 | write!(w, "{}", RESET)?; |
376 | 0 | } |
377 | 0 | } |
378 | | |
379 | 0 | Ok(()) |
380 | 0 | } |
381 | | } |
382 | | |
383 | | // ---- tests ---- |
384 | | |
385 | | #[cfg(test)] |
386 | | mod tests { |
387 | | pub use super::super::{AnsiGenericString, AnsiStrings}; |
388 | | pub use crate::style::Color::*; |
389 | | pub use crate::style::Style; |
390 | | |
391 | | #[test] |
392 | | fn no_control_codes_for_plain() { |
393 | | let one = Style::default().paint("one"); |
394 | | let two = Style::default().paint("two"); |
395 | | let output = AnsiStrings(&[one, two]).to_string(); |
396 | | assert_eq!(output, "onetwo"); |
397 | | } |
398 | | |
399 | | // NOTE: unstyled because it could have OSC escape sequences |
400 | | fn idempotent(unstyled: AnsiGenericString<'_, str>) { |
401 | | let before_g = Green.paint("Before is Green. "); |
402 | | let before = Style::default().paint("Before is Plain. "); |
403 | | let after_g = Green.paint(" After is Green."); |
404 | | let after = Style::default().paint(" After is Plain."); |
405 | | let unstyled_s = unstyled.clone().to_string(); |
406 | | |
407 | | // check that RESET precedes unstyled |
408 | | let joined = AnsiStrings(&[before_g.clone(), unstyled.clone()]).to_string(); |
409 | | assert!(joined.starts_with("\x1B[32mBefore is Green. \x1B[0m")); |
410 | | assert!( |
411 | | joined.ends_with(unstyled_s.as_str()), |
412 | | "{:?} does not end with {:?}", |
413 | | joined, |
414 | | unstyled_s |
415 | | ); |
416 | | |
417 | | // check that RESET does not follow unstyled when appending styled |
418 | | let joined = AnsiStrings(&[unstyled.clone(), after_g.clone()]).to_string(); |
419 | | assert!( |
420 | | joined.starts_with(unstyled_s.as_str()), |
421 | | "{:?} does not start with {:?}", |
422 | | joined, |
423 | | unstyled_s |
424 | | ); |
425 | | assert!(joined.ends_with("\x1B[32m After is Green.\x1B[0m")); |
426 | | |
427 | | // does not introduce spurious SGR codes (reset or otherwise) adjacent |
428 | | // to plain strings |
429 | | let joined = AnsiStrings(&[unstyled.clone()]).to_string(); |
430 | | assert!( |
431 | | !joined.contains("\x1B["), |
432 | | "{:?} does contain \\x1B[", |
433 | | joined |
434 | | ); |
435 | | let joined = AnsiStrings(&[before.clone(), unstyled.clone()]).to_string(); |
436 | | assert!( |
437 | | !joined.contains("\x1B["), |
438 | | "{:?} does contain \\x1B[", |
439 | | joined |
440 | | ); |
441 | | let joined = AnsiStrings(&[before.clone(), unstyled.clone(), after.clone()]).to_string(); |
442 | | assert!( |
443 | | !joined.contains("\x1B["), |
444 | | "{:?} does contain \\x1B[", |
445 | | joined |
446 | | ); |
447 | | let joined = AnsiStrings(&[unstyled.clone(), after.clone()]).to_string(); |
448 | | assert!( |
449 | | !joined.contains("\x1B["), |
450 | | "{:?} does contain \\x1B[", |
451 | | joined |
452 | | ); |
453 | | } |
454 | | |
455 | | #[test] |
456 | | fn title() { |
457 | | let title = AnsiGenericString::title("Test Title"); |
458 | | assert_eq!(title.clone().to_string(), "\x1B]2;Test Title\x1B\\"); |
459 | | idempotent(title) |
460 | | } |
461 | | |
462 | | #[test] |
463 | | fn hyperlink() { |
464 | | let styled = Red |
465 | | .paint("Link to example.com.") |
466 | | .hyperlink("https://example.com"); |
467 | | assert_eq!( |
468 | | styled.to_string(), |
469 | | "\x1B[31m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m" |
470 | | ); |
471 | | } |
472 | | |
473 | | #[test] |
474 | | fn hyperlinks() { |
475 | | let before = Green.paint("Before link. "); |
476 | | let link = Blue |
477 | | .underline() |
478 | | .paint("Link to example.com.") |
479 | | .hyperlink("https://example.com"); |
480 | | let after = Green.paint(" After link."); |
481 | | |
482 | | // Assemble with link by itself |
483 | | let joined = AnsiStrings(&[link.clone()]).to_string(); |
484 | | #[cfg(feature = "gnu_legacy")] |
485 | | assert_eq!(joined, format!("\x1B[04;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m")); |
486 | | #[cfg(not(feature = "gnu_legacy"))] |
487 | | assert_eq!(joined, format!("\x1B[4;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m")); |
488 | | |
489 | | // Assemble with link in the middle |
490 | | let joined = AnsiStrings(&[before.clone(), link.clone(), after.clone()]).to_string(); |
491 | | #[cfg(feature = "gnu_legacy")] |
492 | | assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[04;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m\x1B[32m After link.\x1B[0m")); |
493 | | #[cfg(not(feature = "gnu_legacy"))] |
494 | | assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[4;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m\x1B[32m After link.\x1B[0m")); |
495 | | |
496 | | // Assemble with link first |
497 | | let joined = AnsiStrings(&[link.clone(), after.clone()]).to_string(); |
498 | | #[cfg(feature = "gnu_legacy")] |
499 | | assert_eq!(joined, format!("\x1B[04;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m\x1B[32m After link.\x1B[0m")); |
500 | | #[cfg(not(feature = "gnu_legacy"))] |
501 | | assert_eq!(joined, format!("\x1B[4;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m\x1B[32m After link.\x1B[0m")); |
502 | | |
503 | | // Assemble with link at the end |
504 | | let joined = AnsiStrings(&[before.clone(), link.clone()]).to_string(); |
505 | | #[cfg(feature = "gnu_legacy")] |
506 | | assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[04;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m")); |
507 | | #[cfg(not(feature = "gnu_legacy"))] |
508 | | assert_eq!(joined, format!("\x1B[32mBefore link. \x1B[4;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m")); |
509 | | } |
510 | | } |