1use std::io::{self, IsTerminal};
2use std::sync::LazyLock;
3use std::time::Duration;
4
5use anyhow::{Result, bail};
6use dialoguer::Confirm;
7use dialoguer::theme::ColorfulTheme;
8use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
9use serde::{Deserialize, Serialize};
10
11pub use crate::progress::{
12 CancellationMode, ColorMode, LiveGroup, LiveRow, ProgressMode, Task, TaskKind, TaskOptions,
13 TaskVisibility, Ui, UiOptions, ValidatedUiOptions, format_duration,
14};
15pub use crate::{Cancellation, Cancelled};
16
17const ANSI_BLUE: &str = "\x1b[34m";
18const ANSI_BOLD_CYAN: &str = "\x1b[1;36m";
19const ANSI_BOLD_GREEN: &str = "\x1b[1;32m";
20const ANSI_BOLD_RED: &str = "\x1b[1;31m";
21const ANSI_BOLD_WHITE_RED_BG: &str = "\x1b[1;37;41m";
22const ANSI_BOLD_YELLOW: &str = "\x1b[1;33m";
23const ANSI_CYAN: &str = "\x1b[36m";
24const ANSI_GREEN: &str = "\x1b[32m";
25const ANSI_RED: &str = "\x1b[31m";
26const ANSI_RESET: &str = "\x1b[0m";
27const ANSI_STRIKETHROUGH: &str = "\x1b[9m";
28const ANSI_YELLOW: &str = "\x1b[33m";
29const SPINNER_TICKS: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32pub enum Color {
33 Blue,
34 Cyan,
35 Green,
36 Red,
37 Yellow,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
41pub enum TextEffect {
42 #[default]
43 None,
44 Strikethrough,
45}
46
47pub trait RenderTarget {
48 fn style(&self, text: &str, color: Option<Color>, effect: TextEffect) -> String;
49
50 fn paint(&self, text: &str, color: Color) -> String {
51 self.style(text, Some(color), TextEffect::None)
52 }
53
54 fn effect(&self, text: &str, effect: TextEffect) -> String {
55 self.style(text, None, effect)
56 }
57
58 fn hyperlink(&self, text: &str, url: Option<&str>) -> String {
59 let _ = url;
60 text.to_owned()
61 }
62}
63
64#[derive(Debug, Clone, Copy, Default)]
65pub struct StdoutRenderTarget;
66
67#[derive(Debug, Clone, Copy, Default)]
68pub struct StderrRenderTarget;
69
70#[derive(Debug, Clone, Copy)]
71pub struct ConfiguredRenderTarget {
72 color: bool,
73}
74
75pub fn stdin_is_interactive() -> bool {
76 io::stdin().is_terminal()
77}
78
79pub fn stdout_is_interactive() -> bool {
80 io::stdout().is_terminal()
81}
82
83pub fn stderr_is_interactive() -> bool {
84 io::stderr().is_terminal()
85}
86
87pub fn require_interactive(message: &str) -> Result<()> {
88 if stdin_is_interactive() {
89 Ok(())
90 } else {
91 bail!("{message}")
92 }
93}
94
95pub fn prompt_confirm(prompt: &str, default: bool) -> Result<bool> {
96 prompt_confirm_with_message(prompt, default, "Interactive confirmation required.")
97}
98
99pub fn prompt_confirm_with_message(prompt: &str, default: bool, message: &str) -> Result<bool> {
100 require_interactive(message)?;
101 Confirm::with_theme(prompt_theme())
102 .with_prompt(prompt)
103 .default(default)
104 .interact()
105 .map_err(Into::into)
106}
107
108pub fn prompt_theme() -> &'static ColorfulTheme {
109 static THEME: LazyLock<ColorfulTheme> = LazyLock::new(ColorfulTheme::default);
110 &THEME
111}
112
113pub fn stdout_render_target() -> StdoutRenderTarget {
114 StdoutRenderTarget
115}
116
117pub fn stderr_render_target() -> StderrRenderTarget {
118 StderrRenderTarget
119}
120
121impl Ui {
122 pub fn render_target(&self) -> ConfiguredRenderTarget {
124 ConfiguredRenderTarget {
125 color: self.color_is_enabled(),
126 }
127 }
128
129 pub fn stdout_render_target(&self) -> ConfiguredRenderTarget {
132 ConfiguredRenderTarget {
133 color: self.stdout_color_is_enabled(),
134 }
135 }
136}
137
138pub fn spinner_style(template: &str) -> ProgressStyle {
139 ProgressStyle::with_template(template)
140 .unwrap_or_else(|_| ProgressStyle::default_spinner())
141 .tick_strings(SPINNER_TICKS)
142}
143
144pub fn plain_message_style() -> ProgressStyle {
145 ProgressStyle::with_template("{msg}").unwrap_or_else(|_| ProgressStyle::default_spinner())
146}
147
148pub fn spinner(message: &str) -> ProgressBar {
149 let progress = ProgressBar::with_draw_target(None, ProgressDrawTarget::stderr());
150 progress.set_style(spinner_style("{spinner:.cyan} {msg}"));
151 progress.enable_steady_tick(Duration::from_millis(90));
152 progress.set_message(message.to_owned());
153 progress
154}
155
156pub fn new_list_row_spinner() -> ProgressBar {
157 let row = ProgressBar::new_spinner();
158 row.set_style(spinner_style("{spinner:.cyan} {msg}"));
159 row
160}
161
162pub fn new_list_spacer() -> ProgressBar {
163 let spacer = ProgressBar::new_spinner();
164 spacer.set_style(plain_message_style());
165 spacer
166}
167
168pub fn activate_list_row_spinner(row: &ProgressBar) {
169 row.enable_steady_tick(Duration::from_millis(90));
170}
171
172pub fn finish_list_spacer(spacer: &ProgressBar) {
173 spacer.finish_with_message(" ".to_owned());
174}
175
176pub fn finish_list_row(row: &ProgressBar, message: &str) {
177 row.set_style(plain_message_style());
178 row.finish_with_message(message.to_owned());
179}
180
181pub fn clear_list_spacer(progress: &MultiProgress, spacer: &ProgressBar) {
182 progress.remove(spacer);
183}
184
185pub fn clear_progress_bar(progress: &MultiProgress, progress_bar: &ProgressBar) {
186 progress_bar.finish_and_clear();
187 progress.remove(progress_bar);
188}
189
190pub fn maybe_open_browser(url: &str) {
191 if let Err(err) = webbrowser::open(url) {
192 warn(&format!(
193 "Could not open a browser automatically: {err}. Open this URL manually: {url}"
194 ));
195 }
196}
197
198pub fn print_big_red_error(message: &str) {
199 if stderr_is_interactive() {
200 eprintln!(
201 "{} ERROR {} {}{}{}",
202 ANSI_BOLD_WHITE_RED_BG, ANSI_RESET, ANSI_BOLD_RED, message, ANSI_RESET
203 );
204 } else {
205 eprintln!("ERROR: {message}");
206 }
207}
208
209pub fn print_notice(message: &str) {
210 if stderr_is_interactive() {
211 let prefix = StderrRenderTarget.paint("INFO", Color::Cyan);
212 eprintln!("{prefix} {message}");
213 } else {
214 eprintln!("INFO: {message}");
215 }
216}
217
218pub fn print_warning(message: &str) {
219 warn(message);
220}
221
222pub fn print_error(message: &str) {
223 if stderr_is_interactive() {
224 eprintln!("{ANSI_BOLD_RED}error:{ANSI_RESET} {message}");
225 } else {
226 eprintln!("error: {message}");
227 }
228}
229
230pub fn print_stage(message: &str) {
231 stage(message);
232}
233
234pub fn stage(message: &str) {
235 if stderr_is_interactive() {
236 eprintln!("{ANSI_BOLD_CYAN}==>{ANSI_RESET} {message}");
237 } else {
238 eprintln!("==> {message}");
239 }
240}
241
242pub fn detail(message: &str) {
243 eprintln!(" {message}");
244}
245
246pub fn success(message: &str) {
247 if stderr_is_interactive() {
248 eprintln!("{ANSI_BOLD_GREEN}ok:{ANSI_RESET} {message}");
249 } else {
250 eprintln!("ok: {message}");
251 }
252}
253
254pub fn warn(message: &str) {
255 if stderr_is_interactive() {
256 eprintln!("{ANSI_BOLD_YELLOW}warning:{ANSI_RESET} {message}");
257 } else {
258 eprintln!("warning: {message}");
259 }
260}
261
262impl RenderTarget for StdoutRenderTarget {
263 fn style(&self, text: &str, color: Option<Color>, effect: TextEffect) -> String {
264 style_for_terminal(text, color, effect, stdout_is_interactive())
265 }
266
267 fn hyperlink(&self, text: &str, url: Option<&str>) -> String {
268 if !stdout_is_interactive() {
269 return text.to_owned();
270 }
271 let Some(url) = url else {
272 return text.to_owned();
273 };
274 format!("\x1b]8;;{url}\x1b\\{text}\x1b]8;;\x1b\\")
275 }
276}
277
278impl RenderTarget for StderrRenderTarget {
279 fn style(&self, text: &str, color: Option<Color>, effect: TextEffect) -> String {
280 style_for_terminal(text, color, effect, stderr_is_interactive())
281 }
282}
283
284impl RenderTarget for ConfiguredRenderTarget {
285 fn style(&self, text: &str, color: Option<Color>, effect: TextEffect) -> String {
286 style_for_terminal(text, color, effect, self.color)
287 }
288}
289
290fn ansi_code(color: Color) -> &'static str {
291 match color {
292 Color::Blue => ANSI_BLUE,
293 Color::Cyan => ANSI_CYAN,
294 Color::Green => ANSI_GREEN,
295 Color::Red => ANSI_RED,
296 Color::Yellow => ANSI_YELLOW,
297 }
298}
299
300fn style_for_terminal(
301 text: &str,
302 color: Option<Color>,
303 effect: TextEffect,
304 is_interactive: bool,
305) -> String {
306 if !is_interactive || (color.is_none() && effect == TextEffect::None) {
307 return text.to_owned();
308 }
309 let mut prefix = String::new();
310 if effect == TextEffect::Strikethrough {
311 prefix.push_str(ANSI_STRIKETHROUGH);
312 }
313 if let Some(color) = color {
314 prefix.push_str(ansi_code(color));
315 }
316 format!("{prefix}{text}{ANSI_RESET}")
317}