scarb_ui/
lib.rs

1//! Terminal user interface primitives used by [Scarb] and its extensions.
2//!
3//! This crate focuses mainly on two areas:
4//!
5//! 1. [`Ui`] and [`components`]: Serving a unified interface for communication with the user,
6//!    either via:
7//!     - rendering human-readable messages or interactive widgets,
8//!     - or printing machine-parseable JSON-NL messages, depending on runtime configuration.
9//! 2. [`args`]: Providing reusable [`clap`] arguments for common tasks.
10//!
11//! There are also re-export from various TUI crates recommended for use in Scarb ecosystem,
12//! such as [`indicatif`] or [`console`].
13//!
14//! [scarb]: https://docs.swmansion.com/scarb
15
16#![deny(clippy::dbg_macro)]
17#![deny(clippy::disallowed_methods)]
18#![deny(missing_docs)]
19#![deny(rustdoc::broken_intra_doc_links)]
20#![deny(rustdoc::missing_crate_level_docs)]
21#![deny(rustdoc::private_intra_doc_links)]
22#![warn(rust_2018_idioms)]
23
24use clap::ValueEnum;
25use indicatif::WeakProgressBar;
26pub use indicatif::{
27    BinaryBytes, DecimalBytes, FormattedDuration, HumanBytes, HumanCount, HumanDuration,
28    HumanFloatCount,
29};
30use std::fmt::Debug;
31use std::sync::{Arc, RwLock};
32
33pub use message::*;
34pub use verbosity::*;
35pub use widget::*;
36
37use crate::components::TypedMessage;
38
39pub mod args;
40pub mod components;
41mod message;
42mod verbosity;
43mod widget;
44
45/// The requested format of output (either textual or JSON).
46#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, ValueEnum)]
47pub enum OutputFormat {
48    /// Render human-readable messages and interactive widgets.
49    #[default]
50    Text,
51    /// Render machine-parseable JSON-NL messages.
52    Json,
53}
54
55/// An abstraction around console output which stores preferences for output format (human vs JSON),
56/// colour, etc.
57///
58/// All human-oriented messaging (basically all writes to `stdout`) must go through this object.
59#[derive(Clone)]
60pub struct Ui {
61    verbosity: Verbosity,
62    output_format: OutputFormat,
63    state: Arc<RwLock<State>>,
64}
65
66impl Debug for Ui {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        f.debug_struct("Ui")
69            .field("verbosity", &self.verbosity)
70            .field("output_format", &self.output_format)
71            .finish()
72    }
73}
74
75/// An encapsulation of the UI state.
76///
77/// This can be used by `Ui` to store stateful information.
78#[derive(Default)]
79#[non_exhaustive]
80struct State {
81    active_spinner: WeakProgressBar,
82}
83
84impl Ui {
85    /// Create a new [`Ui`] instance configured with the given verbosity and output format.
86    pub fn new(verbosity: Verbosity, output_format: OutputFormat) -> Self {
87        Self {
88            verbosity,
89            output_format,
90            state: Default::default(),
91        }
92    }
93
94    /// Get the verbosity level of this [`Ui`] instance.
95    pub fn verbosity(&self) -> Verbosity {
96        self.verbosity
97    }
98
99    /// Get the output format of this [`Ui`] instance.
100    pub fn output_format(&self) -> OutputFormat {
101        self.output_format
102    }
103
104    /// Print the message to standard output if not in quiet verbosity mode.
105    pub fn print<T: Message>(&self, message: T) {
106        if self.verbosity > Verbosity::Quiet {
107            self.do_print(message);
108        }
109    }
110
111    /// Print the message to standard output regardless of the verbosity mode.
112    pub fn force_print<T: Message>(&self, message: T) {
113        self.do_print(message);
114    }
115
116    /// Print the message to the standard output only in verbose mode.
117    pub fn verbose<T: Message>(&self, message: T) {
118        if self.verbosity >= Verbosity::Verbose {
119            self.do_print(message);
120        }
121    }
122
123    /// Display an interactive widget and return a handle for further interaction.
124    ///
125    /// The widget will be only displayed if not in quiet mode, and if the output format is text.
126    pub fn widget<T: Widget>(&self, widget: T) -> Option<T::Handle> {
127        if self.output_format == OutputFormat::Text && self.verbosity >= Verbosity::Normal {
128            let handle = widget.text();
129            if let Some(handle) = handle.weak_progress_bar() {
130                self.state
131                    .write()
132                    .expect("cannot lock ui state for writing")
133                    .active_spinner = handle;
134            }
135            Some(handle)
136        } else {
137            None
138        }
139    }
140
141    /// Print a warning to the user.
142    pub fn warn(&self, message: impl AsRef<str>) {
143        if self.verbosity > Verbosity::NoWarnings {
144            self.print(TypedMessage::styled("warn", "yellow", message.as_ref()))
145        }
146    }
147
148    /// Print an error to the user.
149    pub fn error(&self, message: impl AsRef<str>) {
150        self.print(TypedMessage::styled("error", "red", message.as_ref()))
151    }
152
153    /// Print a warning to the user.
154    pub fn warn_with_code(&self, code: impl AsRef<str>, message: impl AsRef<str>) {
155        if self.verbosity > Verbosity::NoWarnings {
156            self.print(
157                TypedMessage::styled("warn", "yellow", message.as_ref()).with_code(code.as_ref()),
158            )
159        }
160    }
161
162    /// Print an error to the user.
163    pub fn error_with_code(&self, code: impl AsRef<str>, message: impl AsRef<str>) {
164        self.print(TypedMessage::styled("error", "red", message.as_ref()).with_code(code.as_ref()))
165    }
166
167    /// Nicely format an [`anyhow::Error`] for display to the user, and print it with [`Ui::error`].
168    pub fn anyhow(&self, error: &anyhow::Error) {
169        // NOTE: Some errors, particularly ones from `toml_edit` like to add trailing newlines.
170        //   This isn't a big problem for users, but it's causing issues in tests, where trailing
171        //   whitespace collides with `indoc`.
172        self.error(format!("{error:?}").trim())
173    }
174
175    /// Nicely format an [`anyhow::Error`] for display to the user, and print it with [`Ui::warn`].
176    pub fn warn_anyhow(&self, error: &anyhow::Error) {
177        // NOTE: Some errors, particularly ones from `toml_edit` like to add trailing newlines.
178        //   This isn't a big problem for users, but it's causing issues in tests, where trailing
179        //   whitespace collides with `indoc`.
180        self.warn(format!("{error:?}").trim())
181    }
182
183    fn do_print<T: Message>(&self, message: T) {
184        let print = || match self.output_format {
185            OutputFormat::Text => message.print_text(),
186            OutputFormat::Json => message.print_json(),
187        };
188        let handle = self
189            .state
190            .read()
191            .expect("cannot lock ui state for reading")
192            .active_spinner
193            .clone();
194        if let Some(pb) = handle.upgrade() {
195            pb.suspend(print);
196        } else {
197            print();
198        }
199    }
200
201    /// Forces colorization on or off for stdout.
202    ///
203    /// This overrides the default for the current process and changes the return value of
204    /// the [`Ui::has_colors_enabled`] function.
205    pub fn force_colors_enabled(&self, enable: bool) {
206        console::set_colors_enabled(enable);
207    }
208
209    /// Returns `true` if colors should be enabled for stdout.
210    ///
211    /// This honors the [clicolors spec](http://bixense.com/clicolors/).
212    ///
213    /// * `CLICOLOR != 0`: ANSI colors are supported and should be used when the program isn't piped.
214    /// * `CLICOLOR == 0`: Don't output ANSI color escape codes.
215    /// * `CLICOLOR_FORCE != 0`: ANSI colors should be enabled no matter what.
216    pub fn has_colors_enabled(&self) -> bool {
217        console::colors_enabled()
218    }
219
220    /// Forces colorization on or off for stdout.
221    ///
222    /// This overrides the default for the current process and changes the return value of
223    /// the [`Ui::has_colors_enabled`] function.
224    pub fn force_colors_enabled_stderr(&self, enable: bool) {
225        console::set_colors_enabled_stderr(enable);
226    }
227
228    /// Returns `true` if colors should be enabled for stderr.
229    ///
230    /// This honors the [clicolors spec](http://bixense.com/clicolors/).
231    ///
232    /// * `CLICOLOR != 0`: ANSI colors are supported and should be used when the program isn't piped.
233    /// * `CLICOLOR == 0`: Don't output ANSI color escape codes.
234    /// * `CLICOLOR_FORCE != 0`: ANSI colors should be enabled no matter what.
235    pub fn has_colors_enabled_stderr(&self) -> bool {
236        console::colors_enabled_stderr()
237    }
238}