xbp 10.36.7

XBP is a zero-config build pack that can also interact with proxies, kafka, sockets, synthetic monitors.
Documentation
use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};
use std::future::Future;
use std::time::Duration;
use supports_color::Stream;

const SPINNER_SETS: &[&[&str]] = &[
    &["", "", "", "", "", "", "", "", "", ""],
    &["", "", "", ""],
    &["", "", "", ""],
    &["∙∙∙", "●∙∙", "∙●∙", "∙∙●", "∙●∙"],
];

pub struct Loader {
    pb: ProgressBar,
    label: String,
}

impl Loader {
    pub fn start(label: &str) -> Self {
        let spinner_frames = select_spinner_set(label);
        let pb = ProgressBar::new_spinner();
        let style = ProgressStyle::with_template("{spinner:.cyan} {msg}")
            .unwrap_or_else(|_| ProgressStyle::default_spinner())
            .tick_strings(spinner_frames);
        pb.set_style(style);
        pb.set_message(format!("{}", label.bright_cyan()));
        pb.enable_steady_tick(Duration::from_millis(95));
        Self {
            pb,
            label: label.to_string(),
        }
    }

    pub fn success(&self) {
        self.pb
            .finish_with_message(format!("{} {}", "OK".bright_green().bold(), self.label));
    }

    pub fn success_with(&self, message: &str) {
        self.pb
            .finish_with_message(format!("{} {}", "OK".bright_green().bold(), message));
    }

    pub fn fail(&self, details: &str) {
        self.pb.finish_with_message(format!(
            "{} {} {}",
            "ERR".bright_red().bold(),
            self.label,
            format!("({})", details).bright_black()
        ));
    }

    pub fn update(&self, message: &str) {
        self.pb.set_message(format!("{}", message.bright_cyan()));
    }

    pub fn log(&self, message: &str) {
        self.pb.println(message);
    }

    pub fn suspend<T, F>(&self, op: F) -> T
    where
        F: FnOnce() -> T,
    {
        self.pb.suspend(op)
    }

    /// Finish the spinner so a determinate bar can take over the next line.
    pub fn finish_for_handoff(&self) {
        self.pb.finish_and_clear();
    }
}

/// Left-to-right determinate progress bar (tqdm-style) for multi-item work
/// like secrets push / pull.
pub struct TaskBar {
    pb: ProgressBar,
    label: String,
}

impl TaskBar {
    pub fn start(label: &str, total: u64) -> Self {
        let pb = ProgressBar::new(total.max(1));
        let style = ProgressStyle::with_template(
            "{spinner:.cyan} {msg} [{bar:32.cyan/blue}] {pos}/{len} ({percent}%)",
        )
        .unwrap_or_else(|_| ProgressStyle::default_bar())
        .progress_chars("█▓░")
        .tick_strings(select_spinner_set(label));
        pb.set_style(style);
        pb.set_message(format!("{}", label.bright_cyan()));
        pb.enable_steady_tick(Duration::from_millis(95));
        if total == 0 {
            pb.set_position(0);
        }
        Self {
            pb,
            label: label.to_string(),
        }
    }

    pub fn set_position(&self, completed: u64) {
        self.pb.set_position(completed);
    }

    pub fn set_message(&self, message: &str) {
        self.pb.set_message(format!("{}", message.bright_cyan()));
    }

    pub fn tick(&self, completed: u64, message: &str) {
        self.set_message(message);
        self.set_position(completed);
    }

    pub fn success_with(&self, message: &str) {
        self.pb
            .finish_with_message(format!("{} {}", "OK".bright_green().bold(), message));
    }

    pub fn fail(&self, details: &str) {
        self.pb.finish_with_message(format!(
            "{} {} {}",
            "ERR".bright_red().bold(),
            self.label,
            format!("({})", details).bright_black()
        ));
    }
}

pub async fn with_loader<T, E, F>(label: &str, op: F) -> Result<T, E>
where
    E: std::fmt::Display,
    F: Future<Output = Result<T, E>>,
{
    let loader = Loader::start(label);
    let result = op.await;
    match &result {
        Ok(_) => loader.success(),
        Err(err) => loader.fail(&err.to_string()),
    }
    result
}

