git_iris/theme/adapters/
cli.rs

1//! CLI adapter for theme types.
2//!
3//! Provides conversion from theme types to colored crate types for terminal output.
4
5use colored::{ColoredString, Colorize};
6
7use crate::theme::{Gradient, ThemeColor, ThemeStyle};
8
9/// Convert a `ThemeColor` to an RGB tuple for use with the colored crate.
10pub trait ToColoredRgb {
11    /// Convert to RGB tuple for colored crate's `.truecolor()` method.
12    fn to_rgb(&self) -> (u8, u8, u8);
13}
14
15impl ToColoredRgb for ThemeColor {
16    fn to_rgb(&self) -> (u8, u8, u8) {
17        (self.r, self.g, self.b)
18    }
19}
20
21/// Extension trait for applying theme colors to strings.
22pub trait ColoredExt {
23    /// Apply a theme color as foreground.
24    fn theme_fg(self, color: ThemeColor) -> ColoredString;
25
26    /// Apply a theme color as background.
27    fn theme_bg(self, color: ThemeColor) -> ColoredString;
28
29    /// Apply a theme style.
30    fn theme_style(self, style: &ThemeStyle) -> ColoredString;
31}
32
33impl<S: AsRef<str>> ColoredExt for S {
34    fn theme_fg(self, color: ThemeColor) -> ColoredString {
35        self.as_ref().truecolor(color.r, color.g, color.b)
36    }
37
38    fn theme_bg(self, color: ThemeColor) -> ColoredString {
39        self.as_ref().on_truecolor(color.r, color.g, color.b)
40    }
41
42    fn theme_style(self, style: &ThemeStyle) -> ColoredString {
43        let mut result: ColoredString = self.as_ref().into();
44
45        if let Some(fg) = style.fg {
46            result = result.truecolor(fg.r, fg.g, fg.b);
47        }
48
49        if let Some(bg) = style.bg {
50            result = result.on_truecolor(bg.r, bg.g, bg.b);
51        }
52
53        if style.bold {
54            result = result.bold();
55        }
56
57        if style.italic {
58            result = result.italic();
59        }
60
61        if style.underline {
62            result = result.underline();
63        }
64
65        if style.dim {
66            result = result.dimmed();
67        }
68
69        result
70    }
71}
72
73/// Apply a gradient to a string for CLI output.
74///
75/// Returns a string with ANSI escape codes for each character.
76#[allow(clippy::cast_precision_loss, clippy::as_conversions)]
77pub fn gradient_string(text: &str, gradient: &Gradient) -> String {
78    let chars: Vec<char> = text.chars().collect();
79    let len = chars.len().max(1);
80
81    let mut result = String::new();
82
83    for (i, c) in chars.into_iter().enumerate() {
84        let t = if len == 1 {
85            0.0
86        } else {
87            i as f32 / (len - 1) as f32
88        };
89        let color = gradient.at(t);
90        let colored = c.to_string().truecolor(color.r, color.g, color.b);
91        result.push_str(&colored.to_string());
92    }
93
94    result
95}
96
97/// Extension trait for easy access to theme colors for CLI output.
98pub trait ThemeCliExt {
99    /// Get a token color as RGB tuple.
100    fn cli_rgb(&self, token: &str) -> (u8, u8, u8);
101
102    /// Apply a token color to a string.
103    fn cli_colored(&self, text: &str, token: &str) -> ColoredString;
104
105    /// Apply a gradient to a string.
106    fn cli_gradient(&self, text: &str, gradient_name: &str) -> String;
107}
108
109impl ThemeCliExt for crate::theme::Theme {
110    fn cli_rgb(&self, token: &str) -> (u8, u8, u8) {
111        self.color(token).to_rgb()
112    }
113
114    fn cli_colored(&self, text: &str, token: &str) -> ColoredString {
115        let color = self.color(token);
116        text.truecolor(color.r, color.g, color.b)
117    }
118
119    fn cli_gradient(&self, text: &str, gradient_name: &str) -> String {
120        if let Some(gradient) = self.get_gradient(gradient_name) {
121            gradient_string(text, gradient)
122        } else {
123            text.to_string()
124        }
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_to_rgb() {
134        let color = ThemeColor::new(225, 53, 255);
135        assert_eq!(color.to_rgb(), (225, 53, 255));
136    }
137
138    #[test]
139    fn test_theme_fg() {
140        let color = ThemeColor::new(255, 0, 0);
141        let result = "test".theme_fg(color);
142        // Just verify it doesn't panic and produces output
143        assert!(!result.to_string().is_empty());
144    }
145
146    #[test]
147    fn test_gradient_string() {
148        // Force color output for testing
149        colored::control::set_override(true);
150
151        let gradient = Gradient::new(vec![ThemeColor::new(255, 0, 0), ThemeColor::new(0, 0, 255)]);
152        let result = gradient_string("test", &gradient);
153        // Verify output contains ANSI codes (when color is forced)
154        assert!(result.contains("\x1b[") || result.contains("test"));
155
156        colored::control::unset_override();
157    }
158}