1use colored::Colorize;
2use itertools::Itertools;
3use tabled::{
4 Table,
5 builder::Builder,
6 settings::{
7 Alignment, Modify, Panel, Span,
8 object::{Cell, Columns, Object, Rows},
9 style::{HorizontalLine, Style},
10 themes::BorderCorrection,
11 width::Width,
12 },
13};
14
15use crate::utils::normalize_tabled_separator_rows;
16
17use super::{
18 display::{StyledText, TextColor},
19 status_update::{
20 DiscreteMaxWeightDetailsSection, MainResultsRowGroupKind, MainResultsSection,
21 MaxWeightDetailsSection, StatisticsScope, StatisticsSection, StatusUpdate,
22 },
23};
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub struct TabledRenderOptions {
27 pub max_table_width: usize,
28 pub show_statistics: bool,
29 pub show_max_weight_details: bool,
30 pub show_top_discrete_grid: bool,
31 pub show_discrete_contributions_sum: bool,
32 pub show_max_weight_info_for_discrete_bins: bool,
33}
34
35impl Default for TabledRenderOptions {
36 fn default() -> Self {
37 Self {
38 max_table_width: 250,
39 show_statistics: true,
40 show_max_weight_details: true,
41 show_top_discrete_grid: false,
42 show_discrete_contributions_sum: false,
43 show_max_weight_info_for_discrete_bins: false,
44 }
45 }
46}
47
48struct StatusTable {
49 table: Table,
50 separator_after_rows: Vec<usize>,
51 hidden_vertical_boundaries: Vec<usize>,
52 full_row_vertical_count: usize,
53 suppress_header_middle_separator: bool,
54 suppress_header_tail_separator: bool,
55}
56
57fn render_styled_text(text: &StyledText) -> String {
58 text.spans
59 .iter()
60 .map(|span| {
61 let mut styled = match span.style.color {
62 Some(TextColor::Green) => span.text.green(),
63 Some(TextColor::Blue) => span.text.blue(),
64 Some(TextColor::Red) => span.text.red(),
65 Some(TextColor::Yellow) => span.text.yellow(),
66 Some(TextColor::Pink) => span.text.bright_magenta(),
67 None => span.text.normal(),
68 };
69 if span.style.dimmed {
70 styled = styled.dimmed();
71 }
72 if span.style.bold {
73 styled = styled.bold();
74 }
75 styled.to_string()
76 })
77 .collect()
78}
79
80fn render_styled_text_single_line(text: &StyledText) -> String {
81 render_styled_text(text).replace('\n', " ")
82}
83
84fn maybe_render<T>(value: &Option<super::display::DisplayField<T>>) -> String {
85 value
86 .as_ref()
87 .map(|value| render_styled_text(&value.display))
88 .unwrap_or_default()
89}
90
91fn status_group_separator() -> tabled::settings::style::VerticalLine<
92 tabled::settings::style::On,
93 tabled::settings::style::On,
94 (),
95> {
96 tabled::settings::style::VerticalLine::new('│')
97 .top('┬')
98 .bottom('┴')
99}
100
101fn suppress_iteration_header_separators(
102 rendered: &str,
103 suppress_middle_separator: bool,
104 suppress_tail_separator: bool,
105) -> String {
106 rendered
107 .lines()
108 .enumerate()
109 .map(|(line_index, line)| {
110 if line_index != 1 {
111 return line.to_string();
112 }
113
114 let vertical_positions = line.match_indices('│').map(|(idx, _)| idx).collect_vec();
115 if vertical_positions.len() < 3 {
116 return line.to_string();
117 }
118
119 let mut updated = line.to_string();
120 if suppress_tail_separator {
121 let suppressed_index = vertical_positions[vertical_positions.len() - 2];
122 updated.replace_range(suppressed_index..suppressed_index + '│'.len_utf8(), " ");
123 }
124 if suppress_middle_separator && vertical_positions.len() >= 4 {
125 let suppressed_index = vertical_positions[1];
126 updated.replace_range(suppressed_index..suppressed_index + '│'.len_utf8(), " ");
127 }
128 updated
129 })
130 .join("\n")
131}
132
133fn hide_hidden_vertical_boundaries(
134 rendered: &str,
135 hidden_vertical_boundaries: &[usize],
136 full_row_vertical_count: usize,
137) -> String {
138 rendered
139 .lines()
140 .map(|line| {
141 let vertical_positions = line.match_indices('│').map(|(idx, _)| idx).collect_vec();
142 if vertical_positions.len() != full_row_vertical_count {
143 return line.to_string();
144 }
145
146 let mut updated = line.to_string();
147 for boundary in hidden_vertical_boundaries.iter().rev() {
148 let Some(position) = vertical_positions.get(boundary + 1).copied() else {
149 continue;
150 };
151 updated.replace_range(position..position + '│'.len_utf8(), " ");
152 }
153 updated
154 })
155 .join("\n")
156}
157
158fn insert_separator_rows(rendered: &str, separator_after_rows: &[usize]) -> String {
159 if separator_after_rows.is_empty() {
160 return rendered.to_string();
161 }
162
163 let mut lines = rendered.lines().map(str::to_string).collect_vec();
164 let width = lines
165 .first()
166 .map(|line| line.chars().count())
167 .unwrap_or_default();
168 if width < 2 {
169 return rendered.to_string();
170 }
171 let separator = format!("├{}┤", "─".repeat(width - 2));
172
173 for (inserted, row) in separator_after_rows.iter().enumerate() {
174 let insert_at = *row + 2 + inserted;
175 lines.insert(insert_at, separator.clone());
176 }
177
178 lines.join("\n")
179}
180
181fn suppress_spanned_metadata_header_separators(rendered: &str) -> String {
182 rendered
183 .lines()
184 .map(|line| {
185 if !line.contains("χ²/dof") || !line.contains("mwi") {
186 return line.to_string();
187 }
188
189 let mut updated = line.to_string();
190 let separators_to_remove = [("χ²/dof", "mwi"), ("Δ [σ]", "Δ [%]")];
191
192 for (left_label, right_label) in separators_to_remove {
193 let Some(left_start) = updated.find(left_label) else {
194 continue;
195 };
196 let Some(right_start) = updated.find(right_label) else {
197 continue;
198 };
199 let left_end = left_start + left_label.len();
200 if let Some((separator_index, _)) = updated[left_end..right_start]
201 .match_indices('│')
202 .next_back()
203 {
204 let separator_index = left_end + separator_index;
205 updated.replace_range(separator_index..separator_index + '│'.len_utf8(), " ");
206 }
207 }
208
209 updated
210 })
211 .join("\n")
212}
213
214fn render_tables_with_shared_width(mut tables: Vec<StatusTable>, max_table_width: usize) -> String {
215 let max_width = tables
216 .iter()
217 .map(|table| table.table.total_width())
218 .max()
219 .unwrap_or(0)
220 .min(max_table_width.max(1));
221
222 for table in &mut tables {
223 if table.table.total_width() < max_width {
224 table.table.with(Width::increase(max_width));
225 }
226 }
227
228 tables
229 .into_iter()
230 .map(|table| {
231 let mut rendered = table.table.to_string();
232 rendered = hide_hidden_vertical_boundaries(
233 &rendered,
234 &table.hidden_vertical_boundaries,
235 table.full_row_vertical_count,
236 );
237 if table.suppress_header_middle_separator || table.suppress_header_tail_separator {
238 rendered = suppress_iteration_header_separators(
239 &rendered,
240 table.suppress_header_middle_separator,
241 table.suppress_header_tail_separator,
242 );
243 }
244 rendered = suppress_spanned_metadata_header_separators(&rendered);
245 rendered = insert_separator_rows(&rendered, &table.separator_after_rows);
246 normalize_tabled_separator_rows(&rendered)
247 })
248 .collect_vec()
249 .join("\n")
250}
251
252fn build_main_results_table(
253 section: &MainResultsSection,
254 options: &TabledRenderOptions,
255) -> StatusTable {
256 let visible_row_groups = section
257 .row_groups
258 .iter()
259 .filter(|group| match group.kind {
260 MainResultsRowGroupKind::All => true,
261 MainResultsRowGroupKind::Sum => options.show_discrete_contributions_sum,
262 MainResultsRowGroupKind::Bins => options.show_top_discrete_grid,
263 })
264 .collect_vec();
265 let show_discrete_columns = section.has_discrete_columns
266 && (options.show_top_discrete_grid || options.show_discrete_contributions_sum);
267 let slot_block_width = if show_discrete_columns { 5 } else { 2 };
268 let metadata_columns = if section.has_target_columns { 4 } else { 2 };
269 let n_columns = 2 + section.slot_headers.len() * slot_block_width + metadata_columns;
270
271 let mut builder = Builder::new();
272 let mut first_row = vec![
273 render_styled_text(§ion.header_left),
274 String::new(),
275 render_styled_text(§ion.header_middle),
276 ];
277 first_row.resize(n_columns, String::new());
278 first_row[n_columns - 2] = render_styled_text(§ion.header_tail);
279 builder.push_record(first_row);
280
281 let mut header_row = vec![
282 render_styled_text_single_line(§ion.contribution_header),
283 String::new(),
284 ];
285 for slot_header in §ion.slot_headers {
286 header_row.push(render_styled_text(slot_header));
287 header_row.extend(std::iter::repeat_n(String::new(), slot_block_width - 1));
288 }
289 header_row.extend(
290 section
291 .metadata_headers()
292 .into_iter()
293 .map(|header| render_styled_text(&header)),
294 );
295 builder.push_record(header_row);
296
297 for group in &visible_row_groups {
298 for row in &group.rows {
299 let mut record = vec![
300 render_styled_text(&row.contribution.display),
301 render_styled_text(&row.component.display),
302 ];
303 for slot_cells in &row.slot_cells {
304 record.push(maybe_render(&slot_cells.value));
305 record.push(maybe_render(&slot_cells.relative_error));
306 if show_discrete_columns {
307 record.push(maybe_render(&slot_cells.sample_fraction));
308 record.push(maybe_render(&slot_cells.sample_count));
309 record.push(maybe_render(&slot_cells.target_pdf));
310 }
311 }
312 record.push(maybe_render(&row.chi_sq));
313 record.push(maybe_render(&row.max_weight_impact));
314 if section.has_target_columns {
315 record.push(maybe_render(&row.delta_sigma));
316 record.push(maybe_render(&row.delta_percent));
317 }
318 builder.push_record(record);
319 }
320 }
321
322 let mut table = builder.build();
323 table.modify((0, 0), Span::column(2));
324 table.modify((0, 2), Span::column((n_columns - 4) as isize));
325 table.modify((0, n_columns - 2), Span::column(2));
326 table.modify((1, 0), Span::column(2));
327 for (slot_index, _) in section.slot_headers.iter().enumerate() {
328 table.modify(
329 (1, 2 + slot_index * slot_block_width),
330 Span::column(slot_block_width as isize),
331 );
332 }
333
334 let first_metadata_column = 2 + section.slot_headers.len() * slot_block_width;
335
336 let mut separator_rows = vec![1usize, 2usize];
337 let mut row_offset = 2usize;
338 for (group_index, group) in visible_row_groups.iter().enumerate() {
339 row_offset += group.rows.len();
340 if group_index + 1 < visible_row_groups.len() {
341 separator_rows.push(row_offset);
342 }
343 }
344 separator_rows.sort_unstable();
345 separator_rows.dedup();
346
347 table.with(Style::rounded().remove_horizontals());
348 table.with(BorderCorrection::span());
349 table.with(Modify::new(Rows::new(0..)).with(Alignment::left()));
350 table.with(Modify::new(Cell::new(0, 2)).with(Alignment::center()));
351 table.with(Modify::new(Cell::new(0, n_columns - 2)).with(Alignment::center()));
352 table.with(Modify::new(Rows::new(1..2)).with(Alignment::center()));
353
354 let mut hidden_vertical_boundaries = vec![0usize];
355 for slot_index in 0..section.slot_headers.len() {
356 let block_start = 2 + slot_index * slot_block_width;
357 hidden_vertical_boundaries.extend(block_start..(block_start + slot_block_width - 1));
358 }
359 hidden_vertical_boundaries.push(first_metadata_column);
360 if section.has_target_columns {
361 hidden_vertical_boundaries.push(first_metadata_column + 2);
362 }
363
364 StatusTable {
365 table,
366 separator_after_rows: separator_rows
367 .into_iter()
368 .map(|row| row.saturating_sub(1))
369 .collect(),
370 hidden_vertical_boundaries,
371 full_row_vertical_count: n_columns + 1,
372 suppress_header_middle_separator: true,
373 suppress_header_tail_separator: true,
374 }
375}
376
377fn build_max_weight_details_table(section: &MaxWeightDetailsSection) -> StatusTable {
378 let mut builder = Builder::new();
379 let headers = section.headers();
380 builder.push_record([
381 render_styled_text(&headers[0]),
382 render_styled_text(&headers[1]),
383 render_styled_text(&headers[2]),
384 render_styled_text(&headers[3]),
385 ]);
386
387 for group in §ion.rows_by_slot {
388 for row in group {
389 builder.push_record([
390 render_styled_text(&row.slot.display),
391 render_styled_text(&row.component_sign.display),
392 render_styled_text(&row.max_eval.display),
393 render_styled_text(&row.coordinates.display),
394 ]);
395 }
396 }
397
398 let mut table = builder.build();
399 table.modify((0, 0), Span::column(2));
400 table.with(Panel::header(render_styled_text(§ion.title())));
401 table.with(Style::rounded().remove_horizontals());
402 table.with(BorderCorrection::span());
403 table.with(Modify::new(Rows::new(0..2)).with(Alignment::center()));
404 table.with(Modify::new(Rows::new(2..)).with(Alignment::left()));
405
406 let mut separator_rows = vec![0usize, 1usize];
407 let mut row_offset = 1usize;
408 for (group_index, group) in section.rows_by_slot.iter().enumerate() {
409 row_offset += group.len();
410 if group_index + 1 < section.rows_by_slot.len() {
411 separator_rows.push(row_offset);
412 }
413 }
414
415 StatusTable {
416 table,
417 separator_after_rows: separator_rows,
418 hidden_vertical_boundaries: vec![0],
419 full_row_vertical_count: 5,
420 suppress_header_middle_separator: false,
421 suppress_header_tail_separator: false,
422 }
423}
424
425fn build_discrete_max_weight_details_table(
426 section: &DiscreteMaxWeightDetailsSection,
427) -> StatusTable {
428 let mut builder = Builder::new();
429 let mut header = section
430 .summary_headers()
431 .iter()
432 .map(render_styled_text_single_line)
433 .collect_vec();
434 header.push(render_styled_text(§ion.coordinates_header()));
435 builder.push_record(header);
436
437 for group in §ion.row_groups {
438 for row in group {
439 let mut record = vec![
440 render_styled_text(&row.contribution.display),
441 render_styled_text(&row.component_sign.display),
442 ];
443 record.extend(row.slot_values.iter().map(|value| {
444 value
445 .as_ref()
446 .map(|value| render_styled_text(&value.display))
447 .unwrap_or_default()
448 }));
449 record.push(
450 row.slot_coordinates
451 .iter()
452 .map(|entry| {
453 format!(
454 "{}: {}",
455 render_styled_text(&entry.slot.display),
456 render_styled_text(&entry.coordinates.display),
457 )
458 })
459 .join("\n"),
460 );
461 builder.push_record(record);
462 }
463 }
464
465 let mut table = builder.build();
466 table.modify((0, 0), Span::column(2));
467 table.with(Panel::header(render_styled_text(§ion.title())));
468
469 let mut separator_rows = vec![1usize, 2usize];
470 let mut row_offset = 1usize;
471 for (group_index, group) in section.row_groups.iter().enumerate() {
472 row_offset += group.len();
473 if group_index + 1 < section.row_groups.len() {
474 separator_rows.push(row_offset + 1);
475 }
476 }
477
478 table.with(Style::rounded().remove_horizontals());
479 table.with(BorderCorrection::span());
480 table.with(Modify::new(Rows::new(0..2)).with(Alignment::center()));
481 table.with(Modify::new(Rows::new(2..)).with(Alignment::left()));
482
483 StatusTable {
484 table,
485 separator_after_rows: separator_rows
486 .into_iter()
487 .map(|row| row.saturating_sub(1))
488 .collect(),
489 hidden_vertical_boundaries: vec![0],
490 full_row_vertical_count: section.slot_headers.len() + 4,
491 suppress_header_middle_separator: false,
492 suppress_header_tail_separator: false,
493 }
494}
495
496fn build_statistics_table(section: &StatisticsSection) -> StatusTable {
497 let rows = section.table_rows(StatisticsScope::Global);
498 let mut builder = Builder::new();
499 for row in &rows {
500 let mut record = vec![render_styled_text(&row.row_label)];
501 for entry in &row.entries {
502 record.push(render_styled_text(&entry.label));
503 record.push(render_styled_text(&entry.value));
504 }
505 builder.push_record(record);
506 }
507
508 let mut table = builder.build();
509 table.with(Panel::header(render_styled_text(
510 §ion.statistics_title(StatisticsScope::Global),
511 )));
512 table.with(
513 Style::rounded()
514 .remove_horizontals()
515 .verticals([
516 (1, status_group_separator()),
517 (3, status_group_separator()),
518 (5, status_group_separator()),
519 (7, status_group_separator()),
520 ])
521 .horizontals([(
522 1,
523 HorizontalLine::new('─')
524 .intersection('┬')
525 .left('├')
526 .right('┤'),
527 )])
528 .remove_vertical(),
529 );
530 table.with(BorderCorrection::span());
531 table.with(Modify::new(Rows::new(0..1)).with(Alignment::center()));
532 table.with(Modify::new(Rows::new(1..)).with(Alignment::left()));
533 for column in [1usize, 3, 5, 7] {
534 table.with(
535 Modify::new(Rows::new(1..).intersect(Columns::one(column))).with(Alignment::right()),
536 );
537 }
538
539 StatusTable {
540 table,
541 separator_after_rows: Vec::new(),
542 hidden_vertical_boundaries: Vec::new(),
543 full_row_vertical_count: 0,
544 suppress_header_middle_separator: false,
545 suppress_header_tail_separator: false,
546 }
547}
548
549pub(crate) fn render_status_update(update: &StatusUpdate, options: &TabledRenderOptions) -> String {
550 let mut tables = vec![build_main_results_table(&update.main_results, options)];
551 if options.show_max_weight_details
552 && let Some(section) = update.max_weight_details.as_ref()
553 {
554 tables.push(build_max_weight_details_table(section));
555 }
556 if options.show_max_weight_details
557 && options.show_max_weight_info_for_discrete_bins
558 && let Some(section) = update.discrete_max_weight_details.as_ref()
559 {
560 tables.push(build_discrete_max_weight_details_table(section));
561 }
562 if options.show_statistics
563 && let Some(section) = update.statistics.as_ref()
564 {
565 tables.push(build_statistics_table(section));
566 }
567
568 render_tables_with_shared_width(tables, options.max_table_width)
569}