src/render/mode/table.rs

//! 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
}