Skip to main content

ez_token/cli/
output.rs

1use console::{Emoji, style};
2use indicatif::{ProgressBar, ProgressStyle};
3use miette::{IntoDiagnostic, Result};
4use std::time::Duration;
5
6/// A type alias for emojis with static lifetimes, used throughout the CLI output.
7pub type TermEmoji = Emoji<'static, 'static>;
8
9/// Named emoji variants used in CLI output.
10///
11/// Provides a type-safe alternative to scattering `Emoji("...", "...")` literals
12/// across the codebase. Each variant has a text fallback for terminals that
13/// do not support Unicode.
14pub enum AppEmoji {
15    /// ✨ Used for section headers.
16    Sparkle,
17    /// πŸ“‹ Used for success messages, typically when a token is copied to the clipboard.
18    Clipboard,
19    /// πŸš€ Used for action steps such as opening the browser.
20    Rocket,
21    /// πŸ’Ύ Used when configuration is saved to disk.
22    Floppy,
23}
24
25impl AppEmoji {
26    /// Returns the [`TermEmoji`] value for this variant.
27    pub fn as_emoji(&self) -> TermEmoji {
28        match self {
29            AppEmoji::Sparkle => Emoji("✨ ", ":-)"),
30            AppEmoji::Clipboard => Emoji("πŸ“‹ ", ""),
31            AppEmoji::Rocket => Emoji("πŸš€ ", ""),
32            AppEmoji::Floppy => Emoji("πŸ’Ύ ", ""),
33        }
34    }
35}
36
37/// Prints a bold green section header prefixed with a sparkle emoji.
38///
39/// # Example Output
40/// ```text
41/// ✨ OAuth2 Token Generator
42/// ```
43pub fn print_header(title: &str) {
44    println!(
45        "\n{} {}",
46        AppEmoji::Sparkle.as_emoji(),
47        style(title).bold().green()
48    );
49}
50
51/// Prints a bold step message prefixed with the given emoji.
52///
53/// Used to indicate progress through a multi-step flow.
54///
55/// # Example Output
56/// ```text
57/// πŸš€ Opening browser...
58/// ```
59pub fn print_step(emoji: AppEmoji, message: &str) {
60    println!("\n{} {}", emoji.as_emoji(), style(message).bold());
61}
62
63/// Prints a bold green success message prefixed with a clipboard emoji.
64///
65/// # Example Output
66/// ```text
67/// πŸ“‹ Token copied to clipboard!
68/// ```
69pub fn print_success(message: &str) {
70    println!(
71        "\n{} {}",
72        AppEmoji::Clipboard.as_emoji(),
73        style(message).bold().green()
74    );
75}
76
77/// Prints a yellow warning message prefixed with `Warning:`.
78///
79/// Used for non-fatal issues such as clipboard access failures.
80///
81/// # Example Output
82/// ```text
83/// Warning: Could not access clipboard.
84/// ```
85pub fn print_warning(message: &str) {
86    println!("{} {}", style("Warning:").yellow(), message);
87}
88
89/// Creates and starts an animated spinner with the given message.
90///
91/// The spinner ticks every 80ms and is displayed in blue. Call
92/// [`finish_spinner_success`] or [`finish_spinner_error`] to stop it.
93///
94/// # Errors
95///
96/// Returns an error if the underlying spinner style template is invalid.
97pub fn start_spinner(message: &str) -> Result<ProgressBar> {
98    let spinner = ProgressBar::new_spinner();
99    spinner.set_style(
100        ProgressStyle::default_spinner()
101            .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
102            .template("{spinner:.blue} {msg}")
103            .into_diagnostic()?,
104    );
105    spinner.set_message(message.to_string());
106    spinner.enable_steady_tick(Duration::from_millis(80));
107    Ok(spinner)
108}
109
110/// Stops the spinner and displays a green checkmark with a success message.
111///
112/// # Example Output
113/// ```text
114/// βœ” Authentication successful!
115/// ```
116pub fn finish_spinner_success(spinner: &ProgressBar, message: &str) {
117    spinner.finish_with_message(format!("{} {}", style("βœ”").green(), message));
118}
119
120/// Stops the spinner and displays a red cross with an error message.
121///
122/// # Example Output
123/// ```text
124/// ✘ Authentication failed!
125/// ```
126pub fn finish_spinner_error(spinner: &ProgressBar, message: &str) {
127    spinner.finish_with_message(format!("{} {}", style("✘").red(), message));
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_app_emoji_variants() {
136        let sparkle = AppEmoji::Sparkle.as_emoji();
137        let clipboard = AppEmoji::Clipboard.as_emoji();
138        let rocket = AppEmoji::Rocket.as_emoji();
139        let floppy = AppEmoji::Floppy.as_emoji();
140
141        assert!(!sparkle.to_string().is_empty());
142        assert!(!clipboard.to_string().is_empty());
143        assert!(!rocket.to_string().is_empty());
144        assert!(!floppy.to_string().is_empty());
145    }
146
147    #[test]
148    fn test_start_spinner_template_valid() {
149        let spinner_result = start_spinner("Fetching token...");
150        assert!(spinner_result.is_ok(), "Spinner template should be valid");
151    }
152
153    #[test]
154    fn test_spinner_success_lifecycle() {
155        let spinner = start_spinner("Working...").unwrap();
156        assert!(!spinner.is_finished());
157
158        finish_spinner_success(&spinner, "Done!");
159        assert!(spinner.is_finished());
160    }
161
162    #[test]
163    fn test_spinner_error_lifecycle() {
164        let spinner = start_spinner("Working...").unwrap();
165        assert!(!spinner.is_finished());
166
167        finish_spinner_error(&spinner, "Failed!");
168        assert!(spinner.is_finished());
169    }
170}