Skip to main content

ctl_term/
console.rs

1//! The writer: renders a widget at the stream's width and writes it.
2
3use std::error::Error as StdError;
4use std::fmt;
5use std::io::{self, Write};
6
7use ctl_text::{Context, Renderable};
8
9#[cfg(feature = "detect")]
10use crate::color::ColorMode;
11use crate::encode::Encoder;
12use crate::stream::Stream;
13#[cfg(feature = "detect")]
14use crate::terminal::DetectOptions;
15use crate::terminal::Terminal;
16
17/// Why writing failed.
18///
19/// A closed pipe is its own variant because it is not a failure of the
20/// program. A reader such as `head` that has read enough closes the stream,
21/// and the caller usually exits quietly. This crate does not choose the exit
22/// code.
23#[derive(Debug)]
24#[non_exhaustive]
25pub enum Error {
26    /// The reader closed the stream before the output was written.
27    BrokenPipe(io::Error),
28    /// Any other write or flush failure.
29    Io(io::Error),
30}
31
32impl fmt::Display for Error {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            Self::BrokenPipe(_) => f.write_str("the reader closed the output stream"),
36            Self::Io(_) => f.write_str("could not write the output"),
37        }
38    }
39}
40
41impl StdError for Error {
42    fn source(&self) -> Option<&(dyn StdError + 'static)> {
43        match self {
44            Self::BrokenPipe(source) | Self::Io(source) => Some(source),
45        }
46    }
47}
48
49impl From<io::Error> for Error {
50    /// Sorts a closed pipe from every other failure.
51    fn from(error: io::Error) -> Self {
52        if error.kind() == io::ErrorKind::BrokenPipe {
53            Self::BrokenPipe(error)
54        } else {
55            Self::Io(error)
56        }
57    }
58}
59
60impl From<Error> for io::Error {
61    /// [`Error::Io`] gives back the error it holds, with its message.
62    /// [`Error::BrokenPipe`] always converts to an error of
63    /// [`io::ErrorKind::BrokenPipe`]. When the error it holds already reports
64    /// `BrokenPipe`, as every one [`From<io::Error>`] builds does, it gives
65    /// that error back, so the OS error code survives. Otherwise it gives a new
66    /// `BrokenPipe` error.
67    ///
68    /// Nothing is boxed, so a caller that returns [`io::Result`] keeps a closed
69    /// pipe apart without carrying this type's formatting code.
70    fn from(error: Error) -> Self {
71        match error {
72            Error::BrokenPipe(error) if error.kind() == io::ErrorKind::BrokenPipe => error,
73            Error::BrokenPipe(_) => io::ErrorKind::BrokenPipe.into(),
74            Error::Io(error) => error,
75        }
76    }
77}
78
79/// Writes widgets to one output stream, in the colour and at the width
80/// decided for that stream.
81///
82/// A console renders a [`Renderable`] at the stream's width, encodes the lines
83/// with [`Encoder`], and writes them. With colour off it writes the plain text
84/// of the lines. A stream with no width, because the fallback is off, renders
85/// in an [unbounded](Context::unbounded) context, so a widget takes its
86/// natural width.
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub struct Console {
89    stream: Stream,
90    terminal: Terminal,
91}
92
93impl Console {
94    /// A console for `stream` that follows the decision in `terminal`.
95    #[must_use]
96    pub fn new(stream: Stream, terminal: Terminal) -> Self {
97        Self { stream, terminal }
98    }
99
100    /// A console for `stream` that decides from the process environment and
101    /// the stream, under `options`. Needs the `detect` feature.
102    #[cfg(feature = "detect")]
103    #[must_use]
104    pub fn from_process(stream: Stream, mode: ColorMode, options: DetectOptions) -> Self {
105        Self::new(stream, Terminal::from_process(stream, mode, options))
106    }
107
108    /// The stream this console writes to.
109    #[must_use]
110    pub fn stream(self) -> Stream {
111        self.stream
112    }
113
114    /// The colour, depth, and width decided for the stream.
115    #[must_use]
116    pub fn terminal(self) -> Terminal {
117        self.terminal
118    }
119
120    /// The text `print` writes for `renderable`, without writing it.
121    #[must_use]
122    pub fn render(self, renderable: &dyn Renderable) -> String {
123        let context = self
124            .terminal
125            .width()
126            .map_or_else(Context::unbounded, Context::new);
127        let lines = renderable.render(&context);
128        Encoder::new(self.terminal.color()).encode(&lines)
129    }
130
131    /// Writes `renderable` to `writer` as `print` would write it to the
132    /// stream, then flushes.
133    ///
134    /// # Errors
135    ///
136    /// [`Error::BrokenPipe`] when the reader has closed `writer`, and
137    /// [`Error::Io`] for any other write or flush failure.
138    pub fn write_to(
139        self,
140        writer: &mut dyn Write,
141        renderable: &dyn Renderable,
142    ) -> Result<(), Error> {
143        writer.write_all(self.render(renderable).as_bytes())?;
144        writer.flush()?;
145        Ok(())
146    }
147
148    /// Writes `renderable` to the stream, then flushes. Needs the `detect`
149    /// feature.
150    ///
151    /// # Errors
152    ///
153    /// [`Error::BrokenPipe`] when the reader has closed the stream, and
154    /// [`Error::Io`] for any other write or flush failure.
155    #[cfg(feature = "detect")]
156    pub fn print(self, renderable: &dyn Renderable) -> Result<(), Error> {
157        match self.stream {
158            Stream::Stdout => self.write_to(&mut io::stdout().lock(), renderable),
159            Stream::Stderr => self.write_to(&mut io::stderr().lock(), renderable),
160        }
161    }
162}