Coverage Report

Created: 2025-10-28 06:54

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