pub struct TableStyles {
pub header: &'static str,
pub cell: &'static str,
pub separator: &'static str,
pub border: &'static str,
pub reset: &'static str,
}
impl Default for TableStyles {
fn default() -> Self {
Self {
header: "\x1b[1;38;2;242;169;60m", cell: "\x1b[38;2;236;226;207m", separator: "\x1b[38;2;92;83;70m", border: "\x1b[38;2;92;83;70m", reset: "\x1b[0m",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Align {
Left,
Center,
Right,
}
#[derive(Debug, Clone)]
pub struct ColumnConfig {
pub header: String,
pub align: Align,
pub min_width: usize,
pub max_width: Option<usize>,
}
impl ColumnConfig {
pub fn new(header: &str) -> Self {
Self {
header: header.to_string(),
align: Align::Left,
min_width: header.len(),
max_width: None,
}
}
pub fn with_align(mut self, align: Align) -> Self {
self.align = align;
self
}
}
pub fn render_table(
headers: &[&str],
rows: &[Vec<String>],
max_width: Option<usize>,
styles: Option<&TableStyles>,
) -> String {
if headers.is_empty() {
return String::new();
}
let styles = styles.unwrap_or(&TABLE_STYLES_DEFAULT);
let col_count = headers.len();
let mut col_widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
for row in rows {
for (i, cell) in row.iter().enumerate() {
if i >= col_count {
break;
}
let cell_len = cell.chars().count();
if cell_len > col_widths[i] {
col_widths[i] = cell_len;
}
}
}
for w in col_widths.iter_mut() {
*w = (*w).max(3);
}
if let Some(max_w) = max_width {
let separator_width = (col_count + 1) * 3; let total_content: usize = col_widths.iter().sum::<usize>() + separator_width;
if total_content > max_w {
let available = max_w.saturating_sub(separator_width);
let scale = available as f64 / (total_content - separator_width) as f64;
for w in col_widths.iter_mut() {
*w = ((*w as f64) * scale).max(3.0) as usize;
}
}
}
let mut out = String::new();
render_horizontal_line(&mut out, &col_widths, &styles);
let header_cells: Vec<String> = headers.iter().map(|h| h.to_string()).collect();
render_row(&mut out, &header_cells, &col_widths, &styles, true);
render_separator(&mut out, &col_widths, &styles);
for row in rows {
let cells: Vec<String> = row.iter().map(|c| c.to_string()).collect();
render_row(&mut out, &cells, &col_widths, &styles, false);
}
render_horizontal_line(&mut out, &col_widths, &styles);
out
}
pub fn render_table_with_config(
columns: &[ColumnConfig],
rows: &[Vec<String>],
max_width: Option<usize>,
styles: Option<&TableStyles>,
) -> String {
let headers: Vec<&str> = columns.iter().map(|c| c.header.as_str()).collect();
render_table(&headers, rows, max_width, styles)
}
static TABLE_STYLES_DEFAULT: TableStyles = TableStyles {
header: "\x1b[1;38;2;242;169;60m",
cell: "\x1b[38;2;236;226;207m",
separator: "\x1b[38;2;92;83;70m",
border: "\x1b[38;2;92;83;70m",
reset: "\x1b[0m",
};
fn render_horizontal_line(out: &mut String, widths: &[usize], s: &TableStyles) {
out.push_str(s.border);
for &w in widths {
out.push_str("├");
out.push_str(&"─".repeat(w + 2));
}
out.push_str("┤");
out.push_str(s.reset);
out.push('\n');
}
fn render_separator(out: &mut String, widths: &[usize], s: &TableStyles) {
out.push_str(s.border);
for &w in widths {
out.push_str("├");
out.push_str(&"─".repeat(w + 2));
}
out.push_str("┤");
out.push_str(s.reset);
out.push('\n');
}
fn render_row(
out: &mut String,
cells: &[String],
widths: &[usize],
s: &TableStyles,
is_header: bool,
) {
let style = if is_header { s.header } else { s.cell };
out.push_str(s.border);
out.push_str("│");
for (i, cell) in cells.iter().enumerate() {
if i >= widths.len() {
break;
}
let w = widths[i];
let display = truncate_cell(cell, w);
out.push(' ');
out.push_str(style);
out.push_str(&display);
let display_len = display.chars().count();
if display_len < w {
out.push_str(&" ".repeat(w - display_len));
}
out.push_str(s.reset);
out.push(' ');
out.push_str(s.border);
out.push_str("│");
}
for i in cells.len()..widths.len() {
let w = widths[i];
out.push(' ');
out.push_str(&" ".repeat(w));
out.push(' ');
out.push_str(s.border);
out.push_str("│");
}
out.push_str(s.reset);
out.push('\n');
}
fn truncate_cell(text: &str, width: usize) -> String {
if width == 0 {
return String::new();
}
if text.chars().count() <= width {
return text.to_string();
}
if width <= 1 {
return "…".to_string();
}
let mut out: String = text.chars().take(width - 1).collect();
out.push('…');
out
}