Skip to main content

dotenv_space/utils/
ui.rs

1//! Terminal UI utilities — progress bars, boxed output, and status messages.
2//!
3//! All functions in this module write directly to stdout. They are intentionally
4//! thin wrappers around `colored` and `indicatif` so the rest of the codebase
5//! does not need to import those crates directly.
6//!
7//! # Future work
8//!
9//! - Respect a global `--no-color` flag by checking `colored::control::SHOULD_COLORIZE`.
10//! - Add a `spinner()` helper for indeterminate operations.
11//! - Add a `table()` helper for structured multi-column output.
12
13use colored::*;
14use indicatif::{ProgressBar, ProgressStyle};
15
16/// Create a styled progress bar with `len` steps and an initial `message`.
17///
18/// The bar uses the format:
19/// ```text
20/// ⠙ [████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 12/50 Processing KEY
21/// ```
22///
23/// The caller is responsible for calling [`ProgressBar::inc`] and
24/// [`ProgressBar::finish_with_message`] when done.
25pub fn progress_bar(len: u64, message: &str) -> ProgressBar {
26    let pb = ProgressBar::new(len);
27    pb.set_style(
28        ProgressStyle::default_bar()
29            .template("{spinner:.green} [{bar:40.cyan/blue}] {pos}/{len} {msg}")
30            .unwrap()
31            .progress_chars("#>-"),
32    );
33    pb.set_message(message.to_string());
34    pb
35}
36
37/// Print a cyan boxed header with an optional multi-line body.
38///
39/// ```text
40/// ┌─ Title ──────────────────────────────────────────────┐
41/// │ Optional message line                                │
42/// └──────────────────────────────────────────────────────┘
43/// ```
44///
45/// Pass an empty string for `message` to render a title-only box.
46pub fn print_box(title: &str, message: &str) {
47    let width = 60;
48    let border = "─".repeat(width - 4);
49
50    println!("\n{}", format!("┌─{}─┐", border).cyan());
51    println!(
52        "{}",
53        format!("│ {:<width$} │", title, width = width - 4).cyan()
54    );
55
56    if !message.is_empty() {
57        for line in message.lines() {
58            println!(
59                "{}",
60                format!("│ {:<width$} │", line, width = width - 4).cyan()
61            );
62        }
63    }
64
65    println!("{}\n", format!("└─{}─┘", border).cyan());
66}
67
68/// Print a green `✓ <message>` success line.
69pub fn success(message: &str) {
70    println!("{} {}", "✓".green(), message);
71}
72
73/// Print a red `✗ <message>` error line.
74pub fn error(message: &str) {
75    println!("{} {}", "✗".red(), message);
76}
77
78/// Print a yellow `⚠️ <message>` warning line.
79pub fn warning(message: &str) {
80    println!("{} {}", "⚠️".yellow(), message);
81}
82
83/// Print a cyan `ℹ️ <message>` info line.
84pub fn info(message: &str) {
85    println!("{} {}", "ℹ️".cyan(), message);
86}
87
88/// Print a dimmed horizontal rule (60 dashes).
89pub fn separator() {
90    println!("{}", "─".repeat(60).dimmed());
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_progress_bar_length() {
99        let pb = progress_bar(100, "Testing");
100        assert_eq!(pb.length(), Some(100));
101    }
102
103    #[test]
104    fn test_progress_bar_zero() {
105        // Edge case — a bar with zero steps should not panic.
106        let pb = progress_bar(0, "Empty");
107        assert_eq!(pb.length(), Some(0));
108    }
109}