//! Column-aligned output mode.
//!
//! Keeps a running width for each named field (seeded from the first N lines)
//! and pads columns to that width on output. When the terminal is narrower
//! than the computed width we truncate with a trailing ellipsis.
use std::io::{self, Write};
use crate::render::width::unicode::display_width;
pub struct TableRenderer<W: Write> {
sink: W,
columns: Vec<Column>,
sampled: usize,
sample_budget: usize,
terminal_width: usize,
}
struct Column {
header: String,
width: usize,
}
impl<W: Write> TableRenderer<W> {
pub fn new(sink: W, headers: &[&str], terminal_width: usize) -> Self {
Self {
sink,
columns: headers
.iter()
.map(|h| Column {
header: (*h).to_string(),
width: display_width(h),
})
.collect(),
sampled: 0,
sample_budget: 50,
terminal_width,
}
}
pub fn write_header(&mut self) -> io::Result<()> {
for (i, c) in self.columns.iter().enumerate() {
if i > 0 {
self.sink.write_all(b" ")?;
}
self.sink.write_all(c.header.as_bytes())?;
}
self.sink.write_all(b"\n")
}
pub fn write_row(&mut self, values: &[&str]) -> io::Result<()> {
assert_eq!(values.len(), self.columns.len());
if self.sampled < self.sample_budget {
for (c, v) in self.columns.iter_mut().zip(values.iter()) {
c.width = c.width.max(display_width(v));
}
self.sampled += 1;
}
let available = self.terminal_width;
let mut used = 0;
for (i, (c, v)) in self.columns.iter().zip(values.iter()).enumerate() {
if i > 0 {
self.sink.write_all(b" ")?;
used = used.saturating_add(2);
}
let budget = available.saturating_sub(used);
let rendered = truncate(v, c.width.min(budget));
self.sink.write_all(rendered.as_bytes())?;
used = used.saturating_add(display_width(&rendered));
}
self.sink.write_all(b"\n")
}
}
fn truncate(value: &str, budget: usize) -> String {
if budget == 0 {
return String::new();
}
let width = display_width(value);
if width <= budget {
let mut out = value.to_string();
for _ in width..budget {
out.push(' ');
}
return out;
}
let mut out = String::with_capacity(budget);
let mut w = 0usize;
for ch in value.chars() {
let cw = display_width(&ch.to_string());
if w + cw + 1 > budget {
break;
}
out.push(ch);
w += cw;
}
while display_width(&out) < budget.saturating_sub(1) {
out.push(' ');
}
out.push('…');
out
}