claude-code-status-line 1.2.2

A configurable status line for Claude Code with powerline arrows, context tracking, and quota monitoring
Documentation
use crate::colors::SectionColors;
use crate::config::Config;
use crate::types::Section;
use crate::utils::RESET;

/// Render sections using segmented style (either Powerline arrows or custom separator)
pub fn render_segments(sections: &[&Section], config: &Config) -> String {
    let mut result = String::new();

    for (i, section) in sections.iter().enumerate() {
        result.push_str(&render_section(&section.content, &section.colors, config));

        if i < sections.len() - 1 {
            let next = sections[i + 1];
            if config.display.use_powerline {
                let left_bg = section.colors.background.unwrap_or((0, 0, 0));
                let right_bg = next.colors.background.unwrap_or((0, 0, 0));
                result.push_str(&make_powerline_arrow(left_bg, right_bg, config));
            } else if !config.display.segment_separator.is_empty() {
                result.push_str(&make_separator(config));
            }
        }
    }

    // Add tail arrow only if using arrows
    if config.display.use_powerline {
        if let Some(last) = sections.last() {
            if let Some(bg) = last.colors.background {
                result.push_str(&format!(
                    "\x1b[38;2;{};{};{}m\x1b[49m{}",
                    bg.0, bg.1, bg.2, config.display.arrow
                ));
            }
        }
    }

    result.push_str(RESET);
    result
}

/// Render a single section with its colors (with padding)
fn render_section(content: &str, colors: &SectionColors, config: &Config) -> String {
    let bg_code = match colors.background {
        Some(bg) => format!("\x1b[48;2;{};{};{}m", bg.0, bg.1, bg.2),
        None => "\x1b[49m".to_string(), // Reset background
    };

    let padding = " ".repeat(config.display.section_padding);

    format!(
        "{}\x1b[38;2;{};{};{}m{}{}{}",
        bg_code,
        colors.foreground.0,
        colors.foreground.1,
        colors.foreground.2,
        padding,
        content,
        padding
    )
}

/// Generate powerline arrow between two sections
fn make_powerline_arrow(left_bg: (u8, u8, u8), right_bg: (u8, u8, u8), config: &Config) -> String {
    format!(
        "\x1b[38;2;{};{};{}m\x1b[48;2;{};{};{}m{}",
        left_bg.0, left_bg.1, left_bg.2, right_bg.0, right_bg.1, right_bg.2, config.display.arrow
    )
}

/// Generate ANSI escape code for details text using section's details color
fn make_details_from_section(colors: &SectionColors) -> String {
    format!(
        "\x1b[38;2;{};{};{}m",
        colors.details.0, colors.details.1, colors.details.2
    )
}

/// Generate ANSI escape code to restore foreground color for a section
fn restore_fg_from_section(colors: &SectionColors) -> String {
    format!(
        "\x1b[38;2;{};{};{}m",
        colors.foreground.0, colors.foreground.1, colors.foreground.2
    )
}

/// Helper to get details and foreground color codes (unified logic)
pub fn get_details_and_fg_codes(colors: &SectionColors, _config: &Config) -> (String, String) {
    (
        make_details_from_section(colors),
        restore_fg_from_section(colors),
    )
}

