git_iris/
ui.rs

1//! CLI output utilities with `SilkCircuit` Neon theming.
2//!
3//! This module provides themed CLI output using the centralized theme system.
4//! All colors are resolved at runtime from the active theme.
5
6use colored::Colorize;
7use indicatif::{ProgressBar, ProgressStyle};
8use parking_lot::Mutex;
9use std::fmt::Write;
10use std::time::Duration;
11
12use crate::theme;
13use crate::theme::adapters::cli::gradient_string;
14
15// ═══════════════════════════════════════════════════════════════════════════════
16// Theme-Based RGB Accessors for CLI Output
17// ═══════════════════════════════════════════════════════════════════════════════
18
19/// RGB tuple accessors for use with the `colored` crate's `.truecolor()` method.
20/// All colors resolve from the current theme at runtime.
21pub mod rgb {
22    use crate::theme;
23    use crate::theme::adapters::cli::ToColoredRgb;
24
25    /// Get primary accent color (Electric Purple) RGB from theme
26    pub fn accent_primary() -> (u8, u8, u8) {
27        theme::current().color("accent.primary").to_rgb()
28    }
29
30    /// Get secondary accent color (Neon Cyan) RGB from theme
31    pub fn accent_secondary() -> (u8, u8, u8) {
32        theme::current().color("accent.secondary").to_rgb()
33    }
34
35    /// Get tertiary accent color (Coral) RGB from theme
36    pub fn accent_tertiary() -> (u8, u8, u8) {
37        theme::current().color("accent.tertiary").to_rgb()
38    }
39
40    /// Get warning color (Electric Yellow) RGB from theme
41    pub fn warning() -> (u8, u8, u8) {
42        theme::current().color("warning").to_rgb()
43    }
44
45    /// Get success color (Success Green) RGB from theme
46    pub fn success() -> (u8, u8, u8) {
47        theme::current().color("success").to_rgb()
48    }
49
50    /// Get error color (Error Red) RGB from theme
51    pub fn error() -> (u8, u8, u8) {
52        theme::current().color("error").to_rgb()
53    }
54
55    /// Get primary text color RGB from theme
56    pub fn text_primary() -> (u8, u8, u8) {
57        theme::current().color("text.primary").to_rgb()
58    }
59
60    /// Get secondary text color RGB from theme
61    pub fn text_secondary() -> (u8, u8, u8) {
62        theme::current().color("text.secondary").to_rgb()
63    }
64
65    /// Get muted text color RGB from theme
66    pub fn text_muted() -> (u8, u8, u8) {
67        theme::current().color("text.muted").to_rgb()
68    }
69
70    /// Get dim text color RGB from theme
71    pub fn text_dim() -> (u8, u8, u8) {
72        theme::current().color("text.dim").to_rgb()
73    }
74}
75
76/// Track quiet mode state
77static QUIET_MODE: std::sync::LazyLock<Mutex<bool>> =
78    std::sync::LazyLock::new(|| Mutex::new(false));
79
80/// Enable or disable quiet mode
81pub fn set_quiet_mode(enabled: bool) {
82    let mut quiet_mode = QUIET_MODE.lock();
83    *quiet_mode = enabled;
84}
85
86/// Check if quiet mode is enabled
87pub fn is_quiet_mode() -> bool {
88    *QUIET_MODE.lock()
89}
90
91pub fn create_spinner(message: &str) -> ProgressBar {
92    // Don't create a spinner in quiet mode
93    if is_quiet_mode() {
94        return ProgressBar::hidden();
95    }
96
97    let pb = ProgressBar::new_spinner();
98
99    // Use agent-aware spinner if agent mode is enabled
100    if crate::agents::status::is_agent_mode_enabled() {
101        pb.set_style(
102            ProgressStyle::default_spinner()
103                .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏")
104                .template("{spinner:.bright_cyan.bold} {msg}")
105                .expect("Could not set spinner style"),
106        );
107
108        // Start with Iris initialization message
109        pb.set_message("◎ Iris initializing...");
110
111        // Set up a custom callback to update the message from Iris status
112        let pb_clone = pb.clone();
113        tokio::spawn(async move {
114            let mut interval = tokio::time::interval(tokio::time::Duration::from_millis(200));
115            loop {
116                interval.tick().await;
117                let status_message = crate::agents::status::IRIS_STATUS.get_for_spinner();
118                pb_clone.set_message(status_message.text);
119            }
120        });
121
122        pb.enable_steady_tick(Duration::from_millis(100));
123    } else {
124        pb.set_style(
125            ProgressStyle::default_spinner()
126                .tick_chars("✦✧✶✷✸✹✺✻✼✽")
127                .template("{spinner} {msg}")
128                .expect("Could not set spinner style"),
129        );
130        pb.set_message(message.to_string());
131        pb.enable_steady_tick(Duration::from_millis(100));
132    }
133
134    pb
135}
136
137/// Print info message using theme colors
138pub fn print_info(message: &str) {
139    if !is_quiet_mode() {
140        let color = theme::current().color("info");
141        println!("{}", message.truecolor(color.r, color.g, color.b).bold());
142    }
143}
144
145/// Print warning message using theme colors
146pub fn print_warning(message: &str) {
147    if !is_quiet_mode() {
148        let color = theme::current().color("warning");
149        println!("{}", message.truecolor(color.r, color.g, color.b).bold());
150    }
151}
152
153/// Print error message using theme colors
154pub fn print_error(message: &str) {
155    // Always print errors, even in quiet mode
156    let color = theme::current().color("error");
157    eprintln!("{}", message.truecolor(color.r, color.g, color.b).bold());
158}
159
160/// Print success message using theme colors
161pub fn print_success(message: &str) {
162    if !is_quiet_mode() {
163        let color = theme::current().color("success");
164        println!("{}", message.truecolor(color.r, color.g, color.b).bold());
165    }
166}
167
168pub fn print_version(version: &str) {
169    if !is_quiet_mode() {
170        let t = theme::current();
171        let purple = t.color("accent.primary");
172        let cyan = t.color("accent.secondary");
173        let green = t.color("success");
174
175        println!(
176            "{} {} {}",
177            "🔮 Git-Iris".truecolor(purple.r, purple.g, purple.b).bold(),
178            "version".truecolor(cyan.r, cyan.g, cyan.b),
179            version.truecolor(green.r, green.g, green.b)
180        );
181    }
182}
183
184/// Print content with decorative borders
185pub fn print_bordered_content(content: &str) {
186    if !is_quiet_mode() {
187        let color = theme::current().color("accent.primary");
188        println!("{}", "━".repeat(50).truecolor(color.r, color.g, color.b));
189        println!("{content}");
190        println!("{}", "━".repeat(50).truecolor(color.r, color.g, color.b));
191    }
192}
193
194/// Print a simple message (respects quiet mode)
195pub fn print_message(message: &str) {
196    if !is_quiet_mode() {
197        println!("{message}");
198    }
199}
200
201/// Print an empty line (respects quiet mode)
202pub fn print_newline() {
203    if !is_quiet_mode() {
204        println!();
205    }
206}
207
208/// Create gradient text with `SilkCircuit` Electric Purple -> Neon Cyan
209pub fn create_gradient_text(text: &str) -> String {
210    if let Some(gradient) = theme::current().get_gradient("primary") {
211        gradient_string(text, gradient)
212    } else {
213        // Fallback to legacy gradient
214        let gradient = vec![
215            (225, 53, 255),  // Electric Purple
216            (200, 100, 255), // Mid purple
217            (180, 150, 250), // Light purple
218            (150, 200, 245), // Purple-cyan
219            (128, 255, 234), // Neon Cyan
220        ];
221        apply_gradient(text, &gradient)
222    }
223}
224
225/// Create secondary gradient with `SilkCircuit` Coral -> Electric Yellow
226pub fn create_secondary_gradient_text(text: &str) -> String {
227    if let Some(gradient) = theme::current().get_gradient("warm") {
228        gradient_string(text, gradient)
229    } else {
230        // Fallback to legacy gradient
231        let gradient = vec![
232            (255, 106, 193), // Coral
233            (255, 150, 180), // Light coral
234            (255, 200, 160), // Coral-yellow
235            (248, 230, 140), // Light yellow
236            (241, 250, 140), // Electric Yellow
237        ];
238        apply_gradient(text, &gradient)
239    }
240}
241
242fn apply_gradient(text: &str, gradient: &[(u8, u8, u8)]) -> String {
243    let chars: Vec<char> = text.chars().collect();
244    let chars_len = chars.len();
245    let gradient_len = gradient.len();
246
247    let mut result = String::new();
248
249    if chars_len == 0 || gradient_len == 0 {
250        return result;
251    }
252
253    chars.iter().enumerate().fold(&mut result, |acc, (i, &c)| {
254        let index = if chars_len == 1 {
255            0
256        } else {
257            i * (gradient_len - 1) / (chars_len - 1)
258        };
259        let (r, g, b) = gradient[index];
260        write!(acc, "{}", c.to_string().truecolor(r, g, b)).expect("writing to string cannot fail");
261        acc
262    });
263
264    result
265}