1use colored::Colorize;
2use indicatif::{ProgressBar, ProgressStyle};
3use std::future::Future;
4use std::time::Duration;
5use supports_color::Stream;
6
7const SPINNER_SETS: &[&[&str]] = &[
8 &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
9 &["◐", "◓", "◑", "◒"],
10 &["▖", "▘", "▝", "▗"],
11 &["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙●∙"],
12];
13
14pub struct Loader {
15 pb: ProgressBar,
16 label: String,
17}
18
19impl Loader {
20 pub fn start(label: &str) -> Self {
21 let spinner_frames = select_spinner_set(label);
22 let pb = ProgressBar::new_spinner();
23 let style = ProgressStyle::with_template("{spinner:.cyan} {msg}")
24 .unwrap_or_else(|_| ProgressStyle::default_spinner())
25 .tick_strings(spinner_frames);
26 pb.set_style(style);
27 pb.set_message(format!("{}", label.bright_cyan()));
28 pb.enable_steady_tick(Duration::from_millis(95));
29 Self {
30 pb,
31 label: label.to_string(),
32 }
33 }
34
35 pub fn success(&self) {
36 self.pb
37 .finish_with_message(format!("{} {}", "OK".bright_green().bold(), self.label));
38 }
39
40 pub fn success_with(&self, message: &str) {
41 self.pb
42 .finish_with_message(format!("{} {}", "OK".bright_green().bold(), message));
43 }
44
45 pub fn fail(&self, details: &str) {
46 self.pb.finish_with_message(format!(
47 "{} {} {}",
48 "ERR".bright_red().bold(),
49 self.label,
50 format!("({})", details).bright_black()
51 ));
52 }
53
54 pub fn update(&self, message: &str) {
55 self.pb.set_message(format!("{}", message.bright_cyan()));
56 }
57}
58
59pub async fn with_loader<T, E, F>(label: &str, op: F) -> Result<T, E>
60where
61 E: std::fmt::Display,
62 F: Future<Output = Result<T, E>>,
63{
64 let loader = Loader::start(label);
65 let result = op.await;
66 match &result {
67 Ok(_) => loader.success(),
68 Err(err) => loader.fail(&err.to_string()),
69 }
70 result
71}
72
73pub fn print_cli_header(command: &str, debug: bool) {
74 let mode = if debug {
75 "DEBUG".bright_yellow().bold().to_string()
76 } else {
77 "NORMAL".bright_blue().bold().to_string()
78 };
79 println!(
80 "{} {} {} {}",
81 "xbp".bright_magenta().bold(),
82 "→".bright_black(),
83 command.bright_white().bold(),
84 format!("[{}]", mode).bright_black()
85 );
86}
87
88pub fn configure_color_output() {
89 let disable_via_clicolor = std::env::var("CLICOLOR")
91 .map(|value| value == "0")
92 .unwrap_or(false);
93 if std::env::var_os("NO_COLOR").is_some() || disable_via_clicolor {
94 colored::control::set_override(false);
95 return;
96 }
97 if std::env::var_os("FORCE_COLOR").is_some() || std::env::var_os("CLICOLOR_FORCE").is_some() {
98 colored::control::set_override(true);
99 return;
100 }
101
102 let stdout_color = supports_color::on(Stream::Stdout).is_some();
103 let stderr_color = supports_color::on(Stream::Stderr).is_some();
104 colored::control::set_override(stdout_color || stderr_color);
105}
106
107pub fn section(title: &str) {
108 println!(
109 "\n{} {}",
110 "◆".bright_blue().bold(),
111 title.bright_blue().bold()
112 );
113}
114
115pub fn divider(width: usize) {
116 println!("{}", "─".repeat(width).bright_black());
117}
118
119pub fn status_line(label: &str, status: &str, ok: bool) {
120 let icon = if ok {
121 "✓".bright_green().bold()
122 } else {
123 "✗".bright_red().bold()
124 };
125 let status = if ok {
126 status.bright_green().to_string()
127 } else {
128 status.bright_red().to_string()
129 };
130 println!(" {} {} {}", icon, label.bright_white(), status);
131}
132
133pub fn tip(message: &str) {
134 println!("{} {}", "Hint:".bright_yellow().bold(), message);
135}
136
137fn select_spinner_set(label: &str) -> &'static [&'static str] {
138 let hash = label
139 .bytes()
140 .fold(0_u64, |acc, b| acc.wrapping_mul(16777619) ^ u64::from(b));
141 let idx = (hash as usize) % SPINNER_SETS.len();
142 SPINNER_SETS[idx]
143}