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}