1use crate::ansi::RESET;
2use crate::difference::Difference;
3use crate::style::{Color, Style};
4use crate::write::AnyWrite;
5use std::borrow::Cow;
6use std::fmt;
7use std::io;
8
9#[derive(Eq, PartialEq, Debug)]
10enum OSControl<'a, S: 'a + ToOwned + ?Sized>
11where
12 <S as ToOwned>::Owned: fmt::Debug,
13{
14 Title,
15 Link { url: Cow<'a, S> },
16}
17
18impl<'a, S: 'a + ToOwned + ?Sized> Clone for OSControl<'a, S>
19where
20 <S as ToOwned>::Owned: fmt::Debug,
21{
22 fn clone(&self) -> Self {
23 match self {
24 Self::Link { url: u } => Self::Link { url: u.clone() },
25 Self::Title => Self::Title,
26 }
27 }
28}
29
30#[derive(Eq, PartialEq, Debug)]
34pub struct AnsiGenericString<'a, S: 'a + ToOwned + ?Sized>
35where
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
43impl<'a, S: 'a + ToOwned + ?Sized> Clone for AnsiGenericString<'a, S>
55where
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
67pub type AnsiString<'a> = AnsiGenericString<'a, str>;
110
111pub type AnsiByteString<'a> = AnsiGenericString<'a, [u8]>;
114
115impl<'a, I, S: 'a + ToOwned + ?Sized> From<I> for AnsiGenericString<'a, S>
116where
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
129impl<'a, S: 'a + ToOwned + ?Sized> AnsiGenericString<'a, S>
130where
131 <S as ToOwned>::Owned: fmt::Debug,
132{
133 pub const fn style_ref(&self) -> &Style {
135 &self.style
136 }
137
138 pub fn style_ref_mut(&mut self) -> &mut Style {
140 &mut self.style
141 }
142
143 pub fn as_str(&self) -> &S {
145 self.string.as_ref()
146 }
147
148 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 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 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#[derive(Debug, Eq, PartialEq)]
210pub struct AnsiGenericStrings<'a, S: 'a + ToOwned + ?Sized>(pub &'a [AnsiGenericString<'a, S>])
211where
212 <S as ToOwned>::Owned: fmt::Debug,
213 S: PartialEq;
214
215pub type AnsiStrings<'a> = AnsiGenericStrings<'a, str>;
218
219#[allow(non_snake_case)]
221pub const fn AnsiStrings<'a>(arg: &'a [AnsiString<'a>]) -> AnsiStrings<'a> {
222 AnsiGenericStrings(arg)
223}
224
225pub type AnsiByteStrings<'a> = AnsiGenericStrings<'a, [u8]>;
228
229#[allow(non_snake_case)]
231pub const fn AnsiByteStrings<'a>(arg: &'a [AnsiByteString<'a>]) -> AnsiByteStrings<'a> {
232 AnsiGenericStrings(arg)
233}
234
235impl Style {
238 #[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
253impl Color {
254 #[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
276impl<'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
285impl<'a> AnsiByteString<'a> {
286 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
294impl<'a, S: 'a + ToOwned + ?Sized> AnsiGenericString<'a, S>
295where
296 <S as ToOwned>::Owned: fmt::Debug,
297 &'a S: AsRef<[u8]>,
298{
299 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
325impl<'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
334impl<'a> AnsiByteStrings<'a> {
335 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
344impl<'a, S: 'a + ToOwned + ?Sized + PartialEq> AnsiGenericStrings<'a, S>
345where
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 => { }
365 }
366
367 window[1].write_inner(w)?;
368 }
369
370 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#[cfg(test)]
386mod 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 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 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 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 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 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 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 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 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}