use ansi_str::AnsiStr;
use unicode_width::UnicodeWidthStr;
use crate::commands::list::columns::ColumnKind;
#[derive(Clone, Debug)]
pub struct StatuslineSegment {
pub content: String,
pub priority: u8,
pub kind: Option<ColumnKind>,
}
impl StatuslineSegment {
pub fn new(content: String, priority: u8) -> Self {
Self {
content,
priority,
kind: None,
}
}
pub fn from_column(content: String, kind: ColumnKind) -> Self {
Self {
content,
priority: kind.priority(),
kind: Some(kind),
}
}
pub fn width(&self) -> usize {
self.content.ansi_strip().width()
}
pub fn join(segments: &[Self]) -> String {
segments
.iter()
.map(|s| s.content.as_str())
.collect::<Vec<_>>()
.join(" ")
}
pub fn total_width(segments: &[Self]) -> usize {
if segments.is_empty() {
return 0;
}
let content_width: usize = segments.iter().map(|s| s.width()).sum();
let separator_width = (segments.len() - 1) * 2;
content_width + separator_width
}
pub fn fit_to_width(segments: Vec<Self>, max_width: usize) -> Vec<Self> {
if segments.is_empty() {
return segments;
}
if Self::total_width(&segments) <= max_width {
return segments;
}
let mut indexed: Vec<_> = segments.into_iter().enumerate().collect();
while indexed.len() > 1 && Self::total_width_indexed(&indexed) > max_width {
let drop_idx = indexed
.iter()
.enumerate()
.max_by(|(i, (_, a)), (j, (_, b))| {
a.priority.cmp(&b.priority).then_with(|| i.cmp(j))
})
.map(|(i, _)| i)
.unwrap();
indexed.remove(drop_idx);
}
indexed.sort_by_key(|(idx, _)| *idx);
indexed.into_iter().map(|(_, seg)| seg).collect()
}
fn total_width_indexed(segments: &[(usize, Self)]) -> usize {
if segments.is_empty() {
return 0;
}
let content_width: usize = segments.iter().map(|(_, s)| s.width()).sum();
let separator_width = (segments.len() - 1) * 2;
content_width + separator_width
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_statusline_segment_width() {
let seg = StatuslineSegment::new("hello".to_string(), 1);
assert_eq!(seg.width(), 5);
use color_print::cformat;
let styled = StatuslineSegment::new(cformat!("<green>green</>"), 1);
assert_eq!(styled.width(), 5);
}
#[test]
fn test_statusline_segment_join() {
let segments = vec![
StatuslineSegment::new("a".to_string(), 1),
StatuslineSegment::new("b".to_string(), 2),
StatuslineSegment::new("c".to_string(), 3),
];
assert_eq!(StatuslineSegment::join(&segments), "a b c");
}
#[test]
fn test_statusline_segment_total_width() {
let segments = vec![
StatuslineSegment::new("abc".to_string(), 1), StatuslineSegment::new("de".to_string(), 2), ];
assert_eq!(StatuslineSegment::total_width(&segments), 7);
assert_eq!(StatuslineSegment::total_width(&[]), 0);
let single = vec![StatuslineSegment::new("test".to_string(), 1)];
assert_eq!(StatuslineSegment::total_width(&single), 4);
}
#[test]
fn test_statusline_segment_fit_to_width_no_truncation_needed() {
let segments = vec![
StatuslineSegment::new("abc".to_string(), 1),
StatuslineSegment::new("de".to_string(), 2),
];
let result = StatuslineSegment::fit_to_width(segments.clone(), 10);
assert_eq!(result.len(), 2);
assert_eq!(StatuslineSegment::join(&result), "abc de");
}
#[test]
fn test_statusline_segment_fit_to_width_drops_low_priority() {
let segments = vec![
StatuslineSegment::new("important".to_string(), 1), StatuslineSegment::new("optional".to_string(), 10), ];
let result = StatuslineSegment::fit_to_width(segments, 12);
assert_eq!(result.len(), 1);
assert_eq!(result[0].content, "important");
}
#[test]
fn test_statusline_segment_fit_to_width_preserves_order() {
let segments = vec![
StatuslineSegment::new("A".to_string(), 5), StatuslineSegment::new("B".to_string(), 1), StatuslineSegment::new("C".to_string(), 3), ];
let result = StatuslineSegment::fit_to_width(segments, 10);
assert_eq!(StatuslineSegment::join(&result), "A B C");
}
#[test]
fn test_statusline_segment_fit_to_width_drops_multiple() {
let segments = vec![
StatuslineSegment::new("dir".to_string(), 0), StatuslineSegment::new("branch".to_string(), 1), StatuslineSegment::new("status".to_string(), 2), StatuslineSegment::new("url".to_string(), 8), StatuslineSegment::new("model".to_string(), 1), ];
let result = StatuslineSegment::fit_to_width(segments, 15);
let contents: Vec<_> = result.iter().map(|s| s.content.as_str()).collect();
assert!(!contents.contains(&"url"), "Should have dropped url");
}
#[test]
fn test_statusline_segment_from_column() {
let seg = StatuslineSegment::from_column("test".to_string(), ColumnKind::Branch);
assert_eq!(seg.content, "test");
assert_eq!(seg.priority, ColumnKind::Branch.priority());
assert_eq!(seg.kind, Some(ColumnKind::Branch));
}
#[test]
fn test_statusline_segment_fit_to_width_keeps_highest_priority_when_too_wide() {
let segments = vec![
StatuslineSegment::new("very_long_directory_path".to_string(), 0), StatuslineSegment::new("branch".to_string(), 1), ];
let result = StatuslineSegment::fit_to_width(segments, 5);
assert_eq!(result.len(), 1, "Should keep at least one segment");
assert_eq!(
result[0].content, "very_long_directory_path",
"Should keep highest-priority segment even if too wide"
);
}
#[test]
fn test_statusline_segment_fit_to_width_prefers_priority_over_width() {
let segments = vec![
StatuslineSegment::new("very_important_segment".to_string(), 0), StatuslineSegment::new("x".to_string(), 10), StatuslineSegment::new("y".to_string(), 10), ];
let result = StatuslineSegment::fit_to_width(segments, 25);
assert!(
result.iter().any(|s| s.content == "very_important_segment"),
"Should keep the high-priority segment"
);
}
}