1use indicatif::ProgressDrawTarget;
2use snowchains_core::{color_spec, web::StatusCodeColor};
3use std::{
4 fmt,
5 io::{self, BufRead, Write},
6};
7use strum::{EnumString, EnumVariantNames};
8use termcolor::{BufferedStandardStream, Color, NoColor, WriteColor};
9
10pub struct Shell {
11 input: ShellIn,
12 output: ShellOut,
13 needs_clear: bool,
14}
15
16impl Shell {
17 pub fn new() -> Self {
18 Self {
19 input: ShellIn::stdin(),
20 output: ShellOut::stream(),
21 needs_clear: false,
22 }
23 }
24
25 pub fn from_read_write(rdr: Box<dyn BufRead>, wtr: Box<dyn Write>) -> Self {
26 Self {
27 input: ShellIn::Reader(rdr),
28 output: ShellOut::Write(NoColor::new(wtr)),
29 needs_clear: false,
30 }
31 }
32
33 pub(crate) fn progress_draw_target(&self) -> ProgressDrawTarget {
34 if self.output.stderr_tty() {
35 ProgressDrawTarget::stderr()
36 } else {
37 ProgressDrawTarget::hidden()
38 }
39 }
40
41 pub(crate) fn out(&mut self) -> &mut dyn Write {
42 self.output.stdout()
43 }
44
45 pub fn err(&mut self) -> &mut dyn WriteColor {
46 self.output.stderr()
47 }
48
49 pub(crate) fn set_color_choice(&mut self, color: ColorChoice) {
50 self.output.set_color_choice(color);
51 }
52
53 pub(crate) fn warn(&mut self, message: impl fmt::Display) -> io::Result<()> {
54 if self.needs_clear {
55 self.err_erase_line();
56 }
57
58 let stderr = self.err();
59
60 stderr.set_color(color_spec!(Bold, Fg(Color::Yellow)))?;
61 write!(stderr, "warning:")?;
62 stderr.reset()?;
63
64 writeln!(stderr, " {message}")?;
65
66 stderr.flush()
67 }
68
69 pub(crate) fn status(
70 &mut self,
71 status: impl fmt::Display,
72 message: impl fmt::Display,
73 ) -> io::Result<()> {
74 self.status_with_color(status, message, Color::Green)
75 }
76
77 pub(crate) fn status_with_color(
78 &mut self,
79 status: impl fmt::Display,
80 message: impl fmt::Display,
81 color: Color,
82 ) -> io::Result<()> {
83 if self.needs_clear {
84 self.err_erase_line();
85 }
86 self.output.message_stderr(status, message, color)
87 }
88
89 fn err_erase_line(&mut self) {
90 if let ShellOut::Stream {
91 stderr,
92 stderr_tty: true,
93 ..
94 } = &mut self.output
95 {
96 err_erase_line(stderr);
97 let _ = stderr.flush();
98 self.needs_clear = false;
99 }
100
101 #[cfg(unix)]
102 fn err_erase_line(stderr: &mut impl Write) {
103 let _ = stderr.write_all(b"\x1B[K");
104 }
105
106 #[cfg(windows)]
107 fn err_erase_line(stderr: &mut impl Write) {
108 if let Some((width, _)) = term_size::dimensions_stderr() {
109 let _ = write!(stderr, "{}\r", " ".repeat(width));
110 }
111 }
112 }
113
114 pub(crate) fn read_reply(&mut self, prompt: &str) -> io::Result<String> {
115 if self.needs_clear {
116 self.err_erase_line();
117 }
118
119 let stderr = self.err();
120
121 write!(stderr, "{prompt}")?;
122 stderr.flush()?;
123 self.input.read_reply()
124 }
125
126 pub(crate) fn read_password(&mut self, prompt: &str) -> io::Result<String> {
127 if self.needs_clear {
128 self.err_erase_line();
129 }
130
131 let stderr = self.err();
132
133 write!(stderr, "{prompt}")?;
134 stderr.flush()?;
135 self.input.read_password()
136 }
137}
138
139impl Default for Shell {
140 fn default() -> Self {
141 Self::new()
142 }
143}
144
145impl snowchains_core::web::Shell for Shell {
146 fn progress_draw_target(&self) -> ProgressDrawTarget {
147 self.progress_draw_target()
148 }
149
150 fn print_ansi(&mut self, message: &[u8]) -> io::Result<()> {
151 fwdansi::write_ansi(self.err(), message)
152 }
153
154 fn warn<T: fmt::Display>(&mut self, message: T) -> io::Result<()> {
155 self.warn(message)
156 }
157
158 fn on_request(&mut self, req: &reqwest::blocking::Request) -> io::Result<()> {
159 if let ShellOut::Stream {
160 stderr,
161 stderr_tty: true,
162 ..
163 } = &mut self.output
164 {
165 stderr.set_color(color_spec!(Bold, Fg(Color::Cyan)))?;
166 write!(stderr, "{:>12}", req.method())?;
167 stderr.reset()?;
168 write!(stderr, " {} ...\r", req.url())?;
169 stderr.flush()?;
170
171 self.needs_clear = true;
172 }
173 Ok(())
174 }
175
176 fn on_response(
177 &mut self,
178 _: &reqwest::blocking::Response,
179 _: StatusCodeColor,
180 ) -> io::Result<()> {
181 if self.needs_clear {
182 self.err_erase_line();
183 }
184 Ok(())
185 }
186}
187
188enum ShellIn {
189 Tty,
190 PipedStdin,
191 Reader(Box<dyn BufRead>),
192}
193
194impl ShellIn {
195 fn stdin() -> Self {
196 if atty::is(atty::Stream::Stdin) {
197 Self::Tty
198 } else {
199 Self::PipedStdin
200 }
201 }
202}
203
204impl ShellIn {
205 fn read_reply(&mut self) -> io::Result<String> {
206 match self {
207 Self::Tty | Self::PipedStdin => rprompt::read_reply(),
208 Self::Reader(r) => rpassword::read_password_with_reader(Some(r)),
209 }
210 }
211
212 fn read_password(&mut self) -> io::Result<String> {
213 match self {
214 Self::Tty => rpassword::read_password_from_tty(None),
215 Self::PipedStdin => rprompt::read_reply(),
216 Self::Reader(r) => rpassword::read_password_with_reader(Some(r)),
217 }
218 }
219}
220
221enum ShellOut {
222 Write(NoColor<Box<dyn Write>>),
223 Stream {
224 stdout: BufferedStandardStream,
225 stderr: BufferedStandardStream,
226 stderr_tty: bool,
227 },
228}
229
230impl ShellOut {
231 fn stream() -> Self {
232 Self::Stream {
233 stdout: BufferedStandardStream::stdout(if atty::is(atty::Stream::Stdout) {
234 termcolor::ColorChoice::Auto
235 } else {
236 termcolor::ColorChoice::Never
237 }),
238 stderr: BufferedStandardStream::stderr(if atty::is(atty::Stream::Stderr) {
239 termcolor::ColorChoice::Auto
240 } else {
241 termcolor::ColorChoice::Never
242 }),
243 stderr_tty: atty::is(atty::Stream::Stderr),
244 }
245 }
246
247 fn stdout(&mut self) -> &mut dyn Write {
248 match self {
249 Self::Write(wtr) => wtr,
250 Self::Stream { stdout, .. } => stdout,
251 }
252 }
253
254 fn stderr(&mut self) -> &mut dyn WriteColor {
255 match self {
256 Self::Write(wtr) => wtr,
257 Self::Stream { stderr, .. } => stderr,
258 }
259 }
260
261 fn stderr_tty(&self) -> bool {
262 match *self {
263 Self::Write(_) => false,
264 Self::Stream { stderr_tty, .. } => stderr_tty,
265 }
266 }
267
268 fn set_color_choice(&mut self, color: ColorChoice) {
269 if let Self::Stream { stdout, stderr, .. } = self {
270 let _ = stdout.flush();
271 let _ = stderr.flush();
272
273 *stdout = BufferedStandardStream::stdout(
274 color.to_termcolor_color_choice(atty::Stream::Stdout),
275 );
276
277 *stderr = BufferedStandardStream::stderr(
278 color.to_termcolor_color_choice(atty::Stream::Stderr),
279 );
280 }
281 }
282
283 fn message_stderr(
284 &mut self,
285 status: impl fmt::Display,
286 message: impl fmt::Display,
287 color: Color,
288 ) -> io::Result<()> {
289 let stderr = self.stderr();
290
291 stderr.set_color(color_spec!(Bold, Fg(color)))?;
292 write!(stderr, "{status:>12}")?;
293 stderr.reset()?;
294
295 writeln!(stderr, " {message}")?;
296 stderr.flush()
297 }
298}
299
300#[derive(EnumString, EnumVariantNames, strum::Display, Clone, Copy, Debug)]
301#[strum(serialize_all = "kebab-case")]
302pub enum ColorChoice {
303 Auto,
304 Always,
305 Never,
306}
307
308impl ColorChoice {
309 fn to_termcolor_color_choice(self, stream: atty::Stream) -> termcolor::ColorChoice {
310 match (self, atty::is(stream)) {
311 (Self::Auto, true) => termcolor::ColorChoice::Auto,
312 (Self::Always, _) => termcolor::ColorChoice::Always,
313 _ => termcolor::ColorChoice::Never,
314 }
315 }
316}