pub fn print_cli_header(command: &str, debug: bool) {
    let mode = if debug {
        "DEBUG".bright_yellow().bold().to_string()
    } else {
        "NORMAL".bright_blue().bold().to_string()
    };
    println!(
        "{} {} {} {}",
        "xbp".bright_magenta().bold(),
        "".bright_black(),
        command.bright_white().bold(),
        format!("[{}]", mode).bright_black()
    );
}

pub fn configure_color_output() {
    colored::control::set_override(should_emit_ansi());
}

pub fn should_emit_ansi() -> bool {
    // Respect common color control environment variables first.
    let disable_via_clicolor = std::env::var("CLICOLOR")
        .map(|value| value == "0")
        .unwrap_or(false);
    if std::env::var_os("NO_COLOR").is_some() || disable_via_clicolor {
        return false;
    }
    if std::env::var_os("FORCE_COLOR").is_some() || std::env::var_os("CLICOLOR_FORCE").is_some() {
        return true;
    }

    if legacy_cmd_without_color_hints() {
        return false;
    }

    let stdout_color = supports_color::on(Stream::Stdout).is_some();
    let stderr_color = supports_color::on(Stream::Stderr).is_some();
    stdout_color || stderr_color
}

pub fn section(title: &str) {
    println!(
        "\n{} {}",
        "".bright_blue().bold(),
        title.bright_blue().bold()
    );
}

pub fn divider(width: usize) {
    println!("{}", "".repeat(width).bright_black());
}

pub fn status_line(label: &str, status: &str, ok: bool) {
    let icon = if ok {
        "".bright_green().bold()
    } else {
        "".bright_red().bold()
    };
    let status = if ok {
        status.bright_green().to_string()
    } else {
        status.bright_red().to_string()
    };
    println!("  {} {} {}", icon, label.bright_white(), status);
}

pub fn tip(message: &str) {
    println!("{} {}", "Hint:".bright_yellow().bold(), message);
}

fn select_spinner_set(label: &str) -> &'static [&'static str] {
    let hash = label
        .bytes()
        .fold(0_u64, |acc, b| acc.wrapping_mul(16777619) ^ u64::from(b));
    let idx = (hash as usize) % SPINNER_SETS.len();
    SPINNER_SETS[idx]
}

#[cfg(windows)]
fn legacy_cmd_without_color_hints() -> bool {
    let Some(comspec) = std::env::var("ComSpec").ok() else {
        return false;
    };

    if !comspec.to_ascii_lowercase().ends_with("cmd.exe") {
        return false;
    }

    let has_color_hints = std::env::var_os("WT_SESSION").is_some()
        || std::env::var_os("ANSICON").is_some()
        || std::env::var("TERM_PROGRAM").is_ok()
        || std::env::var("TERM").is_ok_and(|value| value != "dumb")
        || std::env::var("ConEmuANSI")
            .map(|value| value.eq_ignore_ascii_case("ON"))
            .unwrap_or(false);

    !has_color_hints
}

#[cfg(not(windows))]
fn legacy_cmd_without_color_hints() -> bool {
    false
}

#[cfg(all(test, windows))]
mod tests {
    use super::legacy_cmd_without_color_hints;
    use once_cell::sync::Lazy;
    use std::sync::Mutex;

    static ENV_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));

    #[test]
    fn cmd_without_color_hints_disables_ansi() {
        let _guard = ENV_LOCK.lock().expect("env lock");

        let keys = [
            "ComSpec",
            "WT_SESSION",
            "ANSICON",
            "TERM_PROGRAM",
            "TERM",
            "ConEmuANSI",
        ];
        let snapshot: Vec<(String, Option<String>)> = keys
            .iter()
            .map(|key| ((*key).to_string(), std::env::var(key).ok()))
            .collect();

        std::env::set_var("ComSpec", r"C:\Windows\System32\cmd.exe");
        for key in [
            "WT_SESSION",
            "ANSICON",
            "TERM_PROGRAM",
            "TERM",
            "ConEmuANSI",
        ] {
            std::env::remove_var(key);
        }

        assert!(legacy_cmd_without_color_hints());

        std::env::set_var("WT_SESSION", "1");
        assert!(!legacy_cmd_without_color_hints());

        for (key, value) in snapshot {
            match value {
                Some(value) => std::env::set_var(key, value),
                None => std::env::remove_var(key),
            }
        }
    }
}