use comfy_table::{
Attribute, Cell, CellAlignment, ContentArrangement, Table as ComfyTable, TableComponent,
presets,
};
use indexmap::IndexMap;
use owo_colors::OwoColorize;
use vantage_dataset::prelude::ReadableValueSet;
use vantage_table::prelude::ColumnLike;
use vantage_table::table::Table;
use vantage_table::traits::table_source::TableSource;
use vantage_types::{Entity, Record, RichText, Span, Style, TerminalRender};
fn rich_to_ansi(rich: &RichText) -> String {
let mut out = String::with_capacity(rich.spans.iter().map(|s| s.text.len()).sum::<usize>() + 8);
for span in &rich.spans {
out.push_str(&span_to_ansi(span));
}
out
}
fn span_to_ansi(span: &Span) -> String {
let t = span.text.as_str();
match span.style {
Style::Default => t.to_string(),
Style::Dim => t.dimmed().to_string(),
Style::Muted => t.bright_black().to_string(),
Style::Strong => t.bold().to_string(),
Style::Success => t.green().to_string(),
Style::Error => t.red().to_string(),
Style::Warning => t.yellow().to_string(),
Style::Info => t.cyan().to_string(),
}
}
fn alignment_for(type_name: &str) -> CellAlignment {
let last = type_name.rsplit("::").next().unwrap_or(type_name);
if matches!(
last,
"i8" | "i16"
| "i32"
| "i64"
| "i128"
| "isize"
| "u8"
| "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "f32"
| "f64"
) {
CellAlignment::Right
} else {
CellAlignment::Left
}
}
fn header_cell(name: &str) -> Cell {
Cell::new(name.to_uppercase().cyan().bold().to_string())
}
pub async fn print_table<T, E>(table: &Table<T, E>) -> vantage_core::Result<()>
where
T: TableSource,
T::Value: TerminalRender,
T::Id: std::fmt::Display,
E: Entity<T::Value>,
{
let id_field = table.id_field().map(|c| c.name().to_string());
let column_types: IndexMap<String, &'static str> = table
.columns()
.iter()
.map(|(name, col)| (name.clone(), col.get_type()))
.collect();
let records = table.list_values().await?;
render_records_typed(&records, id_field.as_deref(), &column_types);
Ok(())
}
pub fn render_records<Id, V>(records: &IndexMap<Id, Record<V>>, id_field: Option<&str>)
where
Id: std::fmt::Display,
V: TerminalRender,
{
render_records_typed(records, id_field, &IndexMap::new());
}
pub fn render_records_columns<Id, V>(
records: &IndexMap<Id, Record<V>>,
columns: &[String],
column_types: &IndexMap<String, &'static str>,
) where
Id: std::fmt::Display,
V: TerminalRender,
{
if records.is_empty() {
println!("{}", "No records.".dimmed());
return;
}
let mut table = ComfyTable::new();
table
.load_preset(presets::UTF8_HORIZONTAL_ONLY)
.remove_style(TableComponent::HorizontalLines)
.remove_style(TableComponent::MiddleIntersections)
.remove_style(TableComponent::LeftBorderIntersections)
.remove_style(TableComponent::RightBorderIntersections)
.set_content_arrangement(ContentArrangement::Disabled);
let header: Vec<Cell> = columns.iter().map(|c| header_cell(c)).collect();
table.set_header(header);
for (idx, name) in columns.iter().enumerate() {
if let Some(type_name) = column_types.get(name) {
let align = alignment_for(type_name);
if let Some(col) = table.column_mut(idx) {
col.set_cell_alignment(align);
}
}
}
for (_id, record) in records {
let row: Vec<Cell> = columns
.iter()
.map(|col| match record.get(col.as_str()) {
Some(value) => Cell::new(rich_to_ansi(&value.render())),
None => Cell::new("—".bright_black().to_string()),
})
.collect();
table.add_row(row);
}
println!("{table}");
let n = records.len();
let label = if n == 1 { "record" } else { "records" };
println!("{}", format!("{n} {label}").dimmed());
}
pub fn render_records_typed<Id, V>(
records: &IndexMap<Id, Record<V>>,
id_field: Option<&str>,
column_types: &IndexMap<String, &'static str>,
) where
Id: std::fmt::Display,
V: TerminalRender,
{
if records.is_empty() {
println!("{}", "No records.".dimmed());
return;
}
let columns: Vec<String> = if !column_types.is_empty() {
column_types
.keys()
.filter(|k| Some(k.as_str()) != id_field)
.cloned()
.collect()
} else {
records
.values()
.next()
.unwrap()
.keys()
.filter(|k| k.as_str() != "id" && Some(k.as_str()) != id_field)
.cloned()
.collect()
};
let mut table = ComfyTable::new();
table
.load_preset(presets::UTF8_HORIZONTAL_ONLY)
.remove_style(TableComponent::HorizontalLines)
.remove_style(TableComponent::MiddleIntersections)
.remove_style(TableComponent::LeftBorderIntersections)
.remove_style(TableComponent::RightBorderIntersections)
.set_content_arrangement(ContentArrangement::Disabled);
let mut header = vec![header_cell("id")];
header.extend(columns.iter().map(|c| header_cell(c)));
table.set_header(header);
for (idx, name) in columns.iter().enumerate() {
if let Some(type_name) = column_types.get(name) {
let align = alignment_for(type_name);
if let Some(col) = table.column_mut(idx + 1) {
col.set_cell_alignment(align);
}
}
}
for (id, record) in records {
let id_cell = Cell::new(id.to_string()).add_attribute(Attribute::Bold);
let mut row = vec![id_cell];
for col in &columns {
let cell = match record.get(col.as_str()) {
Some(value) => Cell::new(rich_to_ansi(&value.render())),
None => Cell::new("—".bright_black().to_string()),
};
row.push(cell);
}
table.add_row(row);
}
println!("{table}");
let n = records.len();
let label = if n == 1 { "record" } else { "records" };
println!("{}", format!("{n} {label}").dimmed());
}