Skip to main content

ctl_term/
stream.rs

1//! The output stream a colour and width decision is made for.
2
3#[cfg(feature = "detect")]
4use std::io::{self, IsTerminal};
5
6#[cfg(all(feature = "detect", unix))]
7use terminal_size::{Width, terminal_size_of};
8
9/// A standard output stream.
10///
11/// Colour and width are decided for the stream the output goes to, so stdout
12/// can be a pipe while stderr is a terminal.
13#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
14#[non_exhaustive]
15pub enum Stream {
16    /// Standard output.
17    Stdout,
18    /// Standard error.
19    Stderr,
20}
21
22/// What the operating system says about one stream: redirected away from a
23/// terminal, or a terminal of some width.
24///
25/// [`Terminal::from_probe`](crate::Terminal::from_probe) turns a probe into a
26/// decision without touching the process, so a test chooses the probe to
27/// use.
28#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
29#[non_exhaustive]
30pub enum Probe {
31    /// A pipe, a file, or a closed stream: anything but a terminal.
32    Redirected,
33    /// A terminal. Build it with [`Probe::terminal`].
34    #[non_exhaustive]
35    Terminal {
36        /// The window width in cells, when the system reports a non-zero one.
37        columns: Option<u16>,
38    },
39}
40
41impl Probe {
42    /// A terminal whose window is `columns` cells wide, or whose width the
43    /// system does not report, for `None`. A width of 0 counts as not reported.
44    #[must_use]
45    pub const fn terminal(columns: Option<u16>) -> Self {
46        match columns {
47            Some(0) => Self::Terminal { columns: None },
48            Some(_) | None => Self::Terminal { columns },
49        }
50    }
51
52    /// What the system says about `stream` right now. Needs the `detect`
53    /// feature.
54    #[cfg(feature = "detect")]
55    #[must_use]
56    pub fn of(stream: Stream) -> Self {
57        match stream {
58            Stream::Stdout => {
59                let handle = io::stdout();
60                if handle.is_terminal() {
61                    Self::terminal(columns_of(handle))
62                } else {
63                    Self::Redirected
64                }
65            }
66            Stream::Stderr => {
67                let handle = io::stderr();
68                if handle.is_terminal() {
69                    Self::terminal(columns_of(handle))
70                } else {
71                    Self::Redirected
72                }
73            }
74        }
75    }
76
77    /// Whether the stream is a terminal, whether or not it reports a width.
78    #[must_use]
79    pub fn is_terminal(self) -> bool {
80        matches!(self, Self::Terminal { .. })
81    }
82
83    /// The terminal's width in cells, when it is a terminal that reports one.
84    #[must_use]
85    pub fn columns(self) -> Option<u16> {
86        match self {
87            Self::Redirected => None,
88            Self::Terminal { columns } => columns,
89        }
90    }
91}
92
93#[cfg(all(feature = "detect", unix))]
94fn columns_of(stream: impl std::os::fd::AsFd) -> Option<u16> {
95    terminal_size_of(stream).map(|(Width(columns), _)| columns)
96}
97
98/// Only Unix reports a window width. The crate supports and tests Unix only,
99/// so another target gets no width from the window.
100#[cfg(all(feature = "detect", not(unix)))]
101fn columns_of<T>(_stream: T) -> Option<u16> {
102    None
103}