use std::collections::{BTreeMap, HashMap};
use tabled::{
Table, Tabled,
builder::Builder,
settings::{
Color, Modify, Style, Width,
object::{Columns, Rows},
panel::HorizontalPanel,
peaker::PriorityMax,
},
};
use crate::{Expression, expression::table::TableData, libs::bin::into_lib::strip_ansi_escapes};
const MIN_TABLE_WIDTH: usize = 20;
const MIN_READABLE_COL_WIDTH: usize = 4;
const WRAP_TOLERANCE: f64 = 1.5;
fn visible_width(s: &str) -> usize {
s.lines()
.map(|l| strip_ansi_escapes(l).chars().count())
.max()
.unwrap_or(0)
}
fn max_token_width(text: &str) -> usize {
text.split_whitespace()
.map(|w| strip_ansi_escapes(w).chars().count())
.max()
.unwrap_or(0)
}
fn quick_reject(
cols: usize,
first_row_total_len: usize,
max_width: usize,
max_wraped_width: usize,
) -> bool {
if cols == 0 {
return true;
}
if max_wraped_width + cols * 3 + 1 >= max_width {
return true;
}
if cols * MIN_READABLE_COL_WIDTH > max_width {
return true;
}
(first_row_total_len as f64) > max_width as f64 * WRAP_TOLERANCE
}
fn accept_natural_width(table: &Table, max_width: usize) -> bool {
visible_width(&table.to_string()) as f64 <= max_width as f64 * WRAP_TOLERANCE
}
fn finalize_table(mut table: Table, max_width: usize, nested: bool) -> Option<Table> {
if nested && !accept_natural_width(&table, max_width) {
return None;
}
fit_width(&mut table, max_width);
Some(table)
}
pub fn pretty_printer(arg: &Expression) -> Result<Expression, crate::RuntimeError> {
let specified_width = crossterm::terminal::size().unwrap_or((120, 0)).0 as usize;
match arg {
Expression::Table(table_data) => {
let out = print_table_with_tabled(table_data, true, specified_width, false)
.map(|t| t.to_string())
.unwrap_or_else(|| format!("{arg:#}"));
println!("{}", out)
}
Expression::Map(exprs) => {
let out = pprint_map(exprs.as_ref(), true, specified_width)
.map(|t| t.to_string())
.unwrap_or_else(|| format!("{arg:#}"));
println!("{}", out)
}
Expression::HMap(exprs) => {
let out = pprint_hmap(exprs.as_ref(), true, specified_width)
.map(|t| t.to_string())
.unwrap_or_else(|| format!("{arg:#}"));
println!("{}", out)
}
Expression::List(exprs) => {
let out = pprint_list(exprs.as_ref(), true, specified_width, false)
.map(|t| t.to_string())
.unwrap_or_else(|| format!("{arg:#}"));
println!("{}", out)
}
_ => {
println!("{arg:#}");
}
}
Ok(Expression::None)
}
pub fn pretty_formatter(arg: &Expression) -> String {
let specified_width = crossterm::terminal::size().unwrap_or((120, 0)).0 as usize;
match arg {
Expression::Table(table_data) => {
print_table_with_tabled(table_data, false, specified_width, false)
.map(|t| t.to_string())
.unwrap_or_else(|| format!("{arg:#}"))
}
Expression::Map(exprs) => pprint_map(exprs.as_ref(), false, specified_width)
.map(|t| t.to_string())
.unwrap_or_else(|| format!("{arg:#}")),
Expression::HMap(exprs) => pprint_hmap(exprs.as_ref(), false, specified_width)
.map(|t| t.to_string())
.unwrap_or_else(|| format!("{arg:#}")),
Expression::List(exprs) => pprint_list(exprs.as_ref(), false, specified_width, false)
.map(|t| t.to_string())
.unwrap_or_else(|| format!("{arg:#}")),
_ => format!("{arg:#}"),
}
}
#[derive(Tabled, PartialEq, Eq, PartialOrd, Ord)]
struct KeyValueRow {
#[tabled(rename = "KEY")]
key: String,
#[tabled(rename = "VALUE")]
value: String,
}
fn try_render_sub_table<F>(build: F, fallback_val: &Expression, cell_width: usize) -> String
where
F: FnOnce() -> Option<Table>,
{
if cell_width < MIN_TABLE_WIDTH {
return textwrap::fill(&format!("{fallback_val}"), cell_width.max(1));
}
let sub = match build() {
Some(t) => t.to_string(),
None => return textwrap::fill(&format!("{fallback_val}"), cell_width),
};
if visible_width(&sub) > cell_width {
return textwrap::fill(&format!("{fallback_val}"), cell_width);
}
sub
}
fn is_list_of_records(items: &[Expression]) -> bool {
!items.is_empty()
&& items
.iter()
.all(|e| matches!(e, Expression::Map(_) | Expression::HMap(_)))
}
fn print_table_with_tabled(
table: &TableData,
with_color: bool,
max_width: usize,
nested: bool,
) -> Option<Table> {
let group_idx = table.groups(); let all_headers = table.headers();
let mut headers = all_headers.to_vec();
for &g in group_idx {
headers.swap_remove(g);
}
let cols = headers.len();
let mut rows_iter = table.rows().iter();
let first_row: Vec<String> = match rows_iter.next() {
Some(row) => row.iter().map(|x| x.to_string()).collect(),
None => Vec::new(),
};
let first_row_len: usize = first_row.iter().map(|c| visible_width(c)).sum();
let max_wraped_width: usize = first_row.iter().map(|c| max_token_width(c)).sum();
if nested && quick_reject(cols, first_row_len, max_width, max_wraped_width) {
return None;
}
let mut current_group: Vec<String> = vec![];
let mut panels: Vec<(usize, String)> = vec![];
let mut builder = Builder::with_capacity(table.row_count(), cols);
builder.push_record(&headers);
for row in table.rows() {
let mut row = row.clone();
let labels: Vec<String> = group_idx
.iter()
.map(|&g| row.swap_remove(g).to_string())
.collect();
if !group_idx.is_empty() && labels != current_group {
panels.push((builder.count_records(), labels.join(" ")));
current_group = labels;
}
builder.push_record(row.iter().map(|x| x.to_string()));
}
let mut built = builder.build();
if with_color {
built.modify(Rows::first(), Color::FG_BLUE);
}
if table.is_grouped() {
for (idx, label) in panels.into_iter().rev() {
built.with(HorizontalPanel::new(idx, format!("───── {} ─────", label)));
}
}
apply_table_style(&mut built, false, nested);
finalize_table(built, max_width, nested)
}
type KvIter<'a> = Box<dyn Iterator<Item = (String, Expression)> + 'a>;
fn pprint_map_internal<'a>(
items: KvIter<'a>,
is_hmap: bool,
with_color: bool,
max_width: usize,
nested: bool,
) -> Option<Table> {
let mut entries: Vec<(String, Expression)> = items.collect();
if is_hmap {
entries.sort_by(|a, b| a.0.cmp(&b.0));
}
let use_panel = !nested
&& entries
.first()
.map(|(_, v)| matches!(v, Expression::List(items) if is_list_of_records(items)))
.unwrap_or(false);
if use_panel {
if let Some(t) =
pprint_map_of_record_lists_as_panels(&entries, is_hmap, with_color, max_width)
{
return Some(t);
}
}
const COLS: usize = 2;
let table_padding = COLS * 3 + 1 + 5;
let available_width = max_width.saturating_sub(table_padding);
let key_column_width = 12.min(available_width / 4);
let value_budget = available_width.saturating_sub(key_column_width);
let rows: Vec<KeyValueRow> = entries
.into_iter()
.map(|(key, val)| {
let value = render_field(&val, value_budget);
KeyValueRow { key, value }
})
.collect();
if nested {
if let Some(first) = rows.first() {
let first_row_len = visible_width(&first.key) + visible_width(&first.value);
let max_wraped_width = max_token_width(&first.key) + max_token_width(&first.value);
if quick_reject(COLS, first_row_len, max_width, max_wraped_width) {
return None;
}
}
}
let mut table = Table::new(rows);
if is_hmap {
if with_color {
table.modify(Columns::first(), Color::FG_BLUE);
}
table.modify(
Columns::first(),
Width::truncate(key_column_width).suffix("…"),
);
} else if with_color {
table.modify(Columns::first(), Color::FG_GREEN);
}
apply_table_style(&mut table, is_hmap, nested);
finalize_table(table, max_width, nested)
}
fn pprint_map_of_record_lists_as_panels(
entries: &[(String, Expression)],
is_hmap: bool,
with_color: bool,
max_width: usize,
) -> Option<Table> {
let headers: Vec<String> = match entries.first() {
Some((_, Expression::List(items))) => match items.first() {
Some(Expression::Map(m)) => m.keys().cloned().collect(),
Some(Expression::HMap(m)) => {
let mut ks: Vec<String> = m.keys().cloned().collect();
ks.sort();
ks
}
_ => return None,
},
_ => return None,
};
let cols = headers.len();
if cols == 0 {
return None;
}
let col_budget = (max_width.saturating_sub(cols * 3 + 1)) / cols.max(1);
let mut builder = Builder::with_capacity(entries.len() * 4, cols);
builder.push_record(headers.clone());
let mut panels: Vec<(usize, String)> = vec![];
for (key, val) in entries {
match val {
Expression::List(items) if is_list_of_records(items) => {
panels.push((builder.count_records(), key.clone()));
for item in items.iter() {
let row: Vec<String> = headers
.iter()
.map(|h| {
let field = match item {
Expression::Map(m) => m.get(h),
Expression::HMap(m) => m.get(h),
_ => None,
};
field
.map(|v| render_field(v, col_budget))
.unwrap_or_default()
})
.collect();
builder.push_record(row);
}
}
other => {
panels.push((builder.count_records(), key.clone()));
let mut row = vec![render_field(other, col_budget)];
row.resize(cols, String::new());
builder.push_record(row);
}
}
}
let mut table = builder.build();
if with_color {
table.modify(Rows::first(), Color::FG_BLUE);
}
table.with(
Modify::new(Rows::first()).with(tabled::settings::format::Format::content(|s| {
s.to_uppercase()
})),
);
if is_hmap {
}
for (idx, label) in panels.into_iter().rev() {
table.with(HorizontalPanel::new(idx, format!("───── {} ─────", label)));
}
apply_table_style(&mut table, is_hmap, false);
finalize_table(table, max_width, false)
}
fn fit_width(table: &mut Table, max_width: usize) {
table.with(
Width::wrap(max_width)
.keep_words(true)
.priority(PriorityMax::right()),
);
}
fn apply_table_style(table: &mut Table, use_markdown: bool, nested: bool) {
if nested {
table.with(Style::psql());
} else if use_markdown {
table.with(Style::markdown());
} else {
table.with(Style::rounded());
}
}
fn pprint_map(
exprs: &BTreeMap<String, Expression>,
with_color: bool,
max_width: usize,
) -> Option<Table> {
pprint_map_internal(
Box::new(exprs.iter().map(|(k, v)| (k.clone(), v.clone()))),
false,
with_color,
max_width,
false,
)
}
pub fn pprint_hmap(
exprs: &HashMap<String, Expression>,
with_color: bool,
max_width: usize,
) -> Option<Table> {
pprint_map_internal(
Box::new(exprs.iter().map(|(k, v)| (k.clone(), v.clone()))),
true,
with_color,
max_width,
false,
)
}
fn render_field(val: &Expression, cell_width: usize) -> String {
match val {
Expression::HMap(_) | Expression::Map(_) => render_value(val, cell_width, false, true),
Expression::List(items) if is_list_of_records(items) => {
render_value(val, cell_width, false, true)
}
Expression::Table(_) => render_value(val, cell_width, false, true),
_ => format!("{val}"),
}
}
fn render_value(val: &Expression, cell_width: usize, with_color: bool, nested: bool) -> String {
match val {
Expression::HMap(m) => try_render_sub_table(
|| {
pprint_map_internal(
Box::new(m.iter().map(|(k, v)| (k.clone(), v.clone()))),
true,
with_color,
cell_width,
nested,
)
},
val,
cell_width,
),
Expression::Map(m) => try_render_sub_table(
|| {
pprint_map_internal(
Box::new(m.iter().map(|(k, v)| (k.clone(), v.clone()))),
false,
with_color,
cell_width,
nested,
)
},
val,
cell_width,
),
Expression::List(items) if is_list_of_records(items) => try_render_sub_table(
|| pprint_list(items, with_color, cell_width, nested),
val,
cell_width,
),
Expression::Table(t) => try_render_sub_table(
|| print_table_with_tabled(t, false, cell_width, nested),
val,
cell_width,
),
_ => textwrap::fill(&format!("{val}"), cell_width),
}
}
fn pprint_list(
exprs: &[Expression],
with_color: bool,
max_width: usize,
nested: bool,
) -> Option<Table> {
let (rows, heads_opt) = TableRow {
rows: exprs,
max_width,
col_padding: 5,
}
.split_into_rows();
if rows.is_empty() {
return Some(Table::default());
}
if nested {
let cols = heads_opt.as_ref().map(|h| h.len()).unwrap_or(rows[0].len());
let first_row_len: usize = rows[0].iter().map(|c| visible_width(c)).sum();
let max_wraped_width: usize = rows[0].iter().map(|c| max_token_width(c)).sum();
if quick_reject(cols, first_row_len, max_width, max_wraped_width) {
return None;
}
}
let mut builder;
let has_header = match heads_opt {
Some(heads) => {
builder = Builder::with_capacity(rows.len(), heads.len());
builder.insert_record(0, heads);
true
}
_ => {
builder = Builder::with_capacity(rows.len(), rows[0].len());
false
}
};
for row in rows {
builder.push_record(row);
}
let mut table = builder.build();
if has_header {
if with_color {
table.modify(Rows::first(), Color::FG_BLUE);
}
table.with(
Modify::new(Rows::first()).with(tabled::settings::format::Format::content(|s| {
s.to_uppercase()
})),
);
}
apply_table_style(&mut table, false, nested);
finalize_table(table, max_width, nested)
}
struct TableRow<'a> {
rows: &'a [Expression],
max_width: usize,
col_padding: usize,
}
impl<'a> TableRow<'a> {
fn split_into_rows(&self) -> (Vec<Vec<String>>, Option<Vec<String>>) {
let mut result = Vec::with_capacity(self.rows.len());
let heads = match self.rows.first() {
Some(Expression::List(a)) => {
Some(a.iter().enumerate().map(|(i, _)| format!("C{i}")).collect())
}
Some(Expression::HMap(a)) => Some(a.keys().cloned().collect::<Vec<String>>()),
Some(Expression::Map(a)) => Some(a.keys().cloned().collect::<Vec<String>>()),
_ => None,
};
let mut cols = heads.as_ref().map_or(0, |h| h.len());
let mut current_row = Vec::with_capacity(cols);
if cols > 0 {
let score_sum = self
.rows
.iter()
.map(|x| {
if matches!(
x,
Expression::List(_) | Expression::Map(_) | Expression::HMap(_)
) {
3
} else {
1
}
})
.sum::<usize>();
let per_cell_width =
((self.max_width / score_sum) * 3).saturating_sub(self.col_padding);
for expr in self.rows.iter() {
match expr {
Expression::List(a) => {
for c in a.iter() {
current_row.push(render_field(c, per_cell_width));
}
}
Expression::HMap(a) => {
for (_, v) in a.iter() {
current_row.push(render_field(v, per_cell_width));
}
}
Expression::Map(a) => {
for (_, v) in a.iter() {
current_row.push(render_field(v, per_cell_width));
}
}
other => current_row.push(other.to_string()),
};
if !current_row.is_empty() {
result.push(current_row);
current_row = vec![];
}
}
return (result, heads);
}
let mut current_len = 0;
for (i, expr) in self.rows.iter().enumerate() {
let col = match expr {
Expression::List(a) => a
.as_ref()
.iter()
.map(|f| f.to_string())
.collect::<Vec<String>>()
.join(", "),
Expression::HMap(a) => a
.as_ref()
.values()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join("\t"),
Expression::Map(a) => a
.as_ref()
.values()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join("\t"),
other => other.to_string(),
};
let col_width = strip_ansi_escapes(&col).chars().count() + self.col_padding;
if cols == 0 {
if !current_row.is_empty() && current_len + col_width > self.max_width {
cols = i;
result.push(current_row);
current_row = vec![];
current_len = 0;
}
} else if i % cols == 0 {
result.push(current_row);
current_row = vec![];
current_len = 0;
}
if col_width > self.max_width {
let chunks = self.split_column(&col);
for chunk in chunks {
if !current_row.is_empty() {
result.push(current_row);
current_row = vec![];
}
current_row.push(chunk);
}
current_len = current_row.last().map(|s| s.len()).unwrap_or(0);
} else {
current_row.push(col);
current_len += col_width;
}
}
if !current_row.is_empty() {
result.push(current_row);
}
(result, None)
}
fn split_column(&self, text: &str) -> Vec<String> {
let max_chunk = self.max_width.saturating_sub(self.col_padding);
if max_chunk == 0 {
return vec![text.to_string()];
}
textwrap::wrap(text, max_chunk)
.into_iter()
.map(|s| s.to_string())
.collect()
}
}