claude-code-status-line 1.2.9

A configurable status line for Claude Code with powerline arrows, context tracking, and quota monitoring
Documentation
use std::collections::HashSet;

use crate::config::Config;
use crate::render::render_segments;
use crate::types::{Section, SectionKind};
use crate::utils::{truncate_with_ellipsis, visible_len};

/// Build statusline - wrapper that calls either single-line or multi-line implementation
pub fn build_statusline(sections: Vec<Section>, max_width: usize, config: &Config) -> Vec<String> {
    if config.display.multiline {
        build_statusline_multiline(sections, max_width, config)
    } else {
        vec![build_statusline_single(sections, max_width, config)]
    }
}

/// Calculate the total visible width of a list of sections given the config
fn calculate_total_width(sections: &[&Section], config: &Config) -> usize {
    if sections.is_empty() {
        return 0;
    }

    let content_width: usize = sections
        .iter()
        .map(|s| s.width + 2 * config.display.section_padding)
        .sum();
    let count = sections.len();

    let separator_overhead = if config.display.use_powerline {
        count
    } else if count > 1 {
        (count - 1) * visible_len(&config.display.segment_separator)
    } else {
        0
    };

    content_width + separator_overhead
}

/// Deduplicate sections by preferring detailed versions over compact ones
fn deduplicate_sections(sections: &[Section]) -> Vec<&Section> {
    let detailed_labels: HashSet<&str> = sections
        .iter()
        .filter_map(|s| match &s.kind {
            SectionKind::QuotaDetailed(l) => Some(l.as_str()),
            _ => None,
        })
        .collect();

    let has_context_detailed = sections
        .iter()
        .any(|s| matches!(s.kind, SectionKind::ContextDetailed));

    sections
        .iter()
        .filter(|section| match &section.kind {
            SectionKind::QuotaCompact(label) => !detailed_labels.contains(label.as_str()),
            SectionKind::ContextCompact => !has_context_detailed,
            _ => true,
        })
        .collect()
}

/// Build statusline content (single-line mode), removing low-priority sections if too wide for terminal
fn build_statusline_single(sections: Vec<Section>, max_width: usize, config: &Config) -> String {
    if sections.is_empty() {
        return String::new();
    }

    let mut sorted = sections;
    sorted.sort_by_key(|s| s.priority);

    while sorted.len() > 1 {
        let deduped = deduplicate_sections(&sorted);
        let total_width = calculate_total_width(&deduped, config);

        if total_width <= max_width {
            return render_segments(&deduped, config);
        }

        sorted.pop();
    }

    if let Some(last) = sorted.first() {
        let rendered = render_segments(&[last], config);
        let visible = visible_len(&rendered);
        if visible > max_width {
            return truncate_with_ellipsis(&rendered, max_width);
        }
        return rendered;
    }

    String::new()
}

/// Build statusline content (multi-line mode), wrapping to multiple lines instead of dropping sections
fn build_statusline_multiline(
    sections: Vec<Section>,
    max_width: usize,
    config: &Config,
) -> Vec<String> {
    if sections.is_empty() {
        return vec![];
    }

    let mut sorted = sections;
    sorted.sort_by_key(|s| s.priority);

    let deduped = deduplicate_sections(&sorted);

    let mut lines: Vec<String> = Vec::new();
    let mut current_line: Vec<&Section> = Vec::new();

    for section in deduped {
        let mut test_line = current_line.clone();
        test_line.push(section);
        let total_width = calculate_total_width(&test_line, config);

        if total_width <= max_width || current_line.is_empty() {
            current_line.push(section);
        } else {
            lines.push(render_segments(&current_line, config));
            current_line.clear();
            current_line.push(section);
        }
    }

    if !current_line.is_empty() {
        lines.push(render_segments(&current_line, config));
    }

    lines
}

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

    const TEST_COLORS: SectionColors = SectionColors {
        background: Some((0, 0, 0)),
        foreground: (255, 255, 255),
        details: (128, 128, 128),
    };

    #[test]
    fn test_section_dedup() {
        let sections = vec![
            Section::new_quota_compact("5h", "5h: 10%".to_string(), 1, TEST_COLORS),
            Section::new_quota_detailed("5h", "5h: 10% (1h)".to_string(), 101, TEST_COLORS),
        ];

        let config = Config::default();
        let result = build_statusline(sections.clone(), 100, &config);
        let joined = result.join(" ");
        assert!(joined.contains("(1h)"));

        let result_narrow = build_statusline(sections, 5, &config);
        let joined_narrow = result_narrow.join(" ");
        assert!(joined_narrow.contains("5h"));
    }
}