cargo_component_core/
terminal.rs

1//! Module for the terminal output implementation.
2//!
3//! This implementation is heavily inspired by `Shell` from `cargo`.
4
5use anyhow::{bail, Result};
6use owo_colors::{AnsiColors, OwoColorize};
7use std::{
8    cell::RefCell,
9    fmt,
10    io::{stderr, stdout, IsTerminal, Write},
11    str::FromStr,
12};
13
14pub use owo_colors::AnsiColors as Colors;
15
16/// The supported color options of `cargo`.
17#[derive(Default, Copy, Clone, Debug, Eq, PartialEq, Hash)]
18pub enum Color {
19    /// Automatically provide colorized output based on whether
20    /// the output is a terminal.
21    #[default]
22    Auto,
23    /// Never provide colorized output.
24    Never,
25    /// Always provide colorized output.
26    Always,
27}
28
29impl FromStr for Color {
30    type Err = anyhow::Error;
31
32    fn from_str(value: &str) -> Result<Self> {
33        match value {
34            "auto" => Ok(Self::Auto),
35            "never" => Ok(Self::Never),
36            "always" => Ok(Self::Always),
37            _ => bail!("argument for --color must be auto, always, or never, but found `{value}`"),
38        }
39    }
40}
41
42impl fmt::Display for Color {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        match self {
45            Self::Auto => write!(f, "auto"),
46            Self::Never => write!(f, "never"),
47            Self::Always => write!(f, "always"),
48        }
49    }
50}
51
52/// The requested verbosity of output.
53#[derive(Debug, Clone, Copy, PartialEq)]
54pub enum Verbosity {
55    /// Verbose output.
56    Verbose,
57    /// Normal output.
58    Normal,
59    /// Quiet (no) output.
60    Quiet,
61}
62
63pub(crate) struct TerminalState {
64    pub(crate) output: Output,
65    verbosity: Verbosity,
66    pub(crate) needs_clear: bool,
67}
68
69impl TerminalState {
70    /// Clears the current stderr line if needed.
71    pub(crate) fn clear_stderr(&mut self) {
72        if self.needs_clear {
73            if self.output.supports_color() {
74                imp::stderr_erase_line();
75            }
76
77            self.needs_clear = false;
78        }
79    }
80}
81
82impl fmt::Debug for TerminalState {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        match &self.output {
85            Output::Write(_) => f
86                .debug_struct("Terminal")
87                .field("verbosity", &self.verbosity)
88                .finish(),
89            Output::Stream { color, .. } => f
90                .debug_struct("Terminal")
91                .field("verbosity", &self.verbosity)
92                .field("color", color)
93                .finish(),
94        }
95    }
96}
97
98/// An abstraction around output that considers preferences for verbosity and color.
99///
100/// This is based off of the `cargo` implementation.
101#[derive(Debug)]
102pub struct Terminal(RefCell<TerminalState>);
103
104impl Terminal {
105    /// Creates a new terminal with the given verbosity and color.
106    pub fn new(verbosity: Verbosity, color: Color) -> Self {
107        Self(RefCell::new(TerminalState {
108            output: Output::Stream {
109                is_terminal: stderr().is_terminal(),
110                color,
111            },
112            verbosity,
113            needs_clear: false,
114        }))
115    }
116
117    /// Creates a terminal from a plain writable object, with no color, and max verbosity.
118    pub fn from_write(out: Box<dyn Write>) -> Self {
119        Self(RefCell::new(TerminalState {
120            output: Output::Write(out),
121            verbosity: Verbosity::Verbose,
122            needs_clear: false,
123        }))
124    }
125
126    /// Prints a green 'status' message.
127    pub fn status<T, U>(&self, status: T, message: U) -> Result<()>
128    where
129        T: fmt::Display,
130        U: fmt::Display,
131    {
132        let status_green = status.green();
133
134        let status = if self.0.borrow().output.supports_color() {
135            &status_green as &dyn fmt::Display
136        } else {
137            &status
138        };
139
140        self.print(status, Some(&message), true)
141    }
142
143    /// Prints a 'status' message with the specified color.
144    pub fn status_with_color<T, U>(
145        &self,
146        status: T,
147        message: U,
148        color: owo_colors::AnsiColors,
149    ) -> Result<()>
150    where
151        T: fmt::Display,
152        U: fmt::Display,
153    {
154        let status_color = status.color(color);
155
156        let status = if self.0.borrow().output.supports_color() {
157            &status_color as &dyn fmt::Display
158        } else {
159            &status
160        };
161
162        self.print(status, Some(&message), true)
163    }
164
165    /// Prints a cyan 'note' message.
166    pub fn note<T: fmt::Display>(&self, message: T) -> Result<()> {
167        let status = "note";
168        let status_cyan = status.cyan();
169
170        let status = if self.0.borrow().output.supports_color() {
171            &status_cyan as &dyn fmt::Display
172        } else {
173            &status
174        };
175
176        self.print(status, Some(&message), false)
177    }
178
179    /// Prints a yellow 'warning' message.
180    pub fn warn<T: fmt::Display>(&self, message: T) -> Result<()> {
181        let status = "warning";
182        let status_yellow = status.yellow();
183
184        let status = if self.0.borrow().output.supports_color() {
185            &status_yellow as &dyn fmt::Display
186        } else {
187            &status
188        };
189
190        self.print(status, Some(&message), false)
191    }
192
193    /// Prints a red 'error' message.
194    pub fn error<T: fmt::Display>(&self, message: T) -> Result<()> {
195        let status = "error";
196        let status_red = status.red();
197
198        let status = if self.0.borrow().output.supports_color() {
199            &status_red as &dyn fmt::Display
200        } else {
201            &status
202        };
203
204        // This doesn't call print as errors are always printed even when quiet
205        let mut state = self.0.borrow_mut();
206        state.clear_stderr();
207        state.output.print(status, Some(&message), false)
208    }
209
210    /// Write a styled fragment to stdout.
211    ///
212    /// Caller is responsible for deciding whether [`Shell::verbosity`] is affects output.
213    pub fn write_stdout(
214        &self,
215        fragment: impl fmt::Display,
216        color: Option<AnsiColors>,
217    ) -> Result<()> {
218        self.0.borrow_mut().output.write_stdout(fragment, color)
219    }
220
221    /// Prints a status that can be justified followed by a message.
222    fn print(
223        &self,
224        status: &dyn fmt::Display,
225        message: Option<&dyn fmt::Display>,
226        justified: bool,
227    ) -> Result<()> {
228        let mut state = self.0.borrow_mut();
229        match state.verbosity {
230            Verbosity::Quiet => Ok(()),
231            _ => {
232                state.clear_stderr();
233                state.output.print(status, message, justified)
234            }
235        }
236    }
237
238    /// Returns the width of the terminal in spaces, if any.
239    pub fn width(&self) -> Option<usize> {
240        match &self.0.borrow().output {
241            Output::Stream { .. } => imp::stderr_width(),
242            _ => None,
243        }
244    }
245
246    /// Returns the verbosity of the terminal.
247    pub fn verbosity(&self) -> Verbosity {
248        self.0.borrow().verbosity
249    }
250
251    pub(crate) fn state_mut(&self) -> std::cell::RefMut<'_, TerminalState> {
252        self.0.borrow_mut()
253    }
254}
255
256/// A `Write`able object, either with or without color support.
257pub(crate) enum Output {
258    /// A plain write object without color support.
259    Write(Box<dyn Write>),
260    /// Color-enabled stdio, with information on whether color should be used
261    Stream { is_terminal: bool, color: Color },
262}
263
264impl Output {
265    pub(crate) fn supports_color(&self) -> bool {
266        match self {
267            Output::Write(_) => false,
268            Output::Stream { is_terminal, color } => match color {
269                Color::Auto => *is_terminal,
270                Color::Never => false,
271                Color::Always => true,
272            },
273        }
274    }
275
276    /// Prints out a message with a bold, optionally-justified status.
277    pub(crate) fn print(
278        &mut self,
279        status: &dyn fmt::Display,
280        message: Option<&dyn fmt::Display>,
281        justified: bool,
282    ) -> Result<()> {
283        match *self {
284            Output::Stream { .. } => {
285                let stderr = &mut stderr();
286                let status_bold = status.bold();
287
288                let status = if self.supports_color() {
289                    &status_bold as &dyn fmt::Display
290                } else {
291                    &status
292                };
293
294                if justified {
295                    write!(stderr, "{status:>12}")?;
296                } else {
297                    write!(stderr, "{status}:")?;
298                }
299
300                match message {
301                    Some(message) => writeln!(stderr, " {}", message)?,
302                    None => write!(stderr, " ")?,
303                }
304            }
305            Output::Write(ref mut w) => {
306                if justified {
307                    write!(w, "{status:>12}")?;
308                } else {
309                    write!(w, "{status}:")?;
310                }
311                match message {
312                    Some(message) => writeln!(w, " {}", message)?,
313                    None => write!(w, " ")?,
314                }
315            }
316        }
317
318        Ok(())
319    }
320
321    fn write_stdout(
322        &mut self,
323        fragment: impl fmt::Display,
324        color: Option<AnsiColors>,
325    ) -> Result<()> {
326        match *self {
327            Self::Stream { .. } => {
328                let mut stdout = stdout();
329
330                match color {
331                    Some(color) => {
332                        let colored_fragment = fragment.color(color);
333                        let fragment: &dyn fmt::Display = if self.supports_color() {
334                            &colored_fragment as &dyn fmt::Display
335                        } else {
336                            &fragment
337                        };
338
339                        write!(stdout, "{fragment}")?;
340                    }
341                    None => write!(stdout, "{fragment}")?,
342                }
343            }
344            Self::Write(ref mut w) => {
345                write!(w, "{fragment}")?;
346            }
347        }
348        Ok(())
349    }
350}
351
352#[cfg(unix)]
353mod imp {
354    use std::mem;
355
356    pub fn stderr_width() -> Option<usize> {
357        unsafe {
358            let mut winsize: libc::winsize = mem::zeroed();
359            // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
360            // as c_uint but ioctl wants c_ulong.
361            if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ, &mut winsize) < 0 {
362                return None;
363            }
364            if winsize.ws_col > 0 {
365                Some(winsize.ws_col as usize)
366            } else {
367                None
368            }
369        }
370    }
371
372    pub fn stderr_erase_line() {
373        // This is the "EL - Erase in Line" sequence. It clears from the cursor
374        // to the end of line.
375        // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_sequences
376        eprint!("\x1B[K");
377    }
378}
379
380#[cfg(windows)]
381mod imp {
382    use std::{cmp, mem, ptr};
383    use windows_sys::core::PCSTR;
384    use windows_sys::Win32::Foundation::CloseHandle;
385    use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
386    use windows_sys::Win32::Foundation::{GENERIC_READ, GENERIC_WRITE};
387    use windows_sys::Win32::Storage::FileSystem::{
388        CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
389    };
390    use windows_sys::Win32::System::Console::{
391        GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO, STD_ERROR_HANDLE,
392    };
393
394    pub fn stderr_width() -> Option<usize> {
395        unsafe {
396            let stdout = GetStdHandle(STD_ERROR_HANDLE);
397            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
398            if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
399                return Some((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
400            }
401
402            // On mintty/msys/cygwin based terminals, the above fails with
403            // INVALID_HANDLE_VALUE. Use an alternate method which works
404            // in that case as well.
405            let h = CreateFileA(
406                "CONOUT$\0".as_ptr() as PCSTR,
407                GENERIC_READ | GENERIC_WRITE,
408                FILE_SHARE_READ | FILE_SHARE_WRITE,
409                ptr::null_mut(),
410                OPEN_EXISTING,
411                0,
412                0,
413            );
414
415            if h == INVALID_HANDLE_VALUE {
416                return None;
417            }
418
419            let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
420            let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
421            CloseHandle(h);
422            if rc != 0 {
423                let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
424                // Unfortunately cygwin/mintty does not set the size of the
425                // backing console to match the actual window size. This
426                // always reports a size of 80 or 120 (not sure what
427                // determines that). Use a conservative max of 60 which should
428                // work in most circumstances. ConEmu does some magic to
429                // resize the console correctly, but there's no reasonable way
430                // to detect which kind of terminal we are running in, or if
431                // GetConsoleScreenBufferInfo returns accurate information.
432                return Some(cmp::min(60, width));
433            }
434
435            None
436        }
437    }
438
439    pub fn stderr_erase_line() {
440        match stderr_width() {
441            Some(width) => {
442                let blank = " ".repeat(width);
443                eprint!("{}\r", blank);
444            }
445            _ => (),
446        }
447    }
448}