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}