use crate::utils::unicode::display_width;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum Align {
#[default]
Left,
Center,
Right,
}
#[derive(Clone, Debug)]
pub struct Column {
pub header: String,
pub width: usize,
pub align: Align,
}
impl Column {
pub fn new(header: impl Into<String>, width: usize, align: Align) -> Self {
Self {
header: header.into(),
width,
align,
}
}
pub fn format(&self, value: &str) -> String {
align_text(value, self.width, self.align)
}
pub fn format_header(&self) -> String {
align_text(&self.header, self.width, self.align)
}
}
#[derive(Clone, Debug)]
pub struct Table {
columns: Vec<Column>,
spacing: usize,
prefix: String,
}
impl Default for Table {
fn default() -> Self {
Self::new()
}
}
impl Table {
pub fn new() -> Self {
Self {
columns: Vec::new(),
spacing: 1,
prefix: String::new(),
}
}
pub fn col(mut self, header: impl Into<String>, width: usize, align: Align) -> Self {
self.columns.push(Column::new(header, width, align));
self
}
pub fn col_left(self, header: impl Into<String>, width: usize) -> Self {
self.col(header, width, Align::Left)
}
pub fn col_right(self, header: impl Into<String>, width: usize) -> Self {
self.col(header, width, Align::Right)
}
pub fn col_center(self, header: impl Into<String>, width: usize) -> Self {
self.col(header, width, Align::Center)
}
pub fn spacing(mut self, spacing: usize) -> Self {
self.spacing = spacing;
self
}
pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = prefix.into();
self
}
pub fn header(&self) -> String {
let mut result = self.prefix.clone();
for (i, col) in self.columns.iter().enumerate() {
if i > 0 {
result.push_str(&" ".repeat(self.spacing));
}
result.push_str(&col.format_header());
}
result
}
pub fn row(&self, values: &[&str]) -> String {
let mut result = self.prefix.clone();
for (i, col) in self.columns.iter().enumerate() {
if i > 0 {
result.push_str(&" ".repeat(self.spacing));
}
let value = values.get(i).unwrap_or(&"");
result.push_str(&col.format(value));
}
result
}
pub fn row_owned(&self, values: &[String]) -> String {
let refs: Vec<&str> = values.iter().map(|s| s.as_str()).collect();
self.row(&refs)
}
pub fn total_width(&self) -> usize {
let col_widths: usize = self.columns.iter().map(|c| c.width).sum();
let spacing_width = if self.columns.len() > 1 {
self.spacing * (self.columns.len() - 1)
} else {
0
};
self.prefix.len() + col_widths + spacing_width
}
pub fn column_count(&self) -> usize {
self.columns.len()
}
pub fn get_column(&self, index: usize) -> Option<&Column> {
self.columns.get(index)
}
pub fn column_offset(&self, index: usize) -> usize {
let mut offset = self.prefix.len();
for (i, col) in self.columns.iter().enumerate() {
if i == index {
return offset;
}
offset += col.width;
if i < self.columns.len() - 1 {
offset += self.spacing;
}
}
offset
}
}
pub fn align_text(text: &str, width: usize, align: Align) -> String {
let text_width = display_width(text);
if text_width >= width {
let mut result = String::new();
let mut current_width = 0;
for ch in text.chars() {
let ch_width = crate::utils::unicode::char_width(ch);
if current_width + ch_width > width {
break;
}
result.push(ch);
current_width += ch_width;
}
while current_width < width {
result.push(' ');
current_width += 1;
}
result
} else {
let padding = width - text_width;
match align {
Align::Left => format!("{}{}", text, " ".repeat(padding)),
Align::Right => format!("{}{}", " ".repeat(padding), text),
Align::Center => {
let left_pad = padding / 2;
let right_pad = padding - left_pad;
format!("{}{}{}", " ".repeat(left_pad), text, " ".repeat(right_pad))
}
}
}
}
pub fn align_left(text: &str, width: usize) -> String {
align_text(text, width, Align::Left)
}
pub fn align_right(text: &str, width: usize) -> String {
align_text(text, width, Align::Right)
}
pub fn align_center(text: &str, width: usize) -> String {
align_text(text, width, Align::Center)
}