use colored::*;
use std::cmp;
pub struct Table {
headers: Vec<String>,
rows: Vec<Vec<String>>,
}
impl Table {
pub fn new(headers: &[&str]) -> Self {
Self {
headers: headers.iter().map(|s| s.to_string()).collect(),
rows: Vec::new(),
}
}
pub fn add_row(&mut self, row: Vec<String>) {
if row.len() == self.headers.len() {
self.rows.push(row);
}
}
pub fn print(&self) {
if self.headers.is_empty() {
return;
}
let term = console::Term::stdout();
let (_term_height, term_width) = term.size();
let max_width = term_width as usize;
let mut col_widths = vec![0; self.headers.len()];
for (i, header) in self.headers.iter().enumerate() {
col_widths[i] = cmp::max(col_widths[i], header.chars().count());
}
for row in &self.rows {
for (i, cell) in row.iter().enumerate() {
let clean = sanitize_content(cell);
let clean_len = strip_ansi(&clean).chars().count();
col_widths[i] = cmp::max(col_widths[i], clean_len);
}
}
let overhead = 3 + 3 * self.headers.len();
let total_content_width: usize = col_widths.iter().sum();
let total_required = overhead + total_content_width;
if total_required > max_width {
let available_content_width = max_width.saturating_sub(overhead);
let mut current_width = total_content_width;
let mut changed = true;
while current_width > available_content_width && changed {
changed = false;
let mut max_idx = 0;
let mut max_val = 0;
for (i, &w) in col_widths.iter().enumerate() {
if w > max_val {
max_val = w;
max_idx = i;
}
}
if max_val > 8 {
col_widths[max_idx] -= 1;
current_width -= 1;
changed = true;
}
}
}
let top_left = "┌";
let top_right = "┐";
let bottom_left = "└";
let bottom_right = "┘";
let horizontal = "─";
let vertical = "│";
let top_sep = "┬";
let bottom_sep = "┴";
let mid_left = "├";
let mid_right = "┤";
let mid_sep = "┼";
let make_sep = |left: &str, mid: &str, right: &str| -> String {
let mut s = String::new();
s.push_str(" "); s.push_str(left);
for (i, width) in col_widths.iter().enumerate() {
s.push_str(&horizontal.repeat(width + 2));
if i < col_widths.len() - 1 {
s.push_str(mid);
}
}
s.push_str(right);
s
};
println!("{}", make_sep(top_left, top_sep, top_right));
print!(" {}", vertical);
for (i, header) in self.headers.iter().enumerate() {
let width = col_widths[i];
let truncated = truncate(header, width);
let visible_len = truncated.chars().count();
let padding = width.saturating_sub(visible_len);
print!(" {} {}{}", truncated.bold(), " ".repeat(padding), vertical);
}
println!();
println!("{}", make_sep(mid_left, mid_sep, mid_right));
for row in &self.rows {
print!(" {}", vertical);
for (i, cell) in row.iter().enumerate() {
let clean_raw = sanitize_content(cell);
let width = col_widths[i];
let truncated_cow = console::truncate_str(&clean_raw, width, "...");
let truncated = truncated_cow.to_string();
let clean_trunc = strip_ansi(&truncated);
let visible_len = clean_trunc.chars().count();
let padding = width.saturating_sub(visible_len);
print!(" {} {}{}", truncated, " ".repeat(padding), vertical);
}
println!();
}
println!("{}", make_sep(bottom_left, bottom_sep, bottom_right));
}
}
fn truncate(s: &str, max_width: usize) -> String {
if s.chars().count() > max_width {
let mut result: String = s.chars().take(max_width.saturating_sub(3)).collect();
result.push_str("...");
result
} else {
s.to_string()
}
}
fn sanitize_content(s: &str) -> String {
s.chars()
.map(|c| match c {
'\n' | '\r' | '\t' => ' ',
_ => c,
})
.collect()
}
fn strip_ansi(s: &str) -> String {
let mut result = String::new();
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1b' {
if let Some(&'[') = chars.peek() {
chars.next();
for c in chars.by_ref() {
if c == 'm' {
break;
}
}
}
} else {
result.push(c);
}
}
result
}