Skip to main content

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