nu_ansi_term/
display.rs

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/// 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)]
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
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/// ```
54impl<'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
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/// ```
109pub 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.
113pub 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    /// 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)]
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
215/// A set of `AnsiString`s collected together, in order to be written with a
216/// minimum of control characters.
217pub type AnsiStrings<'a> = AnsiGenericStrings<'a, str>;
218
219/// A function to construct an `AnsiStrings` instance.
220#[allow(non_snake_case)]
221pub 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.
227pub type AnsiByteStrings<'a> = AnsiGenericStrings<'a, [u8]>;
228
229/// A function to construct an `AnsiByteStrings` instance.
230#[allow(non_snake_case)]
231pub const fn AnsiByteStrings<'a>(arg: &'a [AnsiByteString<'a>]) -> AnsiByteStrings<'a> {
232    AnsiGenericStrings(arg)
233}
234
235// ---- paint functions ----
236
237impl 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
253impl 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
278impl<'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    /// 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
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
334impl<'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
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 => { /* 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)]
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    // 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}