vantage_cli_util/
table_display.rs1use comfy_table::{
2 Attribute, Cell, CellAlignment, ContentArrangement, Table as ComfyTable, TableComponent,
3 presets,
4};
5use indexmap::IndexMap;
6use owo_colors::OwoColorize;
7use vantage_dataset::prelude::ReadableValueSet;
8use vantage_table::prelude::ColumnLike;
9use vantage_table::table::Table;
10use vantage_table::traits::table_source::TableSource;
11use vantage_types::{Entity, Record, RichText, Span, Style, TerminalRender};
12
13fn rich_to_ansi(rich: &RichText) -> String {
18 let mut out = String::with_capacity(rich.spans.iter().map(|s| s.text.len()).sum::<usize>() + 8);
19 for span in &rich.spans {
20 out.push_str(&span_to_ansi(span));
21 }
22 out
23}
24
25fn span_to_ansi(span: &Span) -> String {
26 let t = span.text.as_str();
27 match span.style {
28 Style::Default => t.to_string(),
29 Style::Dim => t.dimmed().to_string(),
30 Style::Muted => t.bright_black().to_string(),
31 Style::Strong => t.bold().to_string(),
32 Style::Success => t.green().to_string(),
33 Style::Error => t.red().to_string(),
34 Style::Warning => t.yellow().to_string(),
35 Style::Info => t.cyan().to_string(),
36 }
37}
38
39fn alignment_for(type_name: &str) -> CellAlignment {
41 let last = type_name.rsplit("::").next().unwrap_or(type_name);
42 if matches!(
43 last,
44 "i8" | "i16"
45 | "i32"
46 | "i64"
47 | "i128"
48 | "isize"
49 | "u8"
50 | "u16"
51 | "u32"
52 | "u64"
53 | "u128"
54 | "usize"
55 | "f32"
56 | "f64"
57 ) {
58 CellAlignment::Right
59 } else {
60 CellAlignment::Left
61 }
62}
63
64fn header_cell(name: &str) -> Cell {
65 Cell::new(name.to_uppercase().cyan().bold().to_string())
66}
67
68pub async fn print_table<T, E>(table: &Table<T, E>) -> vantage_core::Result<()>
70where
71 T: TableSource,
72 T::Value: TerminalRender,
73 T::Id: std::fmt::Display,
74 E: Entity<T::Value>,
75{
76 let id_field = table.id_field().map(|c| c.name().to_string());
77
78 let column_types: IndexMap<String, &'static str> = table
79 .columns()
80 .iter()
81 .map(|(name, col)| (name.clone(), col.get_type()))
82 .collect();
83
84 let records = table.list_values().await?;
85 render_records_typed(&records, id_field.as_deref(), &column_types);
86 Ok(())
87}
88
89pub fn render_records<Id, V>(records: &IndexMap<Id, Record<V>>, id_field: Option<&str>)
94where
95 Id: std::fmt::Display,
96 V: TerminalRender,
97{
98 render_records_typed(records, id_field, &IndexMap::new());
99}
100
101pub fn render_records_columns<Id, V>(
109 records: &IndexMap<Id, Record<V>>,
110 columns: &[String],
111 column_types: &IndexMap<String, &'static str>,
112) where
113 Id: std::fmt::Display,
114 V: TerminalRender,
115{
116 if records.is_empty() {
117 println!("{}", "No records.".dimmed());
118 return;
119 }
120
121 let mut table = ComfyTable::new();
122 table
123 .load_preset(presets::UTF8_HORIZONTAL_ONLY)
124 .remove_style(TableComponent::HorizontalLines)
125 .remove_style(TableComponent::MiddleIntersections)
126 .remove_style(TableComponent::LeftBorderIntersections)
127 .remove_style(TableComponent::RightBorderIntersections)
128 .set_content_arrangement(ContentArrangement::Disabled);
129
130 let header: Vec<Cell> = columns.iter().map(|c| header_cell(c)).collect();
131 table.set_header(header);
132
133 for (idx, name) in columns.iter().enumerate() {
134 if let Some(type_name) = column_types.get(name) {
135 let align = alignment_for(type_name);
136 if let Some(col) = table.column_mut(idx) {
137 col.set_cell_alignment(align);
138 }
139 }
140 }
141
142 for (_id, record) in records {
143 let row: Vec<Cell> = columns
144 .iter()
145 .map(|col| match record.get(col.as_str()) {
146 Some(value) => Cell::new(rich_to_ansi(&value.render())),
147 None => Cell::new("—".bright_black().to_string()),
148 })
149 .collect();
150 table.add_row(row);
151 }
152
153 println!("{table}");
154 let n = records.len();
155 let label = if n == 1 { "record" } else { "records" };
156 println!("{}", format!("{n} {label}").dimmed());
157}
158
159pub fn render_records_typed<Id, V>(
167 records: &IndexMap<Id, Record<V>>,
168 id_field: Option<&str>,
169 column_types: &IndexMap<String, &'static str>,
170) where
171 Id: std::fmt::Display,
172 V: TerminalRender,
173{
174 if records.is_empty() {
175 println!("{}", "No records.".dimmed());
176 return;
177 }
178
179 let columns: Vec<String> = if !column_types.is_empty() {
180 column_types
181 .keys()
182 .filter(|k| Some(k.as_str()) != id_field)
183 .cloned()
184 .collect()
185 } else {
186 records
187 .values()
188 .next()
189 .unwrap()
190 .keys()
191 .filter(|k| k.as_str() != "id" && Some(k.as_str()) != id_field)
192 .cloned()
193 .collect()
194 };
195
196 let mut table = ComfyTable::new();
197 table
198 .load_preset(presets::UTF8_HORIZONTAL_ONLY)
199 .remove_style(TableComponent::HorizontalLines)
201 .remove_style(TableComponent::MiddleIntersections)
202 .remove_style(TableComponent::LeftBorderIntersections)
203 .remove_style(TableComponent::RightBorderIntersections)
204 .set_content_arrangement(ContentArrangement::Disabled);
208
209 let mut header = vec![header_cell("id")];
210 header.extend(columns.iter().map(|c| header_cell(c)));
211 table.set_header(header);
212
213 for (idx, name) in columns.iter().enumerate() {
214 if let Some(type_name) = column_types.get(name) {
215 let align = alignment_for(type_name);
216 if let Some(col) = table.column_mut(idx + 1) {
217 col.set_cell_alignment(align);
218 }
219 }
220 }
221
222 for (id, record) in records {
223 let id_cell = Cell::new(id.to_string()).add_attribute(Attribute::Bold);
224 let mut row = vec![id_cell];
225 for col in &columns {
226 let cell = match record.get(col.as_str()) {
227 Some(value) => Cell::new(rich_to_ansi(&value.render())),
228 None => Cell::new("—".bright_black().to_string()),
229 };
230 row.push(cell);
231 }
232 table.add_row(row);
233 }
234
235 println!("{table}");
236
237 let n = records.len();
238 let label = if n == 1 { "record" } else { "records" };
239 println!("{}", format!("{n} {label}").dimmed());
240}