mod styles;
use std::io;
use styles::STYLES;
use termcolor::{ColorSpec, WriteColor};
pub use termcolor::Color;
#[cfg(test)]
mod tests;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TableStyle {
Simple,
Grid,
FancyGrid,
Clean,
Round,
Banner,
Block,
Amiga,
Minimal,
Compact,
Markdown,
Dotted,
Heavy,
Neon,
}
impl TableStyle {
fn config(&self) -> Option<&'static TableStyleConfig> {
match self {
TableStyle::Grid => Some(&STYLES[1]),
TableStyle::FancyGrid => Some(&STYLES[2]),
TableStyle::Clean => Some(&STYLES[3]),
TableStyle::Round => Some(&STYLES[4]),
TableStyle::Banner => Some(&STYLES[5]),
TableStyle::Block => Some(&STYLES[6]),
TableStyle::Minimal => Some(&STYLES[8]),
TableStyle::Compact => Some(&STYLES[9]),
TableStyle::Markdown => Some(&STYLES[10]),
TableStyle::Dotted => Some(&STYLES[11]),
TableStyle::Heavy => Some(&STYLES[12]),
TableStyle::Neon => Some(&STYLES[13]),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Alignment {
Left,
Center,
Right,
}
#[derive(Debug)]
struct LineStyle {
begin: &'static str,
hline: &'static str,
sep: &'static str,
end: &'static str,
}
#[derive(Debug)]
struct TableStyleConfig {
top: LineStyle,
below_header: LineStyle,
bottom: LineStyle,
row: LineStyle,
}
#[derive(Clone, Debug)]
struct ColumnDef {
header: String,
alignment: Alignment,
}
#[derive(Clone, Copy, Debug)]
struct ColumnDim {
effective_content_width: usize,
total_width_for_drawing: usize,
alignment: Alignment,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CellStyle {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub padding: usize,
pub decimal_places: Option<usize>,
pub thousand_separator: bool,
pub background_color: Option<Color>,
pub foreground_color: Option<Color>,
pub align: Option<Alignment>,
}
impl CellStyle {
pub fn new() -> Self {
Self {
bold: false,
italic: false,
underline: false,
padding: 1, decimal_places: None,
thousand_separator: false,
background_color: None,
foreground_color: None,
align: None,
}
}
}
impl Default for CellStyle {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cell {
pub content: String,
pub style: CellStyle,
}
impl Cell {
pub fn new(content: &str) -> Self {
Self {
content: content.to_string(),
style: CellStyle::default(), }
}
fn formatted_content(&self) -> String {
if self.style.decimal_places.is_none() && !self.style.thousand_separator {
return self.content.clone();
}
if let Ok(num) = self.content.trim().parse::<f64>() {
let mut formatted_num_str = if let Some(dp) = self.style.decimal_places {
format!("{num:.dp$}")
} else {
num.to_string() };
if self.style.thousand_separator {
let parts: Vec<&str> = formatted_num_str.splitn(2, '.').collect();
let mut integer_part_original = parts[0].to_string();
let decimal_part_str = if parts.len() > 1 { Some(parts[1]) } else { None };
let sign = if integer_part_original.starts_with('-') {
integer_part_original.remove(0);
"-"
} else { "" };
if !integer_part_original.is_empty() {
let mut separated_digits = String::new();
let digits_len = integer_part_original.len();
let mut first_group_len = digits_len % 3;
if first_group_len == 0 && digits_len > 0 {
first_group_len = 3;
}
if digits_len > 0 { separated_digits.push_str(&integer_part_original[..first_group_len]);
for chunk_bytes in integer_part_original.as_bytes()[first_group_len..].chunks(3) {
separated_digits.push(',');
separated_digits.push_str(std::str::from_utf8(chunk_bytes).unwrap_or(""));
}
integer_part_original = separated_digits;
}
}
let final_integer_part = format!("{sign}{integer_part_original}");
formatted_num_str = if let Some(dp_str) = decimal_part_str {
format!("{final_integer_part}.{dp_str}")
} else {
final_integer_part
};
}
return formatted_num_str;
}
self.content.clone()
}
}
#[derive(Debug)]
pub struct Table {
column_defs: Vec<ColumnDef>,
rows: Vec<Vec<Cell>>,
style: TableStyle,
column_dims: Option<Vec<ColumnDim>>,
row_heights: Option<Vec<usize>>, }
impl Table {
pub fn new(style: TableStyle) -> Self {
Self {
column_defs: Vec::new(),
rows: Vec::new(),
style,
column_dims: None,
row_heights: None,
}
}
#[cfg(feature = "csv")]
pub fn from_csv(file_path: &str) -> Result<Self, Box<dyn std::error::Error>> {
use std::fs::File;
use csv::Reader;
let file = File::open(file_path)?;
let mut reader = Reader::from_reader(file);
let mut table = Table::new(TableStyle::Grid);
if let Ok(headers) = reader.headers() {
for header in headers.iter() {
table.add_column(header, Alignment::Left);
}
}
for result in reader.records() {
let record = result?;
let cells: Vec<Cell> = record.iter().map(|field| Cell::new(field)).collect();
if cells.len() == table.column_defs.len() {
table.add_row(cells);
}
}
Ok(table)
}
pub fn add_column(&mut self, header: &str, alignment: Alignment) {
self.column_defs.push(ColumnDef {
header: header.to_string(),
alignment,
});
self.invalidate_dimensions();
}
pub fn add_row(&mut self, row: Vec<Cell>) {
assert_eq!(
self.column_defs.len(), row.len(),
"Row length ({}) must match number of columns ({})", row.len(), self.column_defs.len()
);
self.rows.push(row);
self.invalidate_dimensions();
}
pub fn sort_by_column(&mut self, column_index: usize, ascending: bool) {
self.rows.sort_by(|a, b| {
let ord = a[column_index].content.cmp(&b[column_index].content);
if ascending {
ord
} else {
ord.reverse()
}
});
}
pub fn filter_rows<F>(&self, predicate: F) -> Self
where
F: Fn(&Vec<Cell>) -> bool,
{
let filtered_rows = self.rows.iter().filter(|&x| predicate(x)).cloned().collect();
Table {
column_defs: self.column_defs.clone(),
rows: filtered_rows,
style: self.style,
column_dims: None,
row_heights: None,
}
}
pub fn get_column_count(&self) -> usize {
self.column_defs.len()
}
pub fn sum_column(&self, column_index: usize) -> Option<f64> {
if column_index >= self.column_defs.len() || self.rows.is_empty() {
return None;
}
let mut sum = 0.0;
let mut has_valid_numbers = false;
for row in &self.rows {
if let Ok(num) = row[column_index].content.trim().parse::<f64>() {
sum += num;
has_valid_numbers = true;
}
}
if has_valid_numbers { Some(sum) } else { None }
}
pub fn average_column(&self, column_index: usize) -> Option<f64> {
if let Some(sum) = self.sum_column(column_index) {
let count = self.rows.iter()
.filter(|row| row[column_index].content.trim().parse::<f64>().is_ok())
.count();
if count > 0 {
Some(sum / count as f64)
} else {
None
}
} else {
None
}
}
pub fn min_column(&self, column_index: usize) -> Option<f64> {
if column_index >= self.column_defs.len() || self.rows.is_empty() {
return None;
}
self.rows.iter()
.filter_map(|row| row[column_index].content.trim().parse::<f64>().ok())
.min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
pub fn max_column(&self, column_index: usize) -> Option<f64> {
if column_index >= self.column_defs.len() || self.rows.is_empty() {
return None;
}
self.rows.iter()
.filter_map(|row| row[column_index].content.trim().parse::<f64>().ok())
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
}
pub fn group_by_column_with_subtotals(&mut self, column_index: usize) {
let mut grouped_rows: Vec<Vec<Cell>> = Vec::new();
let mut current_group: Vec<Vec<Cell>> = Vec::new();
let mut current_value: Option<String> = None;
for row in &self.rows {
let value = &row[column_index].content;
if current_value.is_none() || current_value.as_ref().unwrap() != value {
if !current_group.is_empty() {
let subtotal_row = self.calculate_subtotal(¤t_group);
grouped_rows.push(subtotal_row);
}
current_value = Some(value.clone());
grouped_rows.push(row.clone());
current_group = Vec::new();
} else {
grouped_rows.push(row.clone());
}
current_group.push(row.clone());
}
if !current_group.is_empty() {
let subtotal_row = self.calculate_subtotal(¤t_group);
grouped_rows.push(subtotal_row);
}
self.rows = grouped_rows;
}
fn calculate_subtotal(&self, group: &[Vec<Cell>]) -> Vec<Cell> {
let mut subtotal_row: Vec<Cell> = Vec::new();
for (i, _column) in self.column_defs.iter().enumerate() {
if i == 0 {
subtotal_row.push(Cell::new("Subtotal"));
} else if group
.iter()
.all(|row| row[i].content.parse::<f64>().is_ok())
{
let subtotal: f64 = group
.iter()
.map(|row| row[i].content.parse::<f64>().unwrap())
.sum();
subtotal_row.push(Cell::new(&subtotal.to_string()));
} else {
subtotal_row.push(Cell::new(""));
}
}
subtotal_row
}
pub fn print(&mut self) -> std::io::Result<()> {
let mut stdout = termcolor::StandardStream::stdout(termcolor::ColorChoice::Auto);
self.print_to_writer(&mut stdout)
}
pub fn print_to_writer<W: WriteColor>(&mut self, writer: &mut W) -> std::io::Result<()> {
self.ensure_dimensions();
match self.style {
TableStyle::Amiga => self.print_amiga_color(writer),
_ => {
if let Some(style_cfg_ref) = self.style.config() {
self.print_styled(writer, style_cfg_ref)
} else {
self.print_simple(writer)
}
}
}
}
pub fn print_color<W: WriteColor>(&mut self, writer: &mut W) -> std::io::Result<()> {
self.ensure_dimensions();
match self.style {
TableStyle::Amiga => self.print_amiga_color(writer),
_ => {
if let Some(style_cfg_ref) = self.style.config() {
self.print_styled(writer, style_cfg_ref)
} else {
self.print_simple_color(writer)
}
}
}
}
fn invalidate_dimensions(&mut self) {
self.column_dims = None;
self.row_heights = None;
}
fn ensure_dimensions(&mut self) {
if self.column_dims.is_some() && self.row_heights.is_some() {
return;
}
let num_cols = self.column_defs.len();
let mut max_effective_widths: Vec<usize> = vec![0; num_cols];
let mut calculated_row_heights: Vec<usize> = Vec::new();
if num_cols > 0 {
let mut header_row_max_lines = 0; let header_cells: Vec<Cell> = self.column_defs.iter()
.map(|def| Cell::new(&def.header)) .collect();
if !header_cells.is_empty() { header_row_max_lines = 1; for (i, header_cell) in header_cells.iter().enumerate() {
let formatted_header = header_cell.formatted_content();
max_effective_widths[i] = std::cmp::max(
max_effective_widths[i],
formatted_header.lines().map(|l| l.chars().count()).max().unwrap_or(0),
);
header_row_max_lines = std::cmp::max(header_row_max_lines, formatted_header.lines().count().max(1));
}
}
calculated_row_heights.push(header_row_max_lines);
} else {
calculated_row_heights.push(0); }
for row in &self.rows {
let mut current_row_max_lines = 1; for (i, cell) in row.iter().enumerate() {
if i < num_cols {
let formatted_cell_content = cell.formatted_content();
max_effective_widths[i] = std::cmp::max(
max_effective_widths[i],
formatted_cell_content.lines().map(|l| l.chars().count()).max().unwrap_or(0),
);
current_row_max_lines = std::cmp::max(current_row_max_lines, formatted_cell_content.lines().count().max(1));
}
}
calculated_row_heights.push(current_row_max_lines);
}
let final_column_dims: Vec<ColumnDim> = self.column_defs.iter().enumerate().map(|(i, col_def)| {
let default_cell_padding = CellStyle::default().padding; let content_width = max_effective_widths[i];
let total_width = content_width + default_cell_padding * 2; ColumnDim {
effective_content_width: total_width, total_width_for_drawing: total_width,
alignment: col_def.alignment,
}
}).collect();
self.column_dims = Some(final_column_dims);
self.row_heights = Some(calculated_row_heights);
}
fn print_simple_color(&mut self, writer: &mut dyn WriteColor) -> io::Result<()> {
self.ensure_dimensions();
let column_dims = self.column_dims.as_ref().ok_or_else(|| io::Error::other( "Dimensions not calculated (print_simple_color:column_dims)"))?;
let row_heights = self.row_heights.as_ref().ok_or_else(|| io::Error::other( "Dimensions not calculated (print_simple_color:row_heights)"))?;
if !self.column_defs.is_empty() {
let header_cells: Vec<Cell> = self.column_defs.iter().map(|def| Cell::new(&def.header)).collect();
let header_actual_height = row_heights.first().copied().unwrap_or(0);
if header_actual_height > 0 {
for line_idx in 0..header_actual_height {
self.print_row_simple_color_line(writer, &header_cells, line_idx, column_dims)?;
}
self.print_separator_simple_line(writer, column_dims)?;
}
}
for (row_data_idx, row_cells) in self.rows.iter().enumerate() {
let data_row_actual_height = row_heights.get(row_data_idx + 1).copied().unwrap_or(1);
for line_idx in 0..data_row_actual_height {
self.print_row_simple_color_line(writer, row_cells, line_idx, column_dims)?;
}
}
if !column_dims.is_empty() {
self.print_separator_simple_line(writer, column_dims)?;
}
Ok(())
}
fn print_simple(&mut self, writer: &mut dyn WriteColor) -> io::Result<()> {
self.ensure_dimensions();
let column_dims = self.column_dims.as_ref().ok_or_else(|| io::Error::other( "Dimensions not calculated (print_simple:column_dims)"))?;
let row_heights = self.row_heights.as_ref().ok_or_else(|| io::Error::other( "Dimensions not calculated (print_simple:row_heights)"))?;
if !self.column_defs.is_empty() {
let header_cells: Vec<Cell> = self.column_defs.iter().map(|def| Cell::new(&def.header)).collect();
let header_actual_height = row_heights.first().copied().unwrap_or(0); if header_actual_height > 0 { for line_idx in 0..header_actual_height {
self.print_row_simple_line(writer, &header_cells, line_idx, column_dims)?;
}
self.print_separator_simple_line(writer, column_dims)?;
}
}
for (row_data_idx, row_cells) in self.rows.iter().enumerate() {
let data_row_actual_height = row_heights.get(row_data_idx + 1).copied().unwrap_or(1);
for line_idx in 0..data_row_actual_height {
self.print_row_simple_line(writer, row_cells, line_idx, column_dims)?;
}
}
if !column_dims.is_empty() {
self.print_separator_simple_line(writer, column_dims)?;
}
Ok(())
}
fn print_separator_simple_line(&self, writer: &mut dyn WriteColor, column_dims: &[ColumnDim]) -> io::Result<()> {
if column_dims.is_empty() { return Ok(()); }
write!(writer, "+")?;
for dim in column_dims.iter() {
write!(writer, "{}", "-".repeat(dim.effective_content_width))?;
write!(writer, "+")?;
}
writeln!(writer)?;
Ok(())
}
fn print_row_simple_color_line(
&self,
writer: &mut dyn WriteColor,
row_data: &[Cell],
line_idx: usize,
column_dims: &[ColumnDim],
) -> io::Result<()> {
if column_dims.is_empty() { return writeln!(writer); }
write!(writer, "|")?;
for (col_idx, cell) in row_data.iter().enumerate() {
if col_idx >= column_dims.len() { continue; }
let dim = &column_dims[col_idx];
let width = dim.effective_content_width;
let cell_style = &cell.style;
let formatted_content = cell.formatted_content();
let content_line = formatted_content.lines().nth(line_idx).unwrap_or("");
let mut spec = ColorSpec::new();
if let Some(cell_bg) = cell_style.background_color { spec.set_bg(Some(cell_bg)); }
if let Some(cell_fg) = cell_style.foreground_color { spec.set_fg(Some(cell_fg)); }
if cell_style.bold { spec.set_bold(true); }
if cell_style.italic { spec.set_italic(true); }
if cell_style.underline { spec.set_underline(true); }
writer.set_color(&spec)?;
let content_len = content_line.chars().count();
let (padding_left, padding_right) = match cell_style.align.unwrap_or(Alignment::Left) {
Alignment::Left => (1, width.saturating_sub(content_len).saturating_sub(1)),
Alignment::Right => (width.saturating_sub(content_len).saturating_sub(1), 1),
Alignment::Center => {
let total_padding = width.saturating_sub(content_len);
let pl = total_padding / 2;
let pr = total_padding.saturating_sub(pl);
(pl.max(1), pr.max(1))
}
};
let final_padding_left = if content_len + padding_left + padding_right > width {
1.min(padding_left)
} else {
padding_left
};
let final_padding_right = if content_len + final_padding_left + padding_right > width {
(width.saturating_sub(content_len).saturating_sub(final_padding_left)).max(1)
} else {
padding_right
};
write!(writer, "{}", " ".repeat(final_padding_left))?;
let available_width_for_content = width.saturating_sub(final_padding_left).saturating_sub(final_padding_right);
let display_content: String = content_line.chars().take(available_width_for_content).collect();
write!(writer, "{display_content}")?;
write!(writer, "{}", " ".repeat(width.saturating_sub(display_content.chars().count()).saturating_sub(final_padding_left)))?;
writer.reset()?;
write!(writer, "|")?;
}
writeln!(writer)?;
Ok(())
}
fn print_row_simple_line(
&self,
writer: &mut dyn WriteColor,
row_data: &[Cell],
line_idx: usize,
column_dims: &[ColumnDim],
) -> io::Result<()> {
if column_dims.is_empty() { return writeln!(writer); } write!(writer, "|")?;
for (col_idx, cell) in row_data.iter().enumerate() {
if col_idx >= column_dims.len() { continue; }
let dim = &column_dims[col_idx];
let width = dim.effective_content_width;
let cell_style = &cell.style;
let formatted_content = cell.formatted_content();
let content_line = formatted_content.lines().nth(line_idx).unwrap_or("");
let content_len = content_line.chars().count();
let (padding_left, padding_right) = match cell_style.align.unwrap_or(Alignment::Left) {
Alignment::Left => (1, width.saturating_sub(content_len).saturating_sub(1)),
Alignment::Right => (width.saturating_sub(content_len).saturating_sub(1), 1),
Alignment::Center => {
let total_padding = width.saturating_sub(content_len);
let mut pl = total_padding / 2;
let mut pr = total_padding.saturating_sub(pl);
if pl == 0 && width > 0 { pl = 1; pr = pr.saturating_sub(1); }
else if pr == 0 && width > 0 { pr = 1; pl = pl.saturating_sub(1); }
if width == 0 { (0,0) } else { (pl.max(1), pr.max(1)) } }
};
let final_padding_left = if content_len + padding_left + padding_right > width {
1.min(padding_left)
} else {
padding_left
};
let final_padding_right = if content_len + final_padding_left + padding_right > width {
(width.saturating_sub(content_len).saturating_sub(final_padding_left)).max(1)
} else {
padding_right
};
write!(writer, "{}", " ".repeat(final_padding_left))?;
let available_width_for_content = width.saturating_sub(final_padding_left).saturating_sub(final_padding_right);
let display_content: String = content_line.chars().take(available_width_for_content).collect();
write!(writer, "{display_content}")?;
write!(writer, "{}", " ".repeat(width.saturating_sub(display_content.chars().count()).saturating_sub(final_padding_left)))?;
write!(writer, "|")?;
}
writeln!(writer)?;
Ok(())
}
fn print_amiga_color(&mut self, writer: &mut dyn WriteColor) -> io::Result<()> {
self.ensure_dimensions();
let column_dims = self.column_dims.as_ref().ok_or_else(|| io::Error::other( "Dimensions not calculated (print_amiga_color:column_dims)"))?;
let row_heights = self.row_heights.as_ref().ok_or_else(|| io::Error::other( "Dimensions not calculated (print_amiga_color:row_heights)"))?;
if !self.column_defs.is_empty() {
let header_cells: Vec<Cell> = self.column_defs.iter().map(|def| Cell::new(&def.header)).collect();
let header_actual_height = row_heights.first().copied().unwrap_or(0);
if header_actual_height > 0 {
let mut header_spec = ColorSpec::new();
header_spec.set_fg(Some(Color::Yellow));
for line_idx in 0..header_actual_height {
for (col_idx, cell) in header_cells.iter().enumerate() {
if col_idx >= column_dims.len() { continue; }
let dim = &column_dims[col_idx];
let formatted_header_line = cell.formatted_content().lines().nth(line_idx).unwrap_or("").to_string();
writer.set_color(&header_spec)?;
self.print_formatted_cell_line(writer, &formatted_header_line, dim, &cell.style, true)?; writer.reset()?;
if col_idx < header_cells.len() - 1 {
write!(writer, " ")?; }
}
writeln!(writer)?;
}
}
}
for (row_data_idx, row_cells) in self.rows.iter().enumerate() {
let data_row_actual_height = row_heights.get(row_data_idx + 1).copied().unwrap_or(1);
for line_idx in 0..data_row_actual_height {
for (col_idx, cell) in row_cells.iter().enumerate() {
if col_idx >= column_dims.len() { continue; }
let dim = &column_dims[col_idx];
let cell_style = &cell.style;
let formatted_cell_line = cell.formatted_content().lines().nth(line_idx).unwrap_or("").to_string();
let mut cell_color_spec = ColorSpec::new();
if let Some(fg) = cell_style.foreground_color { cell_color_spec.set_fg(Some(fg)); }
else { cell_color_spec.set_fg(Some(Color::White)); } if let Some(bg) = cell_style.background_color { cell_color_spec.set_bg(Some(bg)); }
if cell_style.bold { cell_color_spec.set_bold(true); }
if cell_style.italic { cell_color_spec.set_italic(true); }
if cell_style.underline { cell_color_spec.set_underline(true); }
writer.set_color(&cell_color_spec)?;
self.print_formatted_cell_line(writer, &formatted_cell_line, dim, cell_style, true)?; writer.reset()?;
if col_idx < row_cells.len() - 1 {
write!(writer, " ")?;
}
}
writeln!(writer)?;
}
}
Ok(())
}
fn print_formatted_cell_line(
&self,
writer: &mut dyn WriteColor,
content_line: &str,
dim: &ColumnDim,
cell_style: &CellStyle, _is_amiga: bool, ) -> io::Result<()> {
let effective_width = dim.effective_content_width;
let line_char_count = content_line.chars().count();
let (pad_left, pad_right) = match cell_style.align.unwrap_or(dim.alignment) { Alignment::Left => (0, effective_width.saturating_sub(line_char_count)),
Alignment::Right => (effective_width.saturating_sub(line_char_count), 0),
Alignment::Center => {
let padding = effective_width.saturating_sub(line_char_count);
(padding / 2, padding.saturating_sub(padding / 2))
}
};
write!(writer, "{}", " ".repeat(pad_left))?;
write!(writer, "{content_line}")?;
write!(writer, "{}", " ".repeat(pad_right))?;
Ok(())
}
fn print_styled(
&mut self,
writer: &mut dyn WriteColor,
style_cfg: &TableStyleConfig,
) -> io::Result<()> {
self.ensure_dimensions();
let column_dims = self.column_dims.as_ref().ok_or_else(|| io::Error::other( "Dimensions not calculated (print_styled:column_dims)"))?;
let row_heights = self.row_heights.as_ref().ok_or_else(|| io::Error::other( "Dimensions not calculated (print_styled:row_heights)"))?;
if column_dims.is_empty() { return Ok(()); }
self.print_horizontal_border(writer, &style_cfg.top, column_dims)?;
if !self.column_defs.is_empty() {
let header_cells: Vec<Cell> = self.column_defs.iter().map(|def| Cell::new(&def.header)).collect();
let header_actual_height = row_heights.first().copied().unwrap_or(0);
if header_actual_height > 0 {
for line_idx in 0..header_actual_height {
self.print_content_row_styled(writer, &header_cells, &style_cfg.row, line_idx, column_dims)?;
}
self.print_horizontal_border(writer, &style_cfg.below_header, column_dims)?;
}
}
for (row_data_idx, row_cells) in self.rows.iter().enumerate() {
let data_row_actual_height = row_heights.get(row_data_idx + 1).copied().unwrap_or(1);
for line_idx in 0..data_row_actual_height {
self.print_content_row_styled(writer, row_cells, &style_cfg.row, line_idx, column_dims)?;
}
}
self.print_horizontal_border(writer, &style_cfg.bottom, column_dims)?;
Ok(())
}
fn print_horizontal_border(&self, writer: &mut dyn WriteColor, line_style: &LineStyle, column_dims: &[ColumnDim]) -> io::Result<()> {
if column_dims.is_empty() { return Ok(()); }
write!(writer, "{}", line_style.begin)?;
for (i, dim) in column_dims.iter().enumerate() {
if i > 0 {
write!(writer, "{}", line_style.sep)?;
}
write!(writer, "{}", line_style.hline.repeat(dim.total_width_for_drawing))?;
}
writeln!(writer, "{}", line_style.end)
}
fn print_content_row_styled(
&self,
writer: &mut dyn WriteColor,
row_data: &[Cell],
line_style: &LineStyle,
line_idx: usize,
column_dims: &[ColumnDim],
) -> io::Result<()> {
if column_dims.is_empty() { return writeln!(writer); }
write!(writer, "{}", line_style.begin)?;
for (col_idx, cell) in row_data.iter().enumerate() {
if col_idx >= column_dims.len() { continue; }
let dim = &column_dims[col_idx];
let width = dim.effective_content_width;
let cell_style = &cell.style;
let formatted_content = cell.formatted_content();
let content_line = formatted_content.lines().nth(line_idx).unwrap_or("");
let mut spec = ColorSpec::new();
if let Some(cell_bg) = cell_style.background_color { spec.set_bg(Some(cell_bg)); }
if let Some(cell_fg) = cell_style.foreground_color { spec.set_fg(Some(cell_fg)); }
if cell_style.bold { spec.set_bold(true); }
if cell_style.italic { spec.set_italic(true); }
if cell_style.underline { spec.set_underline(true); }
writer.set_color(&spec)?;
let content_len = content_line.chars().count();
let alignment = cell_style.align.unwrap_or(dim.alignment);
let (padding_left, _padding_right) = match alignment {
Alignment::Left => (cell_style.padding, width.saturating_sub(content_len).saturating_sub(cell_style.padding)),
Alignment::Right => (width.saturating_sub(content_len).saturating_sub(cell_style.padding), cell_style.padding),
Alignment::Center => {
let total_padding = width.saturating_sub(content_len);
let pl = total_padding / 2;
let pr = total_padding.saturating_sub(pl);
(pl.max(cell_style.padding), pr.max(cell_style.padding))
}
};
let final_padding_left = padding_left.min(width.saturating_sub(content_len));
let final_padding_right = width.saturating_sub(content_len).saturating_sub(final_padding_left);
write!(writer, "{}", " ".repeat(final_padding_left))?;
write!(writer, "{content_line}")?;
write!(writer, "{}", " ".repeat(final_padding_right))?;
writer.reset()?;
if col_idx < row_data.len() - 1 {
write!(writer, "{}", line_style.sep)?;
}
}
writeln!(writer, "{}", line_style.end)?;
Ok(())
}
}