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}