Skip to main content

xbp_cli/cli/
ui.rs

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    pub fn log(&self, message: &str) {
59        self.pb.println(message.to_string());
60    }
61}
62
63pub async fn with_loader<T, E, F>(label: &str, op: F) -> Result<T, E>
64where
65    E: std::fmt::Display,
66    F: Future<Output = Result<T, E>>,
67{
68    let loader = Loader::start(label);
69    let result = op.await;
70    match &result {
71        Ok(_) => loader.success(),
72        Err(err) => loader.fail(&err.to_string()),
73    }
74    result
75}
76
77pub fn print_cli_header(command: &str, debug: bool) {
78    let mode = if debug {
79        "DEBUG".bright_yellow().bold().to_string()
80    } else {
81        "NORMAL".bright_blue().bold().to_string()
82    };
83    println!(
84        "{} {} {} {}",
85        "xbp".bright_magenta().bold(),
86        "→".bright_black(),
87        command.bright_white().bold(),
88        format!("[{}]", mode).bright_black()
89    );
90}
91
92pub fn configure_color_output() {
93    // Respect common color control environment variables first.
94    let disable_via_clicolor = std::env::var("CLICOLOR")
95        .map(|value| value == "0")
96        .unwrap_or(false);
97    if std::env::var_os("NO_COLOR").is_some() || disable_via_clicolor {
98        colored::control::set_override(false);
99        return;
100    }
101    if std::env::var_os("FORCE_COLOR").is_some() || std::env::var_os("CLICOLOR_FORCE").is_some() {
102        colored::control::set_override(true);
103        return;
104    }
105
106    let stdout_color = supports_color::on(Stream::Stdout).is_some();
107    let stderr_color = supports_color::on(Stream::Stderr).is_some();
108    colored::control::set_override(stdout_color || stderr_color);
109}
110
111pub fn section(title: &str) {
112    println!(
113        "\n{} {}",
114        "◆".bright_blue().bold(),
115        title.bright_blue().bold()
116    );
117}
118
119pub fn divider(width: usize) {
120    println!("{}", "─".repeat(width).bright_black());
121}
122
123pub fn status_line(label: &str, status: &str, ok: bool) {
124    let icon = if ok {
125        "✓".bright_green().bold()
126    } else {
127        "✗".bright_red().bold()
128    };
129    let status = if ok {
130        status.bright_green().to_string()
131    } else {
132        status.bright_red().to_string()
133    };
134    println!("  {} {} {}", icon, label.bright_white(), status);
135}
136
137pub fn tip(message: &str) {
138    println!("{} {}", "Hint:".bright_yellow().bold(), message);
139}
140
141fn select_spinner_set(label: &str) -> &'static [&'static str] {
142    let hash = label
143        .bytes()
144        .fold(0_u64, |acc, b| acc.wrapping_mul(16777619) ^ u64::from(b));
145    let idx = (hash as usize) % SPINNER_SETS.len();
146    SPINNER_SETS[idx]
147}