/// Generate separator string with reset colors
fn make_separator(config: &Config) -> String {
    let color = config.theme.separator;
    format!(
        "{}\x1b[38;2;{};{};{}m{}\x1b[0m",
        RESET, color.0, color.1, color.2, config.display.segment_separator
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    const TEST_COLORS: SectionColors = SectionColors {
        background: Some((100, 100, 100)),
        foreground: (200, 200, 200),
        details: (150, 150, 150),
    };

    const TEST_COLORS_NO_BG: SectionColors = SectionColors {
        background: None,
        foreground: (255, 255, 255),
        details: (128, 128, 128),
    };

    #[test]
    fn test_render_segments_empty() {
        let config = Config::default();
        let sections: Vec<&Section> = vec![];
        let result = render_segments(&sections, &config);
        assert_eq!(result, RESET);
    }

    #[test]
    fn test_render_segments_single() {
        let config = Config::default();
        let section = Section::new("test".to_string(), 0, TEST_COLORS);
        let result = render_segments(&[&section], &config);

        // Should contain the content
        assert!(result.contains("test"));
        // Should contain reset at the end
        assert!(result.ends_with(RESET));
    }

    #[test]
    fn test_render_segments_with_powerline() {
        let mut config = Config::default();
        config.display.use_powerline = true;
        config.display.arrow = "".to_string();

        let section1 = Section::new("test1".to_string(), 0, TEST_COLORS);
        let section2 = Section::new("test2".to_string(), 1, TEST_COLORS);

        let result = render_segments(&[&section1, &section2], &config);

        // Should contain both sections
        assert!(result.contains("test1"));
        assert!(result.contains("test2"));
        // Should contain powerline arrow
        assert!(result.contains(""));
    }

    #[test]
    fn test_render_segments_with_separator() {
        let mut config = Config::default();
        config.display.use_powerline = false;
        config.display.segment_separator = " | ".to_string();

        let section1 = Section::new("test1".to_string(), 0, TEST_COLORS);
        let section2 = Section::new("test2".to_string(), 1, TEST_COLORS);

        let result = render_segments(&[&section1, &section2], &config);

        // Should contain separator
        assert!(result.contains("|"));
    }

    #[test]
    fn test_make_details_from_section() {
        let result = make_details_from_section(&TEST_COLORS);
        // Should be an ANSI color code
        assert!(result.starts_with("\x1b[38;2;"));
        assert!(result.contains("150"));
    }

    #[test]
    fn test_restore_fg_from_section() {
        let result = restore_fg_from_section(&TEST_COLORS);
        // Should be an ANSI color code
        assert!(result.starts_with("\x1b[38;2;"));
        assert!(result.contains("200"));
    }

    #[test]
    fn test_get_details_and_fg_codes() {
        let config = Config::default();
        let (details, fg) = get_details_and_fg_codes(&TEST_COLORS, &config);

        // Both should be ANSI codes
        assert!(details.starts_with("\x1b["));
        assert!(fg.starts_with("\x1b["));
        // Should be different
        assert_ne!(details, fg);
    }

    #[test]
    fn test_render_section_no_background() {
        let config = Config::default();
        let result = render_section("test", &TEST_COLORS_NO_BG, &config);

        // Should contain reset background code
        assert!(result.contains("\x1b[49m"));
        // Should contain content
        assert!(result.contains("test"));
    }

    #[test]
    fn test_render_section_with_background() {
        let config = Config::default();
        let result = render_section("test", &TEST_COLORS, &config);

        // Should contain background color code
        assert!(result.contains("\x1b[48;2;"));
        // Should contain RGB values
        assert!(result.contains("100"));
    }

    #[test]
    fn test_make_powerline_arrow() {
        let mut config = Config::default();
        config.display.arrow = "".to_string();

        let left = (100, 100, 100);
        let right = (200, 200, 200);
        let result = make_powerline_arrow(left, right, &config);

        // Should contain left color as foreground
        assert!(result.contains("100"));
        // Should contain right color as background
        assert!(result.contains("200"));
        // Should contain arrow
        assert!(result.contains(""));
    }

    #[test]
    fn test_rgb_values_boundary() {
        // Test with min RGB values
        let colors_min = SectionColors {
            background: Some((0, 0, 0)),
            foreground: (0, 0, 0),
            details: (0, 0, 0),
        };
        let result = make_details_from_section(&colors_min);
        assert!(result.contains("0"));

        // Test with max RGB values
        let colors_max = SectionColors {
            background: Some((255, 255, 255)),
            foreground: (255, 255, 255),
            details: (255, 255, 255),
        };
        let result = make_details_from_section(&colors_max);
        assert!(result.contains("255"));
    }
}