nu_ansi_term/
display.rs

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/// An `AnsiGenericString` includes a generic string type and a `Style` to
30/// display that string.  `AnsiString` and `AnsiByteString` are aliases for
31/// this type on `str` and `\[u8]`, respectively.
32#[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
42/// Cloning an `AnsiGenericString` will clone its underlying string.
43///
44/// # Examples
45///
46/// ```
47/// use nu_ansi_term::AnsiString;
48///
49/// let plain_string = AnsiString::from("a plain string");
50/// let clone_string = plain_string.clone();
51/// assert_eq!(clone_string, plain_string);
52/// ```
53impl<'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
66// You might think that the hand-written Clone impl above is the same as the
67// one that gets generated with #[derive]. But it’s not *quite* the same!
68//
69// `str` is not Clone, and the derived Clone implementation puts a Clone
70// constraint on the S type parameter (generated using --pretty=expanded):
71//
72//                  ↓_________________↓
73//     impl <'a, S: ::std::clone::Clone + 'a + ToOwned + ?Sized> ::std::clone::Clone
74//     for ANSIGenericString<'a, S> where
75//     <S as ToOwned>::Owned: fmt::Debug { ... }
76//
77// This resulted in compile errors when you tried to derive Clone on a type
78// that used it:
79//
80//     #[derive(PartialEq, Debug, Clone, Default)]
81//     pub struct TextCellContents(Vec<AnsiString<'static>>);
82//                                 ^^^^^^^^^^^^^^^^^^^^^^^^^
83//     error[E0277]: the trait `std::clone::Clone` is not implemented for `str`
84//
85// The hand-written impl above can ignore that constraint and still compile.
86
87/// An ANSI String is a string coupled with the `Style` to display it
88/// in a terminal.
89///
90/// Although not technically a string itself, it can be turned into
91/// one with the `to_string` method.
92///
93/// # Examples
94///
95/// ```
96/// use nu_ansi_term::AnsiString;
97/// use nu_ansi_term::Color::Red;
98///
99/// let red_string = Red.paint("a red string");
100/// println!("{}", red_string);
101/// ```
102///
103/// ```
104/// use nu_ansi_term::AnsiString;
105///
106/// let plain_string = AnsiString::from("a plain string");
107/// ```
108pub type AnsiString<'a> = AnsiGenericString<'a, str>;
109
110/// An `AnsiByteString` represents a formatted series of bytes.  Use
111/// `AnsiByteString` when styling text with an unknown encoding.
112pub 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    /// Directly access the style
133    pub const fn style_ref(&self) -> &Style {
134        &self.style
135    }
136
137    /// Directly access the style mutably
138    pub fn style_ref_mut(&mut self) -> &mut Style {
139        &mut self.style
140    }
141
142    /// Directly access the underlying string
143    pub fn as_str(&self) -> &S {
144        self.string.as_ref()
145    }
146
147    // Instances that imply wrapping in OSC sequences
148    // and do not get displayed in the terminal text
149    // area.
150    //
151    /// Produce an ANSI string that changes the title shown
152    /// by the terminal emulator.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use nu_ansi_term::AnsiGenericString;
158    /// let title_string = AnsiGenericString::title("My Title");
159    /// println!("{}", title_string);
160    /// ```
161    /// Should produce an empty line but set the terminal title.
162    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    //
174    // Annotations (OSC sequences that do more than wrap)
175    //
176
177    /// Cause the styled ANSI string to link to the given URL
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// use nu_ansi_term::Color::Red;
183    ///
184    /// let link_string = Red.paint("a red string").hyperlink("https://www.example.com");
185    /// println!("{}", link_string);
186    /// ```
187    /// Should show a red-painted string which, on terminals
188    /// that support it, is a clickable hyperlink.
189    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    /// Get any URL associated with the string
198    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/// A set of `AnsiGenericStrings`s collected together, in order to be
207/// written with a minimum of control characters.
208#[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
214/// A set of `AnsiString`s collected together, in order to be written with a
215/// minimum of control characters.
216pub type AnsiStrings<'a> = AnsiGenericStrings<'a, str>;
217
218/// A function to construct an `AnsiStrings` instance.
219#[allow(non_snake_case)]
220pub const fn AnsiStrings<'a>(arg: &'a [AnsiString<'a>]) -> AnsiStrings<'a> {
221    AnsiGenericStrings(arg)
222}
223
224/// A set of `AnsiByteString`s collected together, in order to be
225/// written with a minimum of control characters.
226pub type AnsiByteStrings<'a> = AnsiGenericStrings<'a, [u8]>;
227
228/// A function to construct an `AnsiByteStrings` instance.
229#[allow(non_snake_case)]
230pub const fn AnsiByteStrings<'a>(arg: &'a [AnsiByteString<'a>]) -> AnsiByteStrings<'a> {
231    AnsiGenericStrings(arg)
232}
233
234// ---- paint functions ----
235
236impl Style {
237    /// Paints the given text with this color, returning an ANSI string.
238    #[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    /// Paints the given text with this color, returning an ANSI string.
254    /// This is a short-cut so you don’t have to use `Blue.normal()` just
255    /// to get blue text.
256    ///
257    /// ```
258    /// use nu_ansi_term::Color::Blue;
259    /// println!("{}", Blue.paint("da ba dee"));
260    /// ```
261    #[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
275// ---- writers for individual ANSI strings ----
276
277impl<'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    /// 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: 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    // 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
327impl<'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    /// Write `AnsiByteStrings` to an `io::Write`.  This writes the minimal
337    /// escape sequences for the associated `Style`s around each set of
338    /// bytes.
339    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 => { /* Do nothing! */ }
366            }
367
368            window[1].write_inner(w)?;
369        }
370
371        // Write the final reset string after all of the AnsiStrings have been
372        // written, *except* if the last one has no styles, because it would
373        // have already been written by this point.
374        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// ---- tests ----
385
386#[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    // NOTE: unstyled because it could have OSC escape sequences
401    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        // check that RESET precedes unstyled
409        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        // check that RESET does not follow unstyled when appending styled
419        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        // does not introduce spurious SGR codes (reset or otherwise) adjacent
429        // to plain strings
430        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        // Assemble with link by itself
484        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        // Assemble with link in the middle
491        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        // Assemble with link first
498        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        // Assemble with link at the end
505        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}