1use crate::ansi::RESET;
2use crate::difference::Difference;
3use crate::style::{Color, Style};
4use crate::write::AnyWrite;
5use alloc::borrow::{Cow, ToOwned};
6use core::fmt;
7
8#[derive(Eq, PartialEq, Debug)]
9enum OSControl<'a, S: 'a + ToOwned + ?Sized>
10where
11 <S as ToOwned>::Owned: fmt::Debug,
12{
13 Title,
14 Link { url: Cow<'a, S> },
15}
16
17impl<'a, S: 'a + ToOwned + ?Sized> Clone for OSControl<'a, S>
18where
19 <S as ToOwned>::Owned: fmt::Debug,
20{
21 fn clone(&self) -> Self {
22 match self {
23 Self::Link { url: u } => Self::Link { url: u.clone() },
24 Self::Title => Self::Title,
25 }
26 }
27}
28
29#[derive(Eq, PartialEq, Debug)]
33pub struct AnsiGenericString<'a, S: 'a + ToOwned + ?Sized>
34where
35 <S as ToOwned>::Owned: fmt::Debug,
36{
37 pub(crate) style: Style,
38 pub(crate) string: Cow<'a, S>,
39 oscontrol: Option<OSControl<'a, S>>,
40}
41
42impl<'a, S: 'a + ToOwned + ?Sized> Clone for AnsiGenericString<'a, S>
54where
55 <S as ToOwned>::Owned: fmt::Debug,
56{
57 fn clone(&self) -> AnsiGenericString<'a, S> {
58 AnsiGenericString {
59 style: self.style,
60 string: self.string.clone(),
61 oscontrol: self.oscontrol.clone(),
62 }
63 }
64}
65
66pub type AnsiString<'a> = AnsiGenericString<'a, str>;
109
110pub type AnsiByteString<'a> = AnsiGenericString<'a, [u8]>;
113
114impl<'a, I, S: 'a + ToOwned + ?Sized> From<I> for AnsiGenericString<'a, S>
115where
116 I: Into<Cow<'a, S>>,
117 <S as ToOwned>::Owned: fmt::Debug,
118{
119 fn from(input: I) -> AnsiGenericString<'a, S> {
120 AnsiGenericString {
121 string: input.into(),
122 style: Style::default(),
123 oscontrol: None,
124 }
125 }
126}
127
128impl<'a, S: 'a + ToOwned + ?Sized> AnsiGenericString<'a, S>
129where
130 <S as ToOwned>::Owned: fmt::Debug,
131{
132 pub const fn style_ref(&self) -> &Style {
134 &self.style
135 }
136
137 pub fn style_ref_mut(&mut self) -> &mut Style {
139 &mut self.style
140 }
141
142 pub fn as_str(&self) -> &S {
144 self.string.as_ref()
145 }
146
147 pub fn title<I>(s: I) -> Self
163 where
164 I: Into<Cow<'a, S>>,
165 {
166 Self {
167 style: Style::default(),
168 string: s.into(),
169 oscontrol: Some(OSControl::<'a, S>::Title),
170 }
171 }
172
173 pub fn hyperlink<I>(mut self, url: I) -> Self
190 where
191 I: Into<Cow<'a, S>>,
192 {
193 self.oscontrol = Some(OSControl::Link { url: url.into() });
194 self
195 }
196
197 pub fn url_string(&self) -> Option<&S> {
199 match &self.oscontrol {
200 Some(OSControl::Link { url: u }) => Some(u.as_ref()),
201 _ => None,
202 }
203 }
204}
205
206#[derive(Debug, Eq, PartialEq)]
209pub struct AnsiGenericStrings<'a, S: 'a + ToOwned + ?Sized>(pub &'a [AnsiGenericString<'a, S>])
210where
211 <S as ToOwned>::Owned: fmt::Debug,
212 S: PartialEq;
213
214pub type AnsiStrings<'a> = AnsiGenericStrings<'a, str>;
217
218#[allow(non_snake_case)]
220pub const fn AnsiStrings<'a>(arg: &'a [AnsiString<'a>]) -> AnsiStrings<'a> {
221 AnsiGenericStrings(arg)
222}
223
224pub type AnsiByteStrings<'a> = AnsiGenericStrings<'a, [u8]>;
227
228#[allow(non_snake_case)]
230pub const fn AnsiByteStrings<'a>(arg: &'a [AnsiByteString<'a>]) -> AnsiByteStrings<'a> {
231 AnsiGenericStrings(arg)
232}
233
234impl Style {
237 #[must_use]
239 pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>(self, input: I) -> AnsiGenericString<'a, S>
240 where
241 I: Into<Cow<'a, S>>,
242 <S as ToOwned>::Owned: fmt::Debug,
243 {
244 AnsiGenericString {
245 string: input.into(),
246 style: self,
247 oscontrol: None,
248 }
249 }
250}
251
252impl Color {
253 #[must_use]
262 pub fn paint<'a, I, S: 'a + ToOwned + ?Sized>(self, input: I) -> AnsiGenericString<'a, S>
263 where
264 I: Into<Cow<'a, S>>,
265 <S as ToOwned>::Owned: fmt::Debug,
266 {
267 AnsiGenericString {
268 string: input.into(),
269 style: self.normal(),
270 oscontrol: None,
271 }
272 }
273}
274
275impl<'a> fmt::Display for AnsiString<'a> {
278 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
279 let w: &mut dyn fmt::Write = f;
280 self.write_to_any(w)
281 }
282}
283
284#[cfg(feature = "std")]
285impl<'a> AnsiByteString<'a> {
286 pub fn write_to<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
289 let w: &mut dyn std::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
334#[cfg(feature = "std")]
335impl<'a> AnsiByteStrings<'a> {
336 pub fn write_to<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
340 let w: &mut dyn std::io::Write = w;
341 self.write_to_any(w)
342 }
343}
344
345impl<'a, S: 'a + ToOwned + ?Sized + PartialEq> AnsiGenericStrings<'a, S>
346where
347 <S as ToOwned>::Owned: fmt::Debug,
348 &'a S: AsRef<[u8]>,
349{
350 fn write_to_any<W: AnyWrite<Wstr = S> + ?Sized>(&self, w: &mut W) -> Result<(), W::Error> {
351 use self::Difference::*;
352
353 let first = match self.0.first() {
354 None => return Ok(()),
355 Some(f) => f,
356 };
357
358 write!(w, "{}", first.style.prefix())?;
359 first.write_inner(w)?;
360
361 for window in self.0.windows(2) {
362 match Difference::between(&window[0].style, &window[1].style) {
363 ExtraStyles(style) => write!(w, "{}", style.prefix())?,
364 Reset => write!(w, "{}{}", RESET, window[1].style.prefix())?,
365 Empty => { }
366 }
367
368 window[1].write_inner(w)?;
369 }
370
371 if let Some(last) = self.0.last() {
375 if !last.style.is_plain() {
376 write!(w, "{}", RESET)?;
377 }
378 }
379
380 Ok(())
381 }
382}
383
384#[cfg(test)]
387mod tests {
388 pub use super::super::{AnsiGenericString, AnsiStrings};
389 pub use crate::style::Color::*;
390 pub use crate::style::Style;
391
392 #[test]
393 fn no_control_codes_for_plain() {
394 let one = Style::default().paint("one");
395 let two = Style::default().paint("two");
396 let output = AnsiStrings(&[one, two]).to_string();
397 assert_eq!(output, "onetwo");
398 }
399
400 fn idempotent(unstyled: AnsiGenericString<'_, str>) {
402 let before_g = Green.paint("Before is Green. ");
403 let before = Style::default().paint("Before is Plain. ");
404 let after_g = Green.paint(" After is Green.");
405 let after = Style::default().paint(" After is Plain.");
406 let unstyled_s = unstyled.clone().to_string();
407
408 let joined = AnsiStrings(&[before_g.clone(), unstyled.clone()]).to_string();
410 assert!(joined.starts_with("\x1B[32mBefore is Green. \x1B[0m"));
411 assert!(
412 joined.ends_with(unstyled_s.as_str()),
413 "{:?} does not end with {:?}",
414 joined,
415 unstyled_s
416 );
417
418 let joined = AnsiStrings(&[unstyled.clone(), after_g.clone()]).to_string();
420 assert!(
421 joined.starts_with(unstyled_s.as_str()),
422 "{:?} does not start with {:?}",
423 joined,
424 unstyled_s
425 );
426 assert!(joined.ends_with("\x1B[32m After is Green.\x1B[0m"));
427
428 let joined = AnsiStrings(&[unstyled.clone()]).to_string();
431 assert!(
432 !joined.contains("\x1B["),
433 "{:?} does contain \\x1B[",
434 joined
435 );
436 let joined = AnsiStrings(&[before.clone(), unstyled.clone()]).to_string();
437 assert!(
438 !joined.contains("\x1B["),
439 "{:?} does contain \\x1B[",
440 joined
441 );
442 let joined = AnsiStrings(&[before.clone(), unstyled.clone(), after.clone()]).to_string();
443 assert!(
444 !joined.contains("\x1B["),
445 "{:?} does contain \\x1B[",
446 joined
447 );
448 let joined = AnsiStrings(&[unstyled.clone(), after.clone()]).to_string();
449 assert!(
450 !joined.contains("\x1B["),
451 "{:?} does contain \\x1B[",
452 joined
453 );
454 }
455
456 #[test]
457 fn title() {
458 let title = AnsiGenericString::title("Test Title");
459 assert_eq!(title.clone().to_string(), "\x1B]2;Test Title\x1B\\");
460 idempotent(title)
461 }
462
463 #[test]
464 fn hyperlink() {
465 let styled = Red
466 .paint("Link to example.com.")
467 .hyperlink("https://example.com");
468 assert_eq!(
469 styled.to_string(),
470 "\x1B[31m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m"
471 );
472 }
473
474 #[test]
475 fn hyperlinks() {
476 let before = Green.paint("Before link. ");
477 let link = Blue
478 .underline()
479 .paint("Link to example.com.")
480 .hyperlink("https://example.com");
481 let after = Green.paint(" After link.");
482
483 let joined = AnsiStrings(&[link.clone()]).to_string();
485 #[cfg(feature = "gnu_legacy")]
486 assert_eq!(joined, format!("\x1B[04;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m"));
487 #[cfg(not(feature = "gnu_legacy"))]
488 assert_eq!(joined, format!("\x1B[4;34m\x1B]8;;https://example.com\x1B\\Link to example.com.\x1B]8;;\x1B\\\x1B[0m"));
489
490 let joined = AnsiStrings(&[before.clone(), link.clone(), after.clone()]).to_string();
492 #[cfg(feature = "gnu_legacy")]
493 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"));
494 #[cfg(not(feature = "gnu_legacy"))]
495 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"));
496
497 let joined = AnsiStrings(&[link.clone(), after.clone()]).to_string();
499 #[cfg(feature = "gnu_legacy")]
500 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"));
501 #[cfg(not(feature = "gnu_legacy"))]
502 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"));
503
504 let joined = AnsiStrings(&[before.clone(), link.clone()]).to_string();
506 #[cfg(feature = "gnu_legacy")]
507 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"));
508 #[cfg(not(feature = "gnu_legacy"))]
509 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"));
510 }
511}