Skip to main content

claude_code_status_line/
render.rs

1use crate::colors::SectionColors;
2use crate::config::Config;
3use crate::types::Section;
4use crate::utils::RESET;
5
6/// Render sections using segmented style (either Powerline arrows or custom separator)
7pub fn render_segments(sections: &[&Section], config: &Config) -> String {
8    let mut result = String::new();
9
10    for (i, section) in sections.iter().enumerate() {
11        result.push_str(&render_section(&section.content, &section.colors, config));
12
13        if i < sections.len() - 1 {
14            let next = sections[i + 1];
15            if config.display.use_powerline {
16                let left_bg = effective_background(&section.colors, config);
17                let right_bg = effective_background(&next.colors, config);
18                result.push_str(&make_powerline_arrow(left_bg, right_bg, config));
19            } else if !config.display.segment_separator.is_empty() {
20                result.push_str(&make_separator(config));
21            }
22        }
23    }
24
25    // Add tail arrow only if using arrows
26    if config.display.use_powerline {
27        if let Some(last) = sections.last() {
28            let tail = match effective_background(&last.colors, config) {
29                Some(bg) => format!(
30                    "\x1b[38;2;{};{};{}m\x1b[49m{}",
31                    bg.0, bg.1, bg.2, config.display.arrow
32                ),
33                None => make_plain_arrow(config),
34            };
35            result.push_str(&tail);
36        }
37    }
38
39    result.push_str(RESET);
40    result
41}
42
43fn effective_background(colors: &SectionColors, config: &Config) -> Option<(u8, u8, u8)> {
44    if config.display.show_background {
45        colors.background
46    } else {
47        None
48    }
49}
50
51/// Render a single section with its colors (with padding)
52fn render_section(content: &str, colors: &SectionColors, config: &Config) -> String {
53    let bg_code = match effective_background(colors, config) {
54        Some(bg) => format!("\x1b[48;2;{};{};{}m", bg.0, bg.1, bg.2),
55        None => "\x1b[49m".to_string(), // Reset background
56    };
57
58    let padding = " ".repeat(config.display.section_padding);
59
60    format!(
61        "{}\x1b[38;2;{};{};{}m{}{}{}",
62        bg_code,
63        colors.foreground.0,
64        colors.foreground.1,
65        colors.foreground.2,
66        padding,
67        content,
68        padding
69    )
70}
71
72/// Generate powerline arrow between two sections
73fn make_powerline_arrow(
74    left_bg: Option<(u8, u8, u8)>,
75    right_bg: Option<(u8, u8, u8)>,
76    config: &Config,
77) -> String {
78    match (left_bg, right_bg) {
79        (Some(left_bg), Some(right_bg)) => format!(
80            "\x1b[38;2;{};{};{}m\x1b[48;2;{};{};{}m{}",
81            left_bg.0,
82            left_bg.1,
83            left_bg.2,
84            right_bg.0,
85            right_bg.1,
86            right_bg.2,
87            config.display.arrow
88        ),
89        _ => make_plain_arrow(config),
90    }
91}
92
93fn make_plain_arrow(config: &Config) -> String {
94    let color = config.theme.separator;
95    format!(
96        "{}\x1b[38;2;{};{};{}m{}",
97        RESET, color.0, color.1, color.2, config.display.arrow
98    )
99}
100
101/// Generate ANSI escape code for details text using section's details color
102fn make_details_from_section(colors: &SectionColors) -> String {
103    format!(
104        "\x1b[38;2;{};{};{}m",
105        colors.details.0, colors.details.1, colors.details.2
106    )
107}
108
109/// Generate ANSI escape code to restore foreground color for a section
110fn restore_fg_from_section(colors: &SectionColors) -> String {
111    format!(
112        "\x1b[38;2;{};{};{}m",
113        colors.foreground.0, colors.foreground.1, colors.foreground.2
114    )
115}
116
117/// Helper to get details and foreground color codes (unified logic)
118pub fn get_details_and_fg_codes(colors: &SectionColors) -> (String, String) {
119    (
120        make_details_from_section(colors),
121        restore_fg_from_section(colors),
122    )
123}
124
125/// Generate separator string with reset colors
126fn make_separator(config: &Config) -> String {
127    let color = config.theme.separator;
128    format!(
129        "{}\x1b[38;2;{};{};{}m{}\x1b[0m",
130        RESET, color.0, color.1, color.2, config.display.segment_separator
131    )
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    const TEST_COLORS: SectionColors = SectionColors {
139        background: Some((100, 100, 100)),
140        foreground: (200, 200, 200),
141        details: (150, 150, 150),
142    };
143
144    const TEST_COLORS_NO_BG: SectionColors = SectionColors {
145        background: None,
146        foreground: (255, 255, 255),
147        details: (128, 128, 128),
148    };
149
150    #[test]
151    fn test_render_segments_empty() {
152        let config = Config::default();
153        let sections: Vec<&Section> = vec![];
154        let result = render_segments(&sections, &config);
155        assert_eq!(result, RESET);
156    }
157
158    #[test]
159    fn test_render_segments_single() {
160        let config = Config::default();
161        let section = Section::new("test".to_string(), 0, TEST_COLORS);
162        let result = render_segments(&[&section], &config);
163
164        // Should contain the content
165        assert!(result.contains("test"));
166        // Should contain reset at the end
167        assert!(result.ends_with(RESET));
168    }
169
170    #[test]
171    fn test_render_segments_with_powerline() {
172        let mut config = Config::default();
173        config.display.use_powerline = true;
174        config.display.arrow = "".to_string();
175
176        let section1 = Section::new("test1".to_string(), 0, TEST_COLORS);
177        let section2 = Section::new("test2".to_string(), 1, TEST_COLORS);
178
179        let result = render_segments(&[&section1, &section2], &config);
180
181        // Should contain both sections
182        assert!(result.contains("test1"));
183        assert!(result.contains("test2"));
184        // Should contain powerline arrow
185        assert!(result.contains(""));
186    }
187
188    #[test]
189    fn test_render_segments_with_separator() {
190        let mut config = Config::default();
191        config.display.use_powerline = false;
192        config.display.segment_separator = " | ".to_string();
193
194        let section1 = Section::new("test1".to_string(), 0, TEST_COLORS);
195        let section2 = Section::new("test2".to_string(), 1, TEST_COLORS);
196
197        let result = render_segments(&[&section1, &section2], &config);
198
199        // Should contain separator
200        assert!(result.contains("|"));
201    }
202
203    #[test]
204    fn test_make_details_from_section() {
205        let result = make_details_from_section(&TEST_COLORS);
206        // Should be an ANSI color code
207        assert!(result.starts_with("\x1b[38;2;"));
208        assert!(result.contains("150"));
209    }
210
211    #[test]
212    fn test_restore_fg_from_section() {
213        let result = restore_fg_from_section(&TEST_COLORS);
214        // Should be an ANSI color code
215        assert!(result.starts_with("\x1b[38;2;"));
216        assert!(result.contains("200"));
217    }
218
219    #[test]
220    fn test_get_details_and_fg_codes() {
221        let (details, fg) = get_details_and_fg_codes(&TEST_COLORS);
222
223        assert!(details.starts_with("\x1b["));
224        assert!(fg.starts_with("\x1b["));
225        assert_ne!(details, fg);
226    }
227
228    #[test]
229    fn test_render_section_no_background() {
230        let config = Config::default();
231        let result = render_section("test", &TEST_COLORS_NO_BG, &config);
232
233        // Should contain reset background code
234        assert!(result.contains("\x1b[49m"));
235        // Should contain content
236        assert!(result.contains("test"));
237    }
238
239    #[test]
240    fn test_render_section_with_background() {
241        let config = Config::default();
242        let result = render_section("test", &TEST_COLORS, &config);
243
244        // Should contain background color code
245        assert!(result.contains("\x1b[48;2;"));
246        // Should contain RGB values
247        assert!(result.contains("100"));
248    }
249
250    #[test]
251    fn test_make_powerline_arrow() {
252        let mut config = Config::default();
253        config.display.arrow = "".to_string();
254
255        let left = Some((100, 100, 100));
256        let right = Some((200, 200, 200));
257        let result = make_powerline_arrow(left, right, &config);
258
259        // Should contain left color as foreground
260        assert!(result.contains("100"));
261        // Should contain right color as background
262        assert!(result.contains("200"));
263        // Should contain arrow
264        assert!(result.contains(""));
265    }
266
267    #[test]
268    fn test_render_section_without_background_when_disabled() {
269        let mut config = Config::default();
270        config.display.show_background = false;
271
272        let result = render_section("test", &TEST_COLORS, &config);
273        assert!(result.contains("\x1b[49m"));
274        assert!(!result.contains("\x1b[48;2;100;100;100m"));
275    }
276
277    #[test]
278    fn test_make_powerline_arrow_without_backgrounds() {
279        let mut config = Config::default();
280        config.display.arrow = ">".to_string();
281
282        let result = make_powerline_arrow(None, None, &config);
283        assert!(result.contains(">"));
284        assert!(!result.contains("\x1b[48;2;"));
285    }
286
287    #[test]
288    fn test_rgb_values_boundary() {
289        // Test with min RGB values
290        let colors_min = SectionColors {
291            background: Some((0, 0, 0)),
292            foreground: (0, 0, 0),
293            details: (0, 0, 0),
294        };
295        let result = make_details_from_section(&colors_min);
296        assert!(result.contains("0"));
297
298        // Test with max RGB values
299        let colors_max = SectionColors {
300            background: Some((255, 255, 255)),
301            foreground: (255, 255, 255),
302            details: (255, 255, 255),
303        };
304        let result = make_details_from_section(&colors_max);
305        assert!(result.contains("255"));
306    }
307}