microsandbox_utils/
term.rs

1//! Module containing terminal utilities
2
3use indicatif::{MultiProgress, MultiProgressAlignment, ProgressBar, ProgressStyle};
4use std::sync::{Arc, LazyLock};
5
6//--------------------------------------------------------------------------------------------------
7// Constants
8//--------------------------------------------------------------------------------------------------
9
10/// The multi-progress bar for CLI visualizations
11pub static MULTI_PROGRESS: LazyLock<Arc<MultiProgress>> = LazyLock::new(|| {
12    let mp = MultiProgress::new();
13    mp.set_alignment(MultiProgressAlignment::Top);
14    Arc::new(mp)
15});
16
17/// The checkmark for CLI visualizations
18pub static CHECKMARK: LazyLock<String> =
19    LazyLock::new(|| format!("{}", console::style("✓").green()));
20
21/// The error mark  for CLI visualizations
22pub static ERROR_MARK: LazyLock<String> =
23    LazyLock::new(|| format!("{}", console::style("✗").red()));
24
25/// The tick strings for CLI visualizations
26pub static TICK_STRINGS: LazyLock<[&str; 11]> =
27    LazyLock::new(|| ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", &CHECKMARK]);
28
29/// The error tick strings for CLI visualizations
30pub static ERROR_TICK_STRINGS: LazyLock<[&str; 2]> = LazyLock::new(|| ["⠏", &ERROR_MARK]);
31
32//--------------------------------------------------------------------------------------------------
33// Functions
34//--------------------------------------------------------------------------------------------------
35
36/// Determines if the process is running in an interactive terminal environment
37pub fn is_interactive_terminal() -> bool {
38    // Check if stdin and stdout are TTYs
39    let stdin_is_tty = unsafe { libc::isatty(libc::STDIN_FILENO) == 1 };
40    let stdout_is_tty = unsafe { libc::isatty(libc::STDOUT_FILENO) == 1 };
41
42    // Base check: both stdin and stdout must be TTYs
43    let is_tty = stdin_is_tty && stdout_is_tty;
44
45    // Optional enhancement: check for TERM, but don't require it
46    let has_term = std::env::var("TERM").is_ok();
47
48    // Log the detection for debugging
49    if is_tty && !has_term {
50        tracing::debug!("detected TTY without TERM environment variable");
51    }
52
53    // Return true if we have TTYs, regardless of TERM
54    is_tty
55}
56
57/// Determines if the process is running in an ANSI terminal environment
58pub fn is_ansi_interactive_terminal() -> bool {
59    is_interactive_terminal() && !std::env::var("TERM").unwrap_or_default().contains("dumb")
60}
61
62/// Creates a spinner progress bar with a message for visualizing operations like fetching.
63///
64/// This is a utility function to standardize the creation of progress spinners across
65/// different operations such as fetching indexes, manifests, and configs.
66///
67/// ## Arguments
68///
69/// * `message` - The message to display next to the spinner
70/// * `insert_at_position` - Optional position to insert the spinner at in the multi-progress display
71///
72/// ## Returns
73///
74/// An Option containing the progress bar, or None if the cli feature is not enabled
75pub fn create_spinner(
76    message: String,
77    insert_at_position: Option<usize>,
78    len: Option<u64>,
79) -> ProgressBar {
80    let pb = if let Some(len) = len {
81        ProgressBar::new(len)
82    } else {
83        ProgressBar::new_spinner()
84    };
85
86    let pb = if let Some(pos) = insert_at_position {
87        MULTI_PROGRESS.insert(pos, pb)
88    } else {
89        MULTI_PROGRESS.add(pb)
90    };
91
92    let style = if let Some(_) = len {
93        ProgressStyle::with_template("{spinner} {msg} {pos:.bold} / {len:.dim}")
94            .unwrap()
95            .tick_strings(&*TICK_STRINGS)
96    } else {
97        ProgressStyle::with_template("{spinner} {msg}")
98            .unwrap()
99            .tick_strings(&*TICK_STRINGS)
100    };
101
102    pb.set_style(style);
103    pb.set_message(message);
104    pb.enable_steady_tick(std::time::Duration::from_millis(80));
105    pb
106}
107
108/// Finishes a spinner with an error mark (✗) instead of a checkmark.
109/// Used for error paths to visually indicate failure.
110///
111/// ## Arguments
112///
113/// * `pb` - The progress bar to finish with an error mark
114pub fn finish_with_error(pb: &ProgressBar) {
115    let style = ProgressStyle::with_template("{spinner} {msg}")
116        .unwrap()
117        .tick_strings(&*ERROR_TICK_STRINGS);
118
119    pb.set_style(style);
120    pb.finish();
121}