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}