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