#[cfg(test)]
mod tests;
use ::std::collections::BTreeMap;
use ::std::io::Write;
const DEFAULT_TABLE_WIDTH: usize = 100;
const NEWS: &str = "┼";
const NEW: &str = "┴";
const NES: &str = "├";
const NWS: &str = "┤";
const EWS: &str = "┬";
const NE: &str = "└";
const NW: &str = "┘";
const NS: &str = "│";
const EW: &str = "─";
const ES: &str = "┌";
const WS: &str = "┐";
#[cfg(feature = "color_codes")]
static COLOR_CODE_PARESR: ::std::sync::LazyLock<::regex::Regex> =
::std::sync::LazyLock::new(|| {
::regex::Regex::new("\u{1b}\\[([0-9]+;)*[0-9]+m").expect("Regex compilation error")
});
#[derive(Clone, Debug, Default)]
pub struct AsciiTable {
max_width: Width,
columns: BTreeMap<usize, Column>,
}
impl AsciiTable {
pub fn new() -> Self {
Default::default()
}
pub fn set_max_width(&mut self, max_width: Width) -> &mut Self {
self.max_width = max_width;
self
}
pub fn max_width(&self) -> Width {
self.max_width
}
pub fn column(&mut self, index: usize) -> &mut Column {
self.columns.entry(index).or_default()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub enum Width {
Default,
Fixed(usize),
#[cfg(feature = "auto_table_width")]
Auto,
}
impl Default for Width {
fn default() -> Self {
#[cfg(not(feature = "auto_table_width"))]
{
Self::Default
}
#[cfg(feature = "auto_table_width")]
{
Self::Auto
}
}
}
#[derive(Clone, Debug)]
pub struct Column {
header: String,
align: Align,
max_width: usize,
}
impl Column {
pub fn set_header<T>(&mut self, header: T) -> &mut Self
where
T: Into<String>,
{
self.header = header.into();
self
}
pub fn header(&self) -> &str {
&self.header
}
pub fn set_align(&mut self, align: Align) -> &mut Self {
self.align = align;
self
}
pub fn align(&self) -> Align {
self.align
}
pub fn set_max_width(&mut self, max_width: usize) -> &mut Self {
self.max_width = max_width;
self
}
pub fn max_width(&self) -> usize {
self.max_width
}
}
impl Default for Column {
fn default() -> Self {
Self {
header: Default::default(),
align: Default::default(),
max_width: usize::MAX,
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Ord, PartialOrd)]
pub enum Align {
#[default]
Left,
Center,
Right,
}
impl AsciiTable {
pub fn println<T1, T2, T3>(&self, data: T1)
where
T1: AsRef<[T2]>,
T2: AsRef<[T3]>,
T3: AsRef<str>,
{
let _ = self.writeln(::std::io::stdout().lock(), data);
}
pub fn writeln<T1, T2, T3, W>(&self, mut writer: W, data: T1) -> Result<(), ::std::io::Error>
where
T1: AsRef<[T2]>,
T2: AsRef<[T3]>,
T3: AsRef<str>,
W: Write,
{
let context = self.context(&data);
self.write_top(&mut writer, &context)?;
self.write_header(&mut writer, &context)?;
self.write_data(&mut writer, &context, data)?;
self.write_bottom(&mut writer, &context)?;
writer.flush()?;
Ok(())
}
fn context<T1, T2, T3>(&self, data: T1) -> Context
where
T1: AsRef<[T2]>,
T2: AsRef<[T3]>,
T3: AsRef<str>,
{
let last_visible_header = self
.columns
.iter()
.filter(|(_, header)| !header.header.is_empty())
.last()
.map(|(&n, _)| n);
let header_count = last_visible_header.map(|n| n + 1).unwrap_or(0);
let mut column_widths = Vec::with_capacity(header_count.max(20));
column_widths.resize(header_count.max(1), 0);
for row in data.as_ref() {
let row = row.as_ref();
if row.len() > column_widths.len() {
column_widths.resize(row.len(), 0);
}
for column in 0..row.len() {
let column_max_width = self
.columns
.get(&column)
.map(|column| column.max_width)
.unwrap_or(usize::MAX);
let cell_width = row
.get(column)
.map(|cell| Self::string_width(cell.as_ref()))
.unwrap_or(0);
column_widths[column] = cell_width.min(column_max_width).max(column_widths[column]);
}
}
for (&column, header) in &self.columns {
if column < column_widths.len() {
let column_max_width = header.max_width;
let cell_width = Self::string_width(&header.header);
column_widths[column] = cell_width.min(column_max_width).max(column_widths[column]);
}
}
let table_max_width = match self.max_width {
Width::Default => DEFAULT_TABLE_WIDTH,
Width::Fixed(width) => width,
#[cfg(feature = "auto_table_width")]
Width::Auto => ::termize::dimensions()
.map(|(width, _)| width)
.unwrap_or(DEFAULT_TABLE_WIDTH),
};
let mut table_width =
column_widths.iter().sum::<usize>() + 2 + ((column_widths.len() - 1) * 3) + 2;
while table_width > table_max_width {
let mut max = 0;
let mut index = 0;
for (n, &value) in column_widths.iter().enumerate() {
if value > max {
max = value;
index = n;
}
}
column_widths[index] -= 1;
table_width -= 1;
}
Context {
column_widths,
has_header: last_visible_header.is_some(),
}
}
fn write_top<W>(&self, mut writer: W, context: &Context) -> Result<(), ::std::io::Error>
where
W: Write,
{
write!(writer, "{ES}")?;
for column in 0..context.columns() {
if column < context.columns() - 1 {
for _ in 0..context.column_widths[column] + 2 {
write!(writer, "{EW}")?;
}
write!(writer, "{EWS}")?;
} else {
for _ in 0..context.column_widths[column] + 2 {
write!(writer, "{EW}")?;
}
}
}
write!(writer, "{WS}\n")?;
Ok(())
}
fn write_header<W>(&self, mut writer: W, context: &Context) -> Result<(), ::std::io::Error>
where
W: Write,
{
if context.has_header {
write!(writer, "{NS} ")?;
for column in 0..context.columns() {
let value = self
.columns
.get(&column)
.map(|column| column.header.as_str())
.unwrap_or("");
self.write_cell(
&mut writer,
value,
context.column_widths[column],
Align::default(),
)?;
if column < context.columns() - 1 {
write!(writer, " {NS} ")?;
}
}
write!(writer, " {NS}\n")?;
write!(writer, "{NES}")?;
for column in 0..context.columns() {
if column < context.columns() - 1 {
for _ in 0..context.column_widths[column] + 2 {
write!(writer, "{EW}")?;
}
write!(writer, "{NEWS}")?;
} else {
for _ in 0..context.column_widths[column] + 2 {
write!(writer, "{EW}")?;
}
}
}
write!(writer, "{NWS}\n")?;
}
Ok(())
}
fn write_data<T1, T2, T3, W>(
&self,
mut writer: W,
context: &Context,
data: T1,
) -> Result<(), ::std::io::Error>
where
T1: AsRef<[T2]>,
T2: AsRef<[T3]>,
T3: AsRef<str>,
W: Write,
{
for row in data.as_ref() {
let row = row.as_ref();
write!(writer, "{NS} ")?;
for column in 0..context.columns() {
let value = row.get(column).map(|cell| cell.as_ref()).unwrap_or("");
let align = self
.columns
.get(&column)
.map(|column| column.align)
.unwrap_or_default();
self.write_cell(&mut writer, value, context.column_widths[column], align)?;
if column < context.columns() - 1 {
write!(writer, " {NS} ")?;
}
}
write!(writer, " {NS}\n")?;
}
Ok(())
}
fn write_bottom<W>(&self, mut writer: W, context: &Context) -> Result<(), ::std::io::Error>
where
W: Write,
{
write!(writer, "{NE}")?;
for column in 0..context.columns() {
if column < context.columns() - 1 {
for _ in 0..context.column_widths[column] + 2 {
write!(writer, "{EW}")?;
}
write!(writer, "{NEW}")?;
} else {
for _ in 0..context.column_widths[column] + 2 {
write!(writer, "{EW}")?;
}
}
}
write!(writer, "{NW}\n")?;
Ok(())
}
fn write_cell<W>(
&self,
mut writer: W,
cell: &str,
width: usize,
align: Align,
) -> Result<(), ::std::io::Error>
where
W: Write,
{
if width == 0 {
return Ok(());
}
let cell_width = Self::string_width(cell);
if cell_width <= width {
let [prepad, postpad] = match align {
Align::Left => [0, width - cell_width],
Align::Center => [
(width - cell_width) / 2,
((width - cell_width) / 2) + ((width - cell_width) % 2),
],
Align::Right => [width - cell_width, 0],
};
for _ in 0..prepad {
write!(writer, " ")?;
}
write!(writer, "{cell}")?;
for _ in 0..postpad {
write!(writer, " ")?;
}
} else {
let (cell, taken_width) = Self::string_take(cell, width - 1);
write!(writer, "{cell}+")?;
for _ in 0..width - 1 - taken_width {
write!(writer, " ")?;
}
}
Ok(())
}
fn string_width(value: &str) -> usize {
#[cfg(not(feature = "color_codes"))]
{
Self::string_width_lv2(value)
}
#[cfg(feature = "color_codes")]
{
let mut width = 0;
let mut last_end = 0;
for r#match in COLOR_CODE_PARESR.find_iter(value) {
let start = r#match.start();
let end = r#match.end();
if last_end < start {
width += Self::string_width_lv2(&value[last_end..start]);
}
last_end = end;
}
if last_end < value.len() {
width += Self::string_width_lv2(&value[last_end..]);
}
width
}
}
fn string_width_lv2(value: &str) -> usize {
#[cfg(not(feature = "wide_characters"))]
{
value.chars().count()
}
#[cfg(feature = "wide_characters")]
{
::unicode_width::UnicodeWidthStr::width(value)
}
}
#[cfg(not(feature = "color_codes"))]
fn string_take(value: &str, amount: usize) -> (&str, usize) {
Self::string_take_lv2(value, amount)
}
#[cfg(feature = "color_codes")]
fn string_take(value: &str, amount: usize) -> (String, usize) {
if amount == 0 {
return (String::new(), 0);
}
let mut result = String::with_capacity(value.len());
let mut last_end = 0;
let mut fill_to_end = false;
let mut width = 0;
for r#match in COLOR_CODE_PARESR.find_iter(value) {
let start = r#match.start();
let end = r#match.end();
if fill_to_end {
result.push_str(&value[start..end]);
} else if last_end < start {
let leading_segment = &value[last_end..start];
let leading_width = Self::string_width_lv2(leading_segment);
if width + leading_width <= amount {
result.push_str(leading_segment);
width += leading_width;
result.push_str(&value[start..end]);
} else {
let (taken_cell, taken_width) =
Self::string_take_lv2(leading_segment, amount - width);
result.push_str(taken_cell);
width += taken_width;
result.push_str(&value[start..end]);
fill_to_end = true;
}
} else {
result.push_str(&value[start..end]);
}
last_end = end;
}
if last_end < value.len() && !fill_to_end {
let (taken_cell, taken_width) =
Self::string_take_lv2(&value[last_end..], amount - width);
result.push_str(taken_cell);
width += taken_width;
}
(result, width)
}
fn string_take_lv2(value: &str, amount: usize) -> (&str, usize) {
if amount == 0 {
return ("", 0);
}
#[cfg(not(feature = "wide_characters"))]
{
let end = value
.char_indices()
.nth(amount)
.map(|(n, _)| n)
.expect("string_take exhausted its input");
(&value[..end], amount)
}
#[cfg(feature = "wide_characters")]
{
let mut width = 0;
for (n, c) in value.char_indices() {
let char_width = ::unicode_width::UnicodeWidthChar::width(c).unwrap_or(0);
if width + char_width == amount {
return (&value[..n + c.len_utf8()], amount);
} else if width + char_width > amount {
return (&value[..n], width);
} else {
width += char_width;
}
}
panic!("string_take exhausted its input")
}
}
}
#[derive(Default, Debug)]
struct Context {
column_widths: Vec<usize>, has_header: bool,
}
impl Context {
fn columns(&self) -> usize {
self.column_widths.len()
}
}