cross/
shell.rs

1// This file was adapted from:
2//   https://github.com/rust-lang/cargo/blob/ca4edabb28fc96fdf2a1d56fe3851831ac166f8a/src/cargo/core/shell.rs
3
4use std::env;
5use std::fmt;
6use std::io::{self, Write};
7use std::str::FromStr;
8
9use crate::errors::Result;
10use owo_colors::{self, OwoColorize};
11
12// get the prefix for stderr messages
13macro_rules! cross_prefix {
14    ($s:literal) => {
15        concat!("[cross]", " ", $s)
16    };
17}
18
19// generate the color style
20macro_rules! write_style {
21    ($stream:ident, $msg_info:expr, $message:expr $(, $style:ident)* $(,)?) => {{
22        match $msg_info.color_choice {
23            ColorChoice::Always => write!($stream, "{}", $message $(.$style())*),
24            ColorChoice::Never => write!($stream, "{}", $message),
25            ColorChoice::Auto => write!(
26                $stream,
27                "{}",
28                $message $(.if_supports_color($stream.owo(), |text| text.$style()))*
29            ),
30        }?;
31    }};
32}
33
34// low-level interface for printing colorized messages
35macro_rules! message {
36    // write a status message, which has the following format:
37    //  "{status}: {message}"
38    // both status and ':' are bold.
39    (@status $stream:ident, $status:expr, $message:expr, $color:ident, $msg_info:expr $(,)?) => {{
40        write_style!($stream, $msg_info, $status, bold, $color);
41        write_style!($stream, $msg_info, ":", bold);
42        match $message {
43            Some(message) => writeln!($stream, " {}", message)?,
44            None => write!($stream, " ")?,
45        }
46
47        Ok(())
48    }};
49
50    (@status @name $name:ident, $status:expr, $message:expr, $color:ident, $msg_info:expr $(,)?) => {{
51        let mut stream = io::$name();
52        message!(@status stream, $status, $message, $color, $msg_info)
53    }};
54}
55
56// high-level interface to message
57macro_rules! status {
58    (@stderr $status:expr, $message:expr, $color:ident, $msg_info:expr $(,)?) => {{
59        message!(@status @name stderr, $status, $message, $color, $msg_info)
60    }};
61
62    (@stdout $status:expr, $message:expr, $color:ident, $msg_info:expr  $(,)?) => {{
63        message!(@status @name stdout, $status, $message, $color, $msg_info)
64    }};
65}
66
67/// the requested verbosity of output.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum Verbosity {
70    Quiet,
71    Normal,
72    Verbose,
73}
74
75impl Verbosity {
76    pub fn verbose(self) -> bool {
77        match self {
78            Self::Verbose => true,
79            Self::Normal | Self::Quiet => false,
80        }
81    }
82
83    fn create(color_choice: ColorChoice, verbose: bool, quiet: bool) -> Option<Self> {
84        match (verbose, quiet) {
85            (true, true) => {
86                MessageInfo::from(color_choice).fatal("cannot set both --verbose and --quiet", 101)
87            }
88            (true, false) => Some(Verbosity::Verbose),
89            (false, true) => Some(Verbosity::Quiet),
90            (false, false) => None,
91        }
92    }
93}
94
95impl Default for Verbosity {
96    fn default() -> Verbosity {
97        Verbosity::Normal
98    }
99}
100
101/// Whether messages should use color output
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum ColorChoice {
104    /// force color output
105    Always,
106    /// force disable color output
107    Never,
108    /// intelligently guess whether to use color output
109    Auto,
110}
111
112impl FromStr for ColorChoice {
113    type Err = eyre::ErrReport;
114
115    fn from_str(s: &str) -> Result<ColorChoice> {
116        match s {
117            "always" => Ok(ColorChoice::Always),
118            "never" => Ok(ColorChoice::Never),
119            "auto" => Ok(ColorChoice::Auto),
120            arg => eyre::bail!(
121                "argument for --color must be auto, always, or never, but found `{arg}`"
122            ),
123        }
124    }
125}
126
127// Should simplify the APIs a lot.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct MessageInfo {
130    pub color_choice: ColorChoice,
131    pub verbosity: Verbosity,
132    pub stdout_needs_erase: bool,
133    pub stderr_needs_erase: bool,
134}
135
136impl MessageInfo {
137    pub const fn new(color_choice: ColorChoice, verbosity: Verbosity) -> MessageInfo {
138        MessageInfo {
139            color_choice,
140            verbosity,
141            stdout_needs_erase: false,
142            stderr_needs_erase: false,
143        }
144    }
145
146    pub fn create(verbose: bool, quiet: bool, color: Option<&str>) -> Result<MessageInfo> {
147        let color_choice = get_color_choice(color)?;
148        let verbosity = get_verbosity(color_choice, verbose, quiet)?;
149
150        Ok(MessageInfo {
151            color_choice,
152            verbosity,
153            stdout_needs_erase: false,
154            stderr_needs_erase: false,
155        })
156    }
157
158    #[must_use]
159    pub fn is_verbose(&self) -> bool {
160        self.verbosity.verbose()
161    }
162
163    fn as_verbosity<T, C: Fn(&mut MessageInfo) -> T>(&mut self, call: C, new: Verbosity) -> T {
164        let old = self.verbosity;
165        self.verbosity = new;
166        let result = call(self);
167        self.verbosity = old;
168
169        result
170    }
171
172    pub fn as_quiet<T, C: Fn(&mut MessageInfo) -> T>(&mut self, call: C) -> T {
173        self.as_verbosity(call, Verbosity::Quiet)
174    }
175
176    pub fn as_normal<T, C: Fn(&mut MessageInfo) -> T>(&mut self, call: C) -> T {
177        self.as_verbosity(call, Verbosity::Normal)
178    }
179
180    pub fn as_verbose<T, C: Fn(&mut MessageInfo) -> T>(&mut self, call: C) -> T {
181        self.as_verbosity(call, Verbosity::Verbose)
182    }
183
184    fn erase_line<S: Stream + Write>(&mut self, stream: &mut S) -> Result<()> {
185        // this is the Erase in Line sequence
186        stream.write_all(b"\x1B[K").map_err(Into::into)
187    }
188
189    fn stdout_check_erase(&mut self) -> Result<()> {
190        if self.stdout_needs_erase {
191            self.erase_line(&mut io::stdout())?;
192            self.stdout_needs_erase = false;
193        }
194        Ok(())
195    }
196
197    fn stderr_check_erase(&mut self) -> Result<()> {
198        if self.stderr_needs_erase {
199            self.erase_line(&mut io::stderr())?;
200            self.stderr_needs_erase = false;
201        }
202        Ok(())
203    }
204
205    /// prints a red 'error' message and terminates.
206    pub fn fatal<T: fmt::Display>(&mut self, message: T, code: i32) -> ! {
207        self.error(message)
208            .expect("could not display fatal message");
209        std::process::exit(code);
210    }
211
212    /// prints a red 'error' message.
213    pub fn error<T: fmt::Display>(&mut self, message: T) -> Result<()> {
214        self.stderr_check_erase()?;
215        status!(@stderr cross_prefix!("error"), Some(&message), red, self)
216    }
217
218    /// prints an amber 'warning' message.
219    pub fn warn<T: fmt::Display>(&mut self, message: T) -> Result<()> {
220        match self.verbosity {
221            Verbosity::Quiet => Ok(()),
222            _ => status!(@stderr
223                cross_prefix!("warning"),
224                Some(&message),
225                yellow,
226                self,
227            ),
228        }
229    }
230
231    /// prints a cyan 'note' message.
232    pub fn note<T: fmt::Display>(&mut self, message: T) -> Result<()> {
233        match self.verbosity {
234            Verbosity::Quiet => Ok(()),
235            _ => status!(@stderr cross_prefix!("note"), Some(&message), cyan, self),
236        }
237    }
238
239    pub fn status<T: fmt::Display>(&mut self, message: T) -> Result<()> {
240        match self.verbosity {
241            Verbosity::Quiet => Ok(()),
242            _ => {
243                eprintln!("{}", message);
244                Ok(())
245            }
246        }
247    }
248
249    /// prints a high-priority message to stdout.
250    pub fn print<T: fmt::Display>(&mut self, message: T) -> Result<()> {
251        self.stdout_check_erase()?;
252        println!("{}", message);
253        Ok(())
254    }
255
256    /// prints a normal message to stdout.
257    pub fn info<T: fmt::Display>(&mut self, message: T) -> Result<()> {
258        match self.verbosity {
259            Verbosity::Quiet => Ok(()),
260            _ => {
261                println!("{}", message);
262                Ok(())
263            }
264        }
265    }
266
267    /// prints a debugging message to stdout.
268    pub fn debug<T: fmt::Display>(&mut self, message: T) -> Result<()> {
269        match self.verbosity {
270            Verbosity::Quiet | Verbosity::Normal => Ok(()),
271            _ => {
272                println!("{}", message);
273                Ok(())
274            }
275        }
276    }
277
278    pub fn fatal_usage<T: fmt::Display>(
279        &mut self,
280        arg: T,
281        provided: Option<&str>,
282        possible: Option<&[&str]>,
283        code: i32,
284    ) -> ! {
285        self.error_usage(arg, provided, possible)
286            .expect("could not display usage message");
287        std::process::exit(code);
288    }
289
290    fn error_usage<T: fmt::Display>(
291        &mut self,
292        arg: T,
293        provided: Option<&str>,
294        possible: Option<&[&str]>,
295    ) -> Result<()> {
296        let mut stream = io::stderr();
297        write_style!(stream, self, cross_prefix!("error"), bold, red);
298        write_style!(stream, self, ":", bold);
299        match provided {
300            Some(value) => {
301                write_style!(
302                    stream,
303                    self,
304                    format_args!(" \"{value}\" isn't a valid value for '")
305                );
306                write_style!(stream, self, arg, yellow);
307                write_style!(stream, self, "'\n");
308            }
309            None => {
310                write_style!(stream, self, " The argument '");
311                write_style!(stream, self, arg, yellow);
312                write_style!(stream, self, "' requires a value but none was supplied\n");
313            }
314        }
315        match possible {
316            Some(values) if !values.is_empty() => {
317                let error_indent = cross_prefix!("error: ").len();
318                write_style!(
319                    stream,
320                    self,
321                    format_args!("{:error_indent$}[possible values: ", "")
322                );
323                let max_index = values.len() - 1;
324                for (index, value) in values.iter().enumerate() {
325                    write_style!(stream, self, value, green);
326                    if index < max_index {
327                        write_style!(stream, self, ", ");
328                    }
329                }
330                write_style!(stream, self, "]\n");
331            }
332            _ => (),
333        }
334        write_style!(stream, self, "Usage:\n");
335        write_style!(
336            stream,
337            self,
338            "    cross [+toolchain] [OPTIONS] [SUBCOMMAND]\n"
339        );
340        write_style!(stream, self, "\n");
341        write_style!(stream, self, "For more information try ");
342        write_style!(stream, self, "--help", green);
343        write_style!(stream, self, "\n");
344
345        stream.flush()?;
346
347        Ok(())
348    }
349}
350
351impl Default for MessageInfo {
352    fn default() -> MessageInfo {
353        MessageInfo::new(ColorChoice::Auto, Verbosity::Normal)
354    }
355}
356
357impl From<ColorChoice> for MessageInfo {
358    fn from(color_choice: ColorChoice) -> MessageInfo {
359        MessageInfo::new(color_choice, Verbosity::Normal)
360    }
361}
362
363impl From<Verbosity> for MessageInfo {
364    fn from(verbosity: Verbosity) -> MessageInfo {
365        MessageInfo::new(ColorChoice::Auto, verbosity)
366    }
367}
368
369impl From<(ColorChoice, Verbosity)> for MessageInfo {
370    fn from(info: (ColorChoice, Verbosity)) -> MessageInfo {
371        MessageInfo::new(info.0, info.1)
372    }
373}
374
375// cargo only accepts literal booleans for some values.
376pub fn cargo_envvar_bool(var: &str) -> Result<bool> {
377    match env::var(var).ok() {
378        Some(value) => value.parse::<bool>().map_err(|_ignore| {
379            eyre::eyre!("environment variable for `{var}` was not `true` or `false`.")
380        }),
381        None => Ok(false),
382    }
383}
384
385pub fn invalid_color(provided: Option<&str>) -> ! {
386    let possible = ["auto", "always", "never"];
387    MessageInfo::default().fatal_usage("--color <WHEN>", provided, Some(&possible), 1);
388}
389
390fn get_color_choice(color: Option<&str>) -> Result<ColorChoice> {
391    Ok(match color {
392        Some(arg) => arg.parse().unwrap_or_else(|_| invalid_color(color)),
393        None => match env::var("CARGO_TERM_COLOR").ok().as_deref() {
394            Some(arg) => arg.parse().unwrap_or_else(|_| invalid_color(color)),
395            None => ColorChoice::Auto,
396        },
397    })
398}
399
400fn get_verbosity(color_choice: ColorChoice, verbose: bool, quiet: bool) -> Result<Verbosity> {
401    // cargo always checks the value of these variables.
402    let env_verbose = cargo_envvar_bool("CARGO_TERM_VERBOSE")?;
403    let env_quiet = cargo_envvar_bool("CARGO_TERM_QUIET")?;
404    Ok(match Verbosity::create(color_choice, verbose, quiet) {
405        Some(v) => v,
406        None => Verbosity::create(color_choice, env_verbose, env_quiet).unwrap_or_default(),
407    })
408}
409
410pub trait Stream {
411    const TTY: atty::Stream;
412    const OWO: owo_colors::Stream;
413
414    #[must_use]
415    fn is_atty() -> bool {
416        atty::is(Self::TTY)
417    }
418
419    fn owo(&self) -> owo_colors::Stream {
420        Self::OWO
421    }
422}
423
424impl Stream for io::Stdin {
425    const TTY: atty::Stream = atty::Stream::Stdin;
426    const OWO: owo_colors::Stream = owo_colors::Stream::Stdin;
427}
428
429impl Stream for io::Stdout {
430    const TTY: atty::Stream = atty::Stream::Stdout;
431    const OWO: owo_colors::Stream = owo_colors::Stream::Stdout;
432}
433
434impl Stream for io::Stderr {
435    const TTY: atty::Stream = atty::Stream::Stderr;
436    const OWO: owo_colors::Stream = owo_colors::Stream::Stderr;
437}
438
439pub fn default_ident() -> usize {
440    cross_prefix!("").len()
441}
442
443#[must_use]
444pub fn indent(message: &str, spaces: usize) -> String {
445    message
446        .lines()
447        .map(|s| format!("{:spaces$}{s}", ""))
448        .collect()
449}