use std::fmt::Write as _;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TableStyle {
Box,
Pipe,
}
impl TableStyle {
fn box_border_chars(self) -> Option<BoxBorderChars> {
match self {
TableStyle::Box => Some(BoxBorderChars {
top_left: '┌',
top_mid: '┬',
top_right: '┐',
middle_left: '├',
middle_mid: '┼',
middle_right: '┤',
bottom_left: '└',
bottom_mid: '┴',
bottom_right: '┘',
vertical: '│',
horizontal: '─',
}),
TableStyle::Pipe => None,
}
}
}
struct BoxBorderChars {
top_left: char,
top_mid: char,
top_right: char,
middle_left: char,
middle_mid: char,
middle_right: char,
bottom_left: char,
bottom_mid: char,
bottom_right: char,
vertical: char,
horizontal: char,
}
pub fn render_table(
headers: &[&str],
rows: &[Vec<String>],
style: TableStyle,
indent: &str,
) -> String {
let mut widths: Vec<usize> = headers.iter().map(|header| visible_width(header)).collect();
for row in rows {
for (index, cell) in row.iter().enumerate() {
if index >= widths.len() {
widths.push(visible_width(cell));
} else {
widths[index] = widths[index].max(visible_width(cell));
}
}
}
match style {
TableStyle::Box => render_box_table(headers, rows, &widths, indent),
TableStyle::Pipe => render_pipe_table(headers, rows, &widths, indent),
}
}
fn render_box_table(
headers: &[&str],
rows: &[Vec<String>],
widths: &[usize],
indent: &str,
) -> String {
let borders = TableStyle::Box
.box_border_chars()
.expect("box border chars");
let mut out = String::new();
let header_count = headers.len();
writeln!(
out,
"{}{}",
indent,
horizontal_rule(
&borders,
widths,
borders.top_left,
borders.top_mid,
borders.top_right
)
)
.expect("write table top border");
writeln!(
out,
"{}{}",
indent,
render_box_row(headers, widths, &borders)
)
.expect("write table header");
writeln!(
out,
"{}{}",
indent,
horizontal_rule(
&borders,
widths,
borders.middle_left,
borders.middle_mid,
borders.middle_right
)
)
.expect("write table header separator");
for row in rows {
writeln!(
out,
"{}{}",
indent,
render_box_row_slice(row, widths, &borders, header_count)
)
.expect("write table row");
}
writeln!(
out,
"{}{}",
indent,
horizontal_rule(
&borders,
widths,
borders.bottom_left,
borders.bottom_mid,
borders.bottom_right
)
)
.expect("write table bottom border");
out
}
fn render_box_row(headers: &[&str], widths: &[usize], borders: &BoxBorderChars) -> String {
let cells: Vec<String> = headers.iter().map(|header| (*header).to_string()).collect();
render_box_row_slice(&cells, widths, borders, headers.len())
}
fn render_box_row_slice(
cells: &[String],
widths: &[usize],
borders: &BoxBorderChars,
column_count: usize,
) -> String {
let mut row = String::new();
row.push(borders.vertical);
row.push(' ');
for (index, width) in widths.iter().enumerate().take(column_count) {
if index > 0 {
row.push(' ');
row.push(borders.vertical);
row.push(' ');
}
let cell = cells.get(index).map(String::as_str).unwrap_or("-");
let _ = write!(row, "{cell}");
let padding = width.saturating_sub(visible_width(cell));
row.push_str(&" ".repeat(padding));
}
row.push(' ');
row.push(borders.vertical);
row
}
fn horizontal_rule(
borders: &BoxBorderChars,
widths: &[usize],
left: char,
middle: char,
right: char,
) -> String {
let mut line = String::new();
line.push(left);
for (index, width) in widths.iter().enumerate() {
if index > 0 {
line.push(middle);
}
line.push_str(&borders.horizontal.to_string().repeat(width + 2));
}
line.push(right);
line
}
fn render_pipe_table(
headers: &[&str],
rows: &[Vec<String>],
widths: &[usize],
indent: &str,
) -> String {
let mut out = String::new();
let separator = pipe_separator(widths);
writeln!(out, "{indent}{separator}").expect("write pipe table top separator");
writeln!(out, "{indent}{}", render_pipe_row(headers, widths)).expect("write pipe table header");
writeln!(out, "{indent}{separator}").expect("write pipe table header separator");
for row in rows {
writeln!(out, "{indent}{}", render_pipe_row_slice(row, widths))
.expect("write pipe table row");
}
writeln!(out, "{indent}{separator}").expect("write pipe table bottom separator");
out
}
fn pipe_separator(widths: &[usize]) -> String {
let mut line = String::new();
for width in widths {
line.push('+');
line.push_str(&"-".repeat(width + 2));
}
line.push('+');
line
}
fn render_pipe_row(headers: &[&str], widths: &[usize]) -> String {
let cells: Vec<String> = headers.iter().map(|header| (*header).to_string()).collect();
render_pipe_row_slice(&cells, widths)
}
fn render_pipe_row_slice(cells: &[String], widths: &[usize]) -> String {
let mut row = String::new();
row.push('|');
for (index, width) in widths.iter().enumerate() {
row.push(' ');
let cell = cells.get(index).map(String::as_str).unwrap_or("-");
let _ = write!(row, "{cell}");
let padding = width.saturating_sub(visible_width(cell));
row.push_str(&" ".repeat(padding));
row.push(' ');
row.push('|');
}
row
}
fn visible_width(value: &str) -> usize {
strip_ansi(value).chars().count()
}
fn strip_ansi(value: &str) -> String {
let mut out = String::with_capacity(value.len());
let mut chars = value.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\u{1b}' && chars.peek() == Some(&'[') {
let _ = chars.next();
for next in chars.by_ref() {
if ('@'..='~').contains(&next) {
break;
}
}
continue;
}
out.push(ch);
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn box_table_renders_with_unicode_borders() {
let rendered = render_table(
&["PORT", "PID"],
&[vec![
"3000".to_string(),
"\u{1b}[32m42\u{1b}[0m".to_string(),
]],
TableStyle::Box,
" ",
);
assert!(rendered.contains("┌"));
assert!(rendered.contains("│ PORT"));
assert!(rendered.contains("3000"));
}
#[test]
fn pipe_table_renders_with_dashed_separators() {
let rendered = render_table(&["NAME"], &[vec!["xbp".to_string()]], TableStyle::Pipe, "");
assert!(rendered.starts_with("+"));
assert!(rendered.contains("| NAME"));
assert!(rendered.contains("xbp"));
}
}