Skip to main content

cascade_cli/cli/
output.rs

1use console::{style, Color, Emoji, Style};
2use std::fmt::Display;
3
4/// Theme configuration for Cascade CLI
5/// Matches the branding: black, gray, green palette
6struct Theme;
7
8impl Theme {
9    /// Bright/bold green for success messages (matches banner accent)
10    /// Using Color256(46) for a brighter, more vibrant green
11    fn success_style() -> Style {
12        Style::new().color256(46).bold() // Bright bold green
13    }
14
15    /// Red for errors
16    const ERROR: Color = Color::Red;
17
18    /// Yellow for warnings
19    const WARNING: Color = Color::Yellow;
20
21    /// Muted green (Color256) for info - complements success green
22    /// Using terminal color 35 (teal/green) for better readability
23    fn info_style() -> Style {
24        Style::new().color256(35) // Muted teal-green
25    }
26
27    /// Same muted green for tips
28    fn tip_style() -> Style {
29        Style::new().color256(35) // Muted teal-green
30    }
31
32    /// Dim gray for secondary text
33    fn dim_style() -> Style {
34        Style::new().dim()
35    }
36}
37
38/// Centralized output formatting utilities for consistent CLI presentation
39pub struct Output;
40
41impl Output {
42    /// Print a success message with checkmark (bright bold green)
43    pub fn success<T: Display>(message: T) {
44        println!("{} {}", Theme::success_style().apply_to("✓"), message);
45    }
46
47    /// Print an error message with X mark
48    pub fn error<T: Display>(message: T) {
49        println!("{} {}", style("✗").fg(Theme::ERROR), message);
50    }
51
52    /// Print a warning message with warning emoji
53    pub fn warning<T: Display>(message: T) {
54        println!("{} {}", style("⚠").fg(Theme::WARNING), message);
55    }
56
57    /// Print an info message with info emoji (muted green)
58    pub fn info<T: Display>(message: T) {
59        println!("{} {}", Theme::info_style().apply_to("ℹ"), message);
60    }
61
62    /// Print a sub-item with arrow prefix
63    pub fn sub_item<T: Display>(message: T) {
64        println!("  {} {}", Theme::dim_style().apply_to("→"), message);
65    }
66
67    /// Print a bullet point
68    pub fn bullet<T: Display>(message: T) {
69        println!("  {} {}", Theme::dim_style().apply_to("•"), message);
70    }
71
72    /// Print a section header
73    pub fn section<T: Display>(title: T) {
74        println!("\n{}", style(title).bold().underlined());
75    }
76
77    /// Print a tip/suggestion (muted green)
78    pub fn tip<T: Display>(message: T) {
79        println!(
80            "{} {}",
81            Theme::tip_style().apply_to("TIP:"),
82            Theme::dim_style().apply_to(message)
83        );
84    }
85
86    /// Print progress indicator (muted green)
87    pub fn progress<T: Display>(message: T) {
88        print!("{} {}", Theme::info_style().apply_to("→"), message);
89        use std::io::{self, Write};
90        io::stdout().flush().unwrap();
91    }
92
93    /// Print success checkmark inline (for use after progress)
94    pub fn success_inline() {
95        println!(" {}", Theme::success_style().apply_to("✓"));
96    }
97
98    /// Print error cross inline (for use after progress)
99    pub fn error_inline<T: Display>(message: T) {
100        if message.to_string().is_empty() {
101            println!(" {}", style("✗").fg(Theme::ERROR));
102        } else {
103            println!(" {} {}", style("✗").fg(Theme::ERROR), message);
104        }
105    }
106
107    /// Print a divider line
108    pub fn divider() {
109        println!("{}", Theme::dim_style().apply_to("─".repeat(50)));
110    }
111
112    /// Print stack information in a formatted way
113    pub fn stack_info(
114        name: &str,
115        id: &str,
116        base_branch: &str,
117        working_branch: Option<&str>,
118        is_active: bool,
119    ) {
120        // Show as info, not success (we're viewing, not creating)
121        println!(
122            "{} {}",
123            Theme::info_style().apply_to("Stack:"),
124            style(name).bold()
125        );
126        Self::sub_item(format!("Stack ID: {}", Theme::dim_style().apply_to(id)));
127        Self::sub_item(format!(
128            "Base branch: {}",
129            Theme::info_style().apply_to(base_branch)
130        ));
131
132        if let Some(working) = working_branch {
133            Self::sub_item(format!(
134                "Working branch: {}",
135                Theme::info_style().apply_to(working)
136            ));
137        }
138
139        if is_active {
140            Self::sub_item(format!(
141                "Status: {}",
142                Theme::success_style().apply_to("Active")
143            ));
144        }
145    }
146
147    /// Print next steps guidance
148    pub fn next_steps(steps: &[&str]) {
149        println!();
150        Self::tip("Next steps:");
151        for step in steps {
152            Self::bullet(step);
153        }
154    }
155
156    /// Print a command example
157    pub fn command_example<T: Display>(command: T) {
158        println!("  {}", style(command).fg(Theme::WARNING));
159    }
160
161    /// Print a check start message
162    pub fn check_start<T: Display>(message: T) {
163        println!("\n{} {}", style("🔍").bright(), style(message).bold());
164    }
165
166    /// Print a solution message
167    pub fn solution<T: Display>(message: T) {
168        println!("     {}: {}", style("Solution").fg(Theme::WARNING), message);
169    }
170
171    /// Print a numbered item (muted green)
172    pub fn numbered_item<T: Display>(number: usize, message: T) {
173        println!("  {}. {}", Theme::info_style().apply_to(number), message);
174    }
175
176    /// Print empty line for spacing
177    pub fn spacing() {
178        println!();
179    }
180
181    /// Format stack entry status with appropriate color
182    /// - pending: Yellow (work in progress)
183    /// - submitted: Muted green (PR open/under review)
184    /// - merged: Bright green (completed!)
185    pub fn entry_status(is_submitted: bool, is_merged: bool) -> String {
186        if is_merged {
187            format!("{}", Theme::success_style().apply_to("[merged]"))
188        } else if is_submitted {
189            format!("{}", Theme::info_style().apply_to("[submitted]"))
190        } else {
191            format!("{}", style("[pending]").fg(Theme::WARNING))
192        }
193    }
194}
195
196/// Emojis for different contexts
197pub struct Emojis;
198
199impl Emojis {
200    pub const SUCCESS: Emoji<'_, '_> = Emoji("✓", "OK");
201    pub const ERROR: Emoji<'_, '_> = Emoji("✗", "ERROR");
202    pub const WARNING: Emoji<'_, '_> = Emoji("⚠", "WARNING");
203    pub const INFO: Emoji<'_, '_> = Emoji("ℹ", "INFO");
204    pub const TIP: Emoji<'_, '_> = Emoji("💡", "TIP");
205    pub const ROCKET: Emoji<'_, '_> = Emoji("🚀", "ROCKET");
206    pub const SEARCH: Emoji<'_, '_> = Emoji("🔍", "SEARCH");
207    pub const UPLOAD: Emoji<'_, '_> = Emoji("📤", "UPLOAD");
208    pub const STACK: Emoji<'_, '_> = Emoji("📊", "STACK");
209}