use crate::{Conf, OutStyle};
use std::{collections::VecDeque,
io::{stdout, BufWriter, Write as _}};
pub trait Disp {
fn out(&self, buf: &mut Vec<u8>, conf: &Conf) -> usize;
}
impl<T: std::fmt::Display> Disp for T {
fn out(&self, buf: &mut Vec<u8>, _conf: &Conf) -> usize {
let start = buf.len();
write!(buf, "{self}").expect("write to buf");
buf.len() - start
}
}
#[derive(Clone, Copy)]
pub enum Align {
Left,
Right,
}
pub struct Table<'a, const N: usize> {
buf: Vec<u8>,
rows: VecDeque<[(usize, usize, usize); N]>,
have_header: bool,
conf: &'a Conf,
aligns: [Align; N],
margins: [&'static str; N],
last: usize,
}
impl<'a, const N: usize> Table<'a, N> {
pub fn new(conf: &'a Conf) -> Table<'a, N> {
Self { rows: VecDeque::with_capacity(32),
buf: Vec::with_capacity(1024),
conf,
have_header: false,
aligns: [Align::Right; N],
margins: [" "; N],
last: usize::MAX }
}
pub const fn align_left(mut self, col: usize) -> Self {
self.aligns[col] = Align::Left;
self
}
pub const fn margin(mut self, col: usize, margin: &'static str) -> Self {
self.margins[col] = margin;
self
}
pub const fn last(mut self, last: usize) -> Self {
self.last = last;
self
}
pub fn header(&mut self, row: [&str; N]) {
if self.conf.header {
self.last = self.last.saturating_add(1);
self.have_header = true;
let mut idxrow = [(0, 0, 0); N];
for i in 0..N {
let start = self.buf.len();
self.buf.extend(row[i].as_bytes());
idxrow[i] = (row[i].len(), start, self.buf.len());
}
self.rows.push_back(idxrow);
}
}
pub fn is_empty(&self) -> bool {
if self.have_header {
self.rows.len() == 1
} else {
self.rows.is_empty()
}
}
pub fn row(&mut self, row: [&[&dyn Disp]; N]) {
let mut idxrow = [(0, 0, 0); N];
for i in 0..N {
let start = self.buf.len();
let len = row[i].iter().map(|c| c.out(&mut self.buf, self.conf)).sum();
idxrow[i] = (len, start, self.buf.len());
}
self.rows.push_back(idxrow);
if self.rows.len() > self.last {
if self.have_header {
self.rows.swap(0, 1);
}
self.rows.pop_front();
}
}
fn flush(&self, mut out: impl std::io::Write) {
if self.is_empty() {
return;
}
let widths: [usize; N] =
std::array::from_fn(|i| self.rows.iter().fold(0, |m, r| usize::max(m, r[i].0)));
let spaces = [b' '; 128];
for row in &self.rows {
let mut first = true;
#[allow(clippy::needless_range_loop)]
for i in 0..N {
if widths[i] == 0 {
continue;
}
let (len, pos0, pos1) = row[i];
if self.conf.out == OutStyle::Tab {
if !first {
out.write_all(b"\t").unwrap_or(());
}
out.write_all(&self.buf[pos0..pos1]).unwrap_or(());
} else {
if !first {
out.write_all(self.margins[i].as_bytes()).unwrap_or(());
}
let pad = &spaces[0..usize::min(spaces.len(), widths[i] - len)];
match self.aligns[i] {
Align::Right => {
out.write_all(pad).unwrap_or(());
out.write_all(&self.buf[pos0..pos1]).unwrap_or(());
},
Align::Left => {
out.write_all(&self.buf[pos0..pos1]).unwrap_or(());
if i < N - 1 {
out.write_all(pad).unwrap_or(());
}
},
}
}
first = false;
}
out.write_all(self.conf.lineend).unwrap_or(());
}
}
}
impl<const N: usize> Drop for Table<'_, N> {
fn drop(&mut self) {
self.flush(BufWriter::new(stdout().lock()))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::parse::Ansi;
fn check<const N: usize>(mut tbl: Table<N>, expect: &str) {
let mut out = Vec::with_capacity(tbl.buf.len());
tbl.flush(&mut out);
tbl.rows.clear();
assert_eq!(expect, String::from_utf8(out).unwrap());
}
#[test]
fn last() {
let conf = Conf::from_str("emlop log --color=n -H");
let mut t = Table::<1>::new(&conf);
for i in 1..10 {
t.row([&[&format!("{i}")]]);
}
check(t, "1\n2\n3\n4\n5\n6\n7\n8\n9\n");
let mut t = Table::<1>::new(&conf).last(5);
for i in 1..10 {
t.row([&[&format!("{i}")]]);
}
check(t, "5\n6\n7\n8\n9\n");
let mut t = Table::new(&conf).last(5);
t.header(["h"]);
for i in 1..10 {
t.row([&[&format!("{i}")]]);
}
check(t, "h\n5\n6\n7\n8\n9\n");
}
#[test]
fn align_cols() {
let conf = Conf::from_str("emlop log --color=n --output=c");
let mut t = Table::<2>::new(&conf).align_left(0);
t.row([&[&"short"], &[&1]]);
t.row([&[&"looooooooooooong"], &[&1]]);
t.row([&[&"high"], &[&9999]]);
let res = "short 1\n\
looooooooooooong 1\n\
high 9999\n";
check(t, res);
}
#[test]
fn align_cols_last() {
let conf = Conf::from_str("emlop log --color=n --output=c");
let mut t = Table::<2>::new(&conf).align_left(0).last(1);
t.row([&[&"looooooooooooong"], &[&1]]);
t.row([&[&"short"], &[&1]]);
let res = "short 1\n";
check(t, res);
}
#[test]
fn align_tab() {
let conf = Conf::from_str("emlop log --color=n --output=t");
let mut t = Table::<2>::new(&conf).align_left(0);
t.row([&[&"short"], &[&1]]);
t.row([&[&"looooooooooooong"], &[&1]]);
t.row([&[&"high"], &[&9999]]);
let res = "short\t1\n\
looooooooooooong\t1\n\
high\t9999\n";
check(t, res);
}
#[test]
fn color() {
let conf = Conf::from_str("emlop log --color=y --output=c");
let mut t = Table::<2>::new(&conf).align_left(0);
t.row([&[&"123"], &[&1]]);
t.row([&[&conf.merge, &1, &conf.dur, &2, &conf.cnt, &3, &conf.clr], &[&1]]);
let res = "123 1\x1B[m\n\
\x1B[1;32m1\x1B[1;35m2\x1B[2;33m3\x1B[m 1\x1B[m\n";
let (l1, l2) = res.split_once('\n').expect("two lines");
assert_eq!(Ansi::strip(l1, 100), "123 1");
assert_eq!(Ansi::strip(l1, 100), Ansi::strip(l2, 100));
check(t, res);
}
#[test]
fn nocolor() {
let conf = Conf::from_str("emlop log --color=n --output=c");
let mut t = Table::<2>::new(&conf).align_left(0);
t.row([&[&"123"], &[&1]]);
t.row([&[&conf.merge, &1, &conf.dur, &2, &conf.cnt, &3, &conf.clr], &[&1]]);
let res = "123 1\n\
>>> 123 1\n";
check(t, res);
}
}