fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut in_escape = false;
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\x1b' {
if chars.next() == Some('[') {
in_escape = true;
}
} else if in_escape {
if c.is_ascii_alphabetic() {
in_escape = false;
}
} else {
out.push(c);
}
}
out
}
pub fn display_width(s: &str) -> usize {
use unicode_width::UnicodeWidthStr;
strip_ansi(s).as_str().width()
}
pub fn terminal_width() -> usize {
use terminal_size::{terminal_size, Width};
terminal_size()
.map(|(Width(w), _)| w as usize)
.unwrap_or(120)
}
#[derive(Clone)]
pub struct Cell {
pub rendered: String,
pub width: usize,
}
impl Cell {
pub fn new(rendered: impl Into<String>) -> Self {
let rendered = rendered.into();
let width = display_width(&rendered);
Self { rendered, width }
}
}
impl From<String> for Cell {
fn from(s: String) -> Self {
Self::new(s)
}
}
impl From<&str> for Cell {
fn from(s: &str) -> Self {
Self::new(s)
}
}
const GAP: usize = 2;
fn pad_cell(cell: &Cell, target_width: usize) -> String {
let padding = target_width.saturating_sub(cell.width);
format!("{}{}", cell.rendered, " ".repeat(padding))
}
fn truncate(s: &str, max_width: usize) -> String {
use unicode_width::UnicodeWidthChar;
if max_width == 0 {
return String::new();
}
let ellipsis_width = 1; let budget = max_width.saturating_sub(ellipsis_width);
let mut width = 0;
let mut result = String::new();
for c in s.chars() {
let cw = c.width().unwrap_or(0);
if width + cw > budget {
result.push('…');
return result;
}
result.push(c);
width += cw;
}
result }
pub struct Table {
rows: Vec<Vec<Cell>>,
term_width: usize,
}
impl Table {
pub fn new(term_width: usize) -> Self {
Self {
rows: Vec::new(),
term_width,
}
}
pub fn push(&mut self, cells: Vec<Cell>) {
self.rows.push(cells);
}
pub fn print(&self) {
if self.rows.is_empty() {
return;
}
let ncols = self.rows[0].len();
if ncols == 0 {
return;
}
let mut col_widths: Vec<usize> = vec![0; ncols];
for row in &self.rows {
for (i, cell) in row.iter().enumerate() {
if col_widths[i] < cell.width {
col_widths[i] = cell.width;
}
}
}
let fixed_width: usize = col_widths[..ncols - 1].iter().map(|w| w + GAP).sum();
let title_budget = self.term_width.saturating_sub(fixed_width);
for row in &self.rows {
let mut line = String::new();
for (i, cell) in row.iter().enumerate() {
if i == ncols - 1 {
let plain = strip_ansi(&cell.rendered);
line.push_str(&truncate(&plain, title_budget));
} else {
line.push_str(&pad_cell(cell, col_widths[i]));
line.push_str(&" ".repeat(GAP));
}
}
println!("{line}");
}
}
}