fn visible_len(s: &str) -> usize {
let mut len = 0usize;
let mut in_escape = false;
for ch in s.chars() {
if in_escape {
if ch.is_ascii_alphabetic() {
in_escape = false;
}
continue;
}
if ch == '\x1b' {
in_escape = true;
continue;
}
len += 1;
}
len
}
fn pad_to_visible(s: &str, target: usize) -> String {
let vis = visible_len(s);
if vis >= target {
s.to_string()
} else {
format!("{}{}", s, " ".repeat(target - vis))
}
}
pub struct GridCard {
pub lines: Vec<String>,
}
impl GridCard {
pub fn visible_width(&self) -> usize {
self.lines.iter().map(|l| visible_len(l)).max().unwrap_or(0)
}
}
pub fn render_grid(cards: &[GridCard], max_cols: usize, gutter: usize) -> String {
if cards.is_empty() {
return String::new();
}
let cell_width = cards.iter().map(|c| c.visible_width()).max().unwrap_or(0);
if cell_width == 0 {
return String::new();
}
let cols_per_row = ((max_cols + gutter) / (cell_width + gutter)).max(1);
let mut out = String::new();
for row_cards in cards.chunks(cols_per_row) {
let max_height = row_cards.iter().map(|c| c.lines.len()).max().unwrap_or(0);
for line_idx in 0..max_height {
for (card_idx, card) in row_cards.iter().enumerate() {
let line = card.lines.get(line_idx).map(|s| s.as_str()).unwrap_or("");
out.push_str(&pad_to_visible(line, cell_width));
if card_idx + 1 < row_cards.len() {
out.push_str(&" ".repeat(gutter));
}
}
out.push('\n');
}
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn visible_len_strips_ansi() {
assert_eq!(visible_len("\x1b[38;2;255;0;0mHello\x1b[0m"), 5);
assert_eq!(visible_len("Plain text"), 10);
assert_eq!(visible_len(""), 0);
}
#[test]
fn pad_to_visible_pads_ansi_string() {
let s = "\x1b[31mHi\x1b[0m"; let padded = pad_to_visible(s, 5);
assert_eq!(visible_len(&padded), 5);
assert!(padded.ends_with(" ")); }
#[test]
fn grid_arranges_cards_side_by_side() {
let cards = vec![
GridCard {
lines: vec!["AAAA".into(), "AAAA".into()],
},
GridCard {
lines: vec!["BBBB".into(), "BBBB".into()],
},
GridCard {
lines: vec!["CCCC".into()],
}, ];
let output = render_grid(&cards, 20, 2);
let lines: Vec<&str> = output.lines().collect();
assert!(lines[0].contains("AAAA"));
assert!(lines[0].contains("BBBB"));
assert!(lines[0].contains("CCCC"));
assert!(lines[1].contains("AAAA"));
assert!(lines[1].contains("BBBB"));
}
#[test]
fn grid_wraps_to_multiple_rows() {
let cards = vec![
GridCard {
lines: vec!["AAAA".into()],
},
GridCard {
lines: vec!["BBBB".into()],
},
GridCard {
lines: vec!["CCCC".into()],
},
];
let output = render_grid(&cards, 10, 2);
let lines: Vec<&str> = output.lines().collect();
assert!(lines[0].contains("AAAA"));
assert!(lines[0].contains("BBBB"));
assert!(lines[2].contains("CCCC"));
}
#[test]
fn empty_cards_returns_empty() {
assert_eq!(render_grid(&[], 80, 2), "");
}
}