| 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 | fn clone(&self) -> Self { | 
|---|
| 23 | match self { | 
|---|
| 24 | Self::Link { url: u: &Cow<'a, S> } => Self::Link { url: u.clone() }, | 
|---|
| 25 | Self::Title => Self::Title, | 
|---|
| 26 | } | 
|---|
| 27 | } | 
|---|
| 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 | fn clone(&self) -> AnsiGenericString<'a, S> { | 
|---|
| 59 | AnsiGenericString { | 
|---|
| 60 | style: self.style, | 
|---|
| 61 | string: self.string.clone(), | 
|---|
| 62 | oscontrol: self.oscontrol.clone(), | 
|---|
| 63 | } | 
|---|
| 64 | } | 
|---|
| 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 | fn from(input: I) -> AnsiGenericString<'a, S> { | 
|---|
| 121 | AnsiGenericString { | 
|---|
| 122 | string: input.into(), | 
|---|
| 123 | style: Style::default(), | 
|---|
| 124 | oscontrol: None, | 
|---|
| 125 | } | 
|---|
| 126 | } | 
|---|
| 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 | pub const fn style_ref(&self) -> &Style { | 
|---|
| 135 | &self.style | 
|---|
| 136 | } | 
|---|
| 137 |  | 
|---|
| 138 | /// Directly access the style mutably | 
|---|
| 139 | pub fn style_ref_mut(&mut self) -> &mut Style { | 
|---|
| 140 | &mut self.style | 
|---|
| 141 | } | 
|---|
| 142 |  | 
|---|
| 143 | /// Directly access the underlying string | 
|---|
| 144 | pub fn as_str(&self) -> &S { | 
|---|
| 145 | self.string.as_ref() | 
|---|
| 146 | } | 
|---|
| 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 | pub fn title<I>(s: I) -> Self | 
|---|
| 164 | where | 
|---|
| 165 | I: Into<Cow<'a, S>>, | 
|---|
| 166 | { | 
|---|
| 167 | Self { | 
|---|
| 168 | style: Style::default(), | 
|---|
| 169 | string: s.into(), | 
|---|
| 170 | oscontrol: Some(OSControl::<'a, S>::Title), | 
|---|
| 171 | } | 
|---|
| 172 | } | 
|---|
| 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 | pub fn hyperlink<I>(mut self, url: I) -> Self | 
|---|
| 191 | where | 
|---|
| 192 | I: Into<Cow<'a, S>>, | 
|---|
| 193 | { | 
|---|
| 194 | self.oscontrol = Some(OSControl::Link { url: url.into() }); | 
|---|
| 195 | self | 
|---|
| 196 | } | 
|---|
| 197 |  | 
|---|
| 198 | /// Get any URL associated with the string | 
|---|
| 199 | pub fn url_string(&self) -> Option<&S> { | 
|---|
| 200 | match &self.oscontrol { | 
|---|
| 201 | Some(OSControl::Link { url: u }) => Some(u.as_ref()), | 
|---|
| 202 | _ => None, | 
|---|
| 203 | } | 
|---|
| 204 | } | 
|---|
| 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 | pub const fn AnsiStrings<'a>(arg: &'a [AnsiString<'a>]) -> AnsiStrings<'a> { | 
|---|
| 222 | AnsiGenericStrings(arg) | 
|---|
| 223 | } | 
|---|
| 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 | pub const fn AnsiByteStrings<'a>(arg: &'a [AnsiByteString<'a>]) -> AnsiByteStrings<'a> { | 
|---|
| 232 | AnsiGenericStrings(arg) | 
|---|
| 233 | } | 
|---|
| 234 |  | 
|---|
| 235 | // ---- paint functions ---- | 
|---|
| 236 |  | 
|---|
| 237 | impl Style { | 
|---|
| 238 | /// Paints the given text with this color, returning an ANSI string. | 
|---|
| 239 | #[ must_use] | 
|---|
| 240 | pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>(self, input: I) -> AnsiGenericString<'a, S> | 
|---|
| 241 | where | 
|---|
| 242 | I: Into<Cow<'a, S>>, | 
|---|
| 243 | <S as ToOwned>::Owned: fmt::Debug, | 
|---|
| 244 | { | 
|---|
| 245 | AnsiGenericString { | 
|---|
| 246 | string: input.into(), | 
|---|
| 247 | style: self, | 
|---|
| 248 | oscontrol: None, | 
|---|
| 249 | } | 
|---|
| 250 | } | 
|---|
| 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 | pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>(self, input: I) -> AnsiGenericString<'a, S> | 
|---|
| 264 | where | 
|---|
| 265 | I: Into<Cow<'a, S>>, | 
|---|
| 266 | <S as ToOwned>::Owned: fmt::Debug, | 
|---|
| 267 | { | 
|---|
| 268 | AnsiGenericString { | 
|---|
| 269 | string: input.into(), | 
|---|
| 270 | style: self.normal(), | 
|---|
| 271 | oscontrol: None, | 
|---|
| 272 | } | 
|---|
| 273 | } | 
|---|
| 274 | } | 
|---|
| 275 |  | 
|---|
| 276 | // ---- writers for individual ANSI strings ---- | 
|---|
| 277 |  | 
|---|
| 278 | impl<'a> fmt::Display for AnsiString<'a> { | 
|---|
| 279 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | 
|---|
| 280 | let w: &mut dyn fmt::Write = f; | 
|---|
| 281 | self.write_to_any(w) | 
|---|
| 282 | } | 
|---|
| 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 | pub fn write_to<W: io::Write>(&self, w: &mut W) -> io::Result<()> { | 
|---|
| 289 | let w: &mut dyn io::Write = w; | 
|---|
| 290 | self.write_to_any(w) | 
|---|
| 291 | } | 
|---|
| 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 | fn write_inner<W: AnyWrite<Wstr = S> + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> { | 
|---|
| 301 | match &self.oscontrol { | 
|---|
| 302 | Some(OSControl::Link { url: u }) => { | 
|---|
| 303 | write!(w, "\x1B ]8;;")?; | 
|---|
| 304 | w.write_str(u.as_ref())?; | 
|---|
| 305 | write!(w, "\x1B\x5C ")?; | 
|---|
| 306 | w.write_str(self.string.as_ref())?; | 
|---|
| 307 | write!(w, "\x1B ]8;;\x1B\x5C ") | 
|---|
| 308 | } | 
|---|
| 309 | Some(OSControl::Title) => { | 
|---|
| 310 | write!(w, "\x1B ]2;")?; | 
|---|
| 311 | w.write_str(self.string.as_ref())?; | 
|---|
| 312 | write!(w, "\x1B\x5C ") | 
|---|
| 313 | } | 
|---|
| 314 | None => w.write_str(self.string.as_ref()), | 
|---|
| 315 | } | 
|---|
| 316 | } | 
|---|
| 317 |  | 
|---|
| 318 | fn write_to_any<W: AnyWrite<Wstr = S> + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> { | 
|---|
| 319 | write!(w, "{} ", self.style.prefix())?; | 
|---|
| 320 | self.write_inner(w)?; | 
|---|
| 321 | write!(w, "{} ", self.style.suffix()) | 
|---|
| 322 | } | 
|---|
| 323 | } | 
|---|
| 324 |  | 
|---|
| 325 | // ---- writers for combined ANSI strings ---- | 
|---|
| 326 |  | 
|---|
| 327 | impl<'a> fmt::Display for AnsiStrings<'a> { | 
|---|
| 328 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | 
|---|
| 329 | let f: &mut dyn fmt::Write = f; | 
|---|
| 330 | self.write_to_any(f) | 
|---|
| 331 | } | 
|---|
| 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 | pub fn write_to<W: io::Write>(&self, w: &mut W) -> io::Result<()> { | 
|---|
| 339 | let w: &mut dyn io::Write = w; | 
|---|
| 340 | self.write_to_any(w) | 
|---|
| 341 | } | 
|---|
| 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 | fn write_to_any<W: AnyWrite<Wstr = S> + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> { | 
|---|
| 350 | use self::Difference::*; | 
|---|
| 351 |  | 
|---|
| 352 | let first = match self.0.first() { | 
|---|
| 353 | None => return Ok(()), | 
|---|
| 354 | Some(f) => f, | 
|---|
| 355 | }; | 
|---|
| 356 |  | 
|---|
| 357 | write!(w, "{} ", first.style.prefix())?; | 
|---|
| 358 | first.write_inner(w)?; | 
|---|
| 359 |  | 
|---|
| 360 | for window in self.0.windows(2) { | 
|---|
| 361 | match Difference::between(&window[0].style, &window[1].style) { | 
|---|
| 362 | ExtraStyles(style) => write!(w, "{} ", style.prefix())?, | 
|---|
| 363 | Reset => write!(w, "{}{} ", RESET, window[1].style.prefix())?, | 
|---|
| 364 | Empty => { /* Do nothing! */ } | 
|---|
| 365 | } | 
|---|
| 366 |  | 
|---|
| 367 | 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 | if let Some(last) = self.0.last() { | 
|---|
| 374 | if !last.style.is_plain() { | 
|---|
| 375 | write!(w, "{} ", RESET)?; | 
|---|
| 376 | } | 
|---|
| 377 | } | 
|---|
| 378 |  | 
|---|
| 379 | Ok(()) | 
|---|
| 380 | } | 
|---|
| 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 | } | 
|---|
| 511 |  | 
|---|