use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Align {
Left,
Right,
}
#[derive(Debug, Clone)]
pub struct Cell {
plain: String,
styled: String,
align: Align,
}
impl Cell {
pub fn new(plain: impl Into<String>, styled: impl Into<String>) -> Self {
Cell {
plain: plain.into(),
styled: styled.into(),
align: Align::Left,
}
}
pub fn right(plain: impl Into<String>, styled: impl Into<String>) -> Self {
Cell {
align: Align::Right,
..Cell::new(plain, styled)
}
}
pub fn plain(text: impl Into<String> + Clone) -> Self {
Cell::new(text.clone(), text)
}
fn width(&self) -> usize {
UnicodeWidthStr::width(self.plain.as_str())
}
}
pub fn columns(rows: &[Vec<Cell>], gap: usize) -> String {
let ncols = rows.iter().map(Vec::len).max().unwrap_or(0);
let mut widths = vec![0usize; ncols];
for row in rows {
for (i, cell) in row.iter().enumerate() {
widths[i] = widths[i].max(cell.width());
}
}
let sep = " ".repeat(gap);
let mut out = String::new();
for row in rows {
let mut line = String::new();
for (i, cell) in row.iter().enumerate() {
let is_last = i + 1 == row.len();
let pad = widths[i].saturating_sub(cell.width());
if i > 0 {
line.push_str(&sep);
}
match cell.align {
Align::Left => {
line.push_str(&cell.styled);
if !is_last {
line.push_str(&" ".repeat(pad));
}
}
Align::Right => {
line.push_str(&" ".repeat(pad));
line.push_str(&cell.styled);
}
}
}
out.push_str(line.trim_end());
out.push('\n');
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aligns_plain_columns() {
let rows = vec![
vec![Cell::plain("a"), Cell::plain("bbb")],
vec![Cell::plain("aaa"), Cell::plain("b")],
];
let out = columns(&rows, 2);
assert_eq!(out, "a bbb\naaa b\n");
}
#[test]
fn width_ignores_ansi_escapes() {
let styled = "\x1b[38;5;42m●\x1b[0m";
let rows = vec![
vec![Cell::new("●", styled), Cell::plain("online")],
vec![Cell::plain("X"), Cell::plain("x")],
];
let out = columns(&rows, 1);
let lines: Vec<&str> = out.lines().collect();
assert!(lines[0].ends_with("online"));
assert!(lines[1] == "X x");
}
#[test]
fn right_align_pads_left() {
let rows = vec![
vec![Cell::plain("host"), Cell::right("12ms", "12ms")],
vec![Cell::plain("h"), Cell::right("8ms", "8ms")],
];
let out = columns(&rows, 1);
assert_eq!(out, "host 12ms\nh 8ms\n");
}
#[test]
fn empty_input() {
assert_eq!(columns(&[], 2), "");
}
}