1use std::sync::{Arc, Mutex, PoisonError};
63
64use rich::{Console, ConsoleOptions, Justify, Overflow, Renderable, Segment, Table, Text};
65
66use super::data::normalize;
67use super::stream::table_lines;
68use super::{frame_builders, style, Column, Frame, Value};
69
70pub const MAX_HEIGHT: usize = 10_000;
73
74pub const MAX_SAMPLE: usize = 10_000;
76
77pub const DEFAULT_SAMPLE: usize = 100;
79
80pub const DEFAULT_MAX_COLUMN_WIDTH: usize = 40;
82
83pub trait VirtualRows {
95 fn row_count(&self) -> Option<usize> {
97 None
98 }
99
100 fn row(&self, index: usize) -> Option<Vec<Value>>;
102
103 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
106 (start..start.saturating_add(len))
107 .map_while(|index| self.row(index))
108 .collect()
109 }
110}
111
112impl VirtualRows for [Vec<Value>] {
113 fn row_count(&self) -> Option<usize> {
114 Some(self.len())
115 }
116
117 fn row(&self, index: usize) -> Option<Vec<Value>> {
118 self.get(index).cloned()
119 }
120
121 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
122 let start = start.min(self.len());
123 let end = start.saturating_add(len).min(self.len());
124 self[start..end].to_vec()
125 }
126}
127
128impl VirtualRows for Vec<Vec<Value>> {
129 fn row_count(&self) -> Option<usize> {
130 Some(self.len())
131 }
132
133 fn row(&self, index: usize) -> Option<Vec<Value>> {
134 self.as_slice().row(index)
135 }
136
137 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
138 self.as_slice().rows(start, len)
139 }
140}
141
142macro_rules! forward_rows {
143 ($($ptr:ty),*) => {$(
144 impl<T: VirtualRows + ?Sized> VirtualRows for $ptr {
145 fn row_count(&self) -> Option<usize> {
146 (**self).row_count()
147 }
148
149 fn row(&self, index: usize) -> Option<Vec<Value>> {
150 (**self).row(index)
151 }
152
153 fn rows(&self, start: usize, len: usize) -> Vec<Vec<Value>> {
154 (**self).rows(start, len)
155 }
156 }
157 )*};
158}
159forward_rows!(&T, Box<T>, Arc<T>);
160
161pub struct FnRows<F> {
173 count: Option<usize>,
174 row: F,
175}
176
177impl<F> FnRows<F>
178where
179 F: Fn(usize) -> Option<Vec<Value>>,
180{
181 pub fn new(count: Option<usize>, row: F) -> Self {
183 FnRows { count, row }
184 }
185}
186
187impl<F> std::fmt::Debug for FnRows<F> {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 f.debug_struct("FnRows")
190 .field("count", &self.count)
191 .finish_non_exhaustive()
192 }
193}
194
195impl<F> VirtualRows for FnRows<F>
196where
197 F: Fn(usize) -> Option<Vec<Value>>,
198{
199 fn row_count(&self) -> Option<usize> {
200 self.count
201 }
202
203 fn row(&self, index: usize) -> Option<Vec<Value>> {
204 if self.count.is_some_and(|count| index >= count) {
205 return None;
206 }
207 (self.row)(index)
208 }
209}
210
211#[derive(Clone, Debug, PartialEq)]
213#[non_exhaustive]
214pub struct Page {
215 pub start: usize,
219 pub rows: Vec<Vec<Value>>,
221 pub total: Option<usize>,
224 pub more: bool,
226}
227
228impl Page {
229 pub fn range(&self) -> std::ops::Range<usize> {
231 self.start..self.start + self.rows.len()
232 }
233
234 pub fn position(&self, ascii: bool) -> String {
239 let number = |n: usize| crate::format::number(i64::try_from(n).unwrap_or(i64::MAX));
240 if self.rows.is_empty() {
241 return match self.start {
242 0 => "no rows".to_string(),
243 start => format!("no rows from {}", number(start + 1)),
244 };
245 }
246 let dash = if ascii { "-" } else { "–" };
247 let range = format!(
248 "rows {}{dash}{}",
249 number(self.start + 1),
250 number(self.start + self.rows.len())
251 );
252 match self.total {
253 Some(total) => format!("{range} of {}", number(total)),
254 None if self.more => {
255 format!(
256 "{range} of {}+",
257 number(self.start.saturating_add(self.rows.len() + 1))
258 )
259 }
260 None => range,
261 }
262 }
263}
264
265pub struct VirtualTable<S> {
271 source: S,
272 columns: Vec<Column>,
273 frame: Frame,
274 offset: usize,
275 height: usize,
276 widths: Vec<Option<usize>>,
277 sample: usize,
278 max_width: usize,
279 wrap: bool,
280 position: bool,
281 row_numbers: bool,
282 fit_window: bool,
283 null: Option<String>,
284 footnote: Option<String>,
285 sampled: Mutex<Option<Vec<usize>>>,
286}
287
288impl<S> std::fmt::Debug for VirtualTable<S> {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 f.debug_struct("VirtualTable")
291 .field("columns", &self.columns)
292 .field("offset", &self.offset)
293 .field("height", &self.height)
294 .field("widths", &self.widths)
295 .field("sample", &self.sample)
296 .finish_non_exhaustive()
297 }
298}
299
300frame_builders!([S] VirtualTable<S>);
301
302impl<S: VirtualRows> VirtualTable<S> {
303 pub fn new(columns: impl IntoIterator<Item = Column>, source: S) -> Self {
305 VirtualTable {
306 source,
307 columns: columns.into_iter().collect(),
308 frame: Frame::default(),
309 offset: 0,
310 height: 20,
311 widths: Vec::new(),
312 sample: DEFAULT_SAMPLE,
313 max_width: DEFAULT_MAX_COLUMN_WIDTH,
314 wrap: false,
315 position: true,
316 row_numbers: false,
317 fit_window: false,
318 null: None,
319 footnote: None,
320 sampled: Mutex::new(None),
321 }
322 }
323
324 pub fn offset(mut self, offset: usize) -> Self {
326 self.offset = offset;
327 self
328 }
329
330 pub fn height(mut self, rows: usize) -> Self {
332 self.set_height(rows);
333 self
334 }
335
336 pub fn widths(mut self, widths: impl IntoIterator<Item = impl Into<Option<usize>>>) -> Self {
340 self.widths = widths.into_iter().map(Into::into).collect();
341 self.resample();
342 self
343 }
344
345 pub fn sample(mut self, rows: usize) -> Self {
349 self.sample = rows.min(MAX_SAMPLE);
350 self.resample();
351 self
352 }
353
354 pub fn max_column_width(mut self, cells: usize) -> Self {
357 self.max_width = cells.max(1);
358 self.resample();
359 self
360 }
361
362 pub fn wrap(mut self, wrap: bool) -> Self {
365 self.wrap = wrap;
366 self
367 }
368
369 pub fn show_position(mut self, show: bool) -> Self {
371 self.position = show;
372 self
373 }
374
375 pub fn fit_window(mut self, fit: bool) -> Self {
379 self.fit_window = fit;
380 self
381 }
382
383 pub fn row_numbers(mut self, show: bool) -> Self {
385 self.row_numbers = show;
386 self
387 }
388
389 pub fn null_marker(mut self, marker: impl Into<String>) -> Self {
393 self.null = Some(marker.into());
394 self.resample();
395 self
396 }
397
398 pub fn footnote(mut self, note: impl Into<String>) -> Self {
401 self.footnote = Some(note.into());
402 self
403 }
404
405 pub fn set_offset(&mut self, offset: usize) {
407 self.offset = offset;
408 }
409
410 pub fn set_height(&mut self, rows: usize) {
412 self.height = rows.min(MAX_HEIGHT);
413 }
414
415 pub fn scroll_by(&mut self, rows: isize) {
418 let offset = self.offset.saturating_add_signed(rows);
419 self.offset = match self.max_offset() {
420 Some(max) => offset.min(max),
421 None => offset,
422 };
423 }
424
425 pub fn window_offset(&self) -> usize {
427 self.offset
428 }
429
430 pub fn window_height(&self) -> usize {
432 self.height
433 }
434
435 pub fn row_count(&self) -> Option<usize> {
437 self.source.row_count()
438 }
439
440 pub fn max_offset(&self) -> Option<usize> {
442 self.row_count()
443 .map(|count| count.saturating_sub(self.height))
444 }
445
446 pub fn columns(&self) -> &[Column] {
448 &self.columns
449 }
450
451 pub fn source(&self) -> &S {
453 &self.source
454 }
455
456 pub fn source_mut(&mut self) -> &mut S {
459 &mut self.source
460 }
461
462 pub fn resample(&self) {
464 *self.sampled.lock().unwrap_or_else(PoisonError::into_inner) = None;
465 }
466
467 pub fn page(&self) -> Page {
471 let count = self.source.row_count();
472 let start = match count {
473 Some(count) => self.offset.min(count.saturating_sub(self.height)),
474 None => self.offset,
475 };
476 let fetch = if count.is_some() {
478 self.height
479 } else {
480 self.height + 1
481 };
482 let mut rows = self.source.rows(start, fetch);
483 rows.truncate(fetch);
484 let more = match count {
485 Some(count) => start + rows.len().min(self.height) < count,
486 None => rows.len() > self.height,
487 };
488 rows.truncate(self.height);
489 let width = self.columns.len();
490 let rows: Vec<Vec<Value>> = rows.into_iter().map(|row| normalize(row, width)).collect();
491 let total = count.or_else(|| (!more && !rows.is_empty()).then(|| start + rows.len()));
492 Page {
493 start,
494 rows,
495 total,
496 more,
497 }
498 }
499
500 pub fn column_widths(&self) -> Vec<usize> {
503 let mut sampled = self.sampled.lock().unwrap_or_else(PoisonError::into_inner);
504 if let Some(widths) = &*sampled {
505 return widths.clone();
506 }
507 let mut widths: Vec<usize> = self
508 .columns
509 .iter()
510 .map(|column| Text::new(column.header()).measurement().1)
511 .collect();
512 for row in self
513 .source
514 .rows(0, self.sample)
515 .into_iter()
516 .take(self.sample)
517 {
518 let row = normalize(row, self.columns.len());
521 for ((width, column), value) in widths.iter_mut().zip(&self.columns).zip(&row) {
522 *width = (*width).max(self.cell(column, value).measurement().1);
523 }
524 }
525 let widths: Vec<usize> = widths
526 .into_iter()
527 .enumerate()
528 .map(|(index, sampled)| {
529 let fixed = self.widths.get(index).copied().flatten();
530 fixed
531 .or(self.columns[index].column_options().width)
532 .unwrap_or_else(|| sampled.clamp(1, self.max_width))
533 })
534 .collect();
535 *sampled = Some(widths.clone());
536 widths
537 }
538
539 pub fn chrome_lines(&self, console: &Console, width: usize) -> usize {
544 let options = console.options().update_width(width.max(1));
545 let empty = Page {
546 start: 0,
547 rows: Vec::new(),
548 total: None,
549 more: false,
550 };
551 let table = self.build(console, &empty);
552 table_lines(&table, console, &options).len()
553 + usize::from(self.position)
554 + usize::from(self.footnote.is_some())
555 }
556
557 pub fn to_table(&self, console: &Console) -> Table {
559 self.build(console, &self.page())
560 }
561
562 fn cell(&self, column: &Column, value: &Value) -> Text {
563 match (&self.null, value) {
564 (Some(marker), Value::Null) => Text::new(marker.clone()),
565 _ => column.cell(value),
566 }
567 }
568
569 fn build(&self, console: &Console, page: &Page) -> Table {
570 let mut widths = self.column_widths();
571 if self.fit_window {
572 for (index, width) in widths.iter_mut().enumerate() {
573 let column = &self.columns[index];
574 let fixed = self.widths.get(index).copied().flatten();
575 if fixed.or(column.column_options().width).is_some() {
576 continue;
577 }
578 for row in &page.rows {
579 let cell = self.cell(column, &row[index]).measurement().1;
580 *width = (*width).max(cell.min(self.max_width));
581 }
582 }
583 }
584 let mut columns: Vec<Column> = Vec::with_capacity(self.columns.len() + 1);
585 if self.row_numbers {
586 let last = page
587 .total
588 .unwrap_or(page.start + page.rows.len())
589 .max(page.start + page.rows.len());
590 let options = rich::ColumnOptions {
591 justify: Justify::Right,
592 width: Some(last.max(1).to_string().len()),
593 no_wrap: true,
594 style: style(console, "table.row_number"),
595 ..Default::default()
596 };
597 columns.push(Column::new("#").options(options));
598 }
599 for (column, width) in self.columns.iter().zip(&widths) {
600 let mut options = column.column_options().clone();
601 options.width = Some(*width);
602 if !self.wrap {
603 options.no_wrap = true;
604 if options.overflow == Overflow::Fold {
605 options.overflow = Overflow::Ellipsis;
606 }
607 }
608 columns.push(Column::new(column.header()).options(options));
609 }
610 let headers: Vec<Text> = columns.iter().map(|c| Text::new(c.header())).collect();
611 let mut table = self.frame.table(&columns, &headers, true, true);
612 let null_style = style(console, "table.null");
613 for (index, row) in page.rows.iter().enumerate() {
614 let mut cells = Vec::with_capacity(columns.len());
615 if self.row_numbers {
616 cells.push(Text::new((page.start + index + 1).to_string()));
617 }
618 for (column, value) in self.columns.iter().zip(row) {
619 let mut text = self.cell(column, value);
620 if self.null.is_some() && matches!(value, Value::Null) {
621 let len = text.plain().len();
622 text.stylize(null_style.clone(), 0, len);
623 }
624 cells.push(text);
625 }
626 table.add_row_text(cells);
627 }
628 table
629 }
630
631 fn note_lines(
632 &self,
633 console: &Console,
634 options: &ConsoleOptions,
635 text: String,
636 ) -> Vec<Vec<Segment>> {
637 let text = Text::styled(text, style(console, "table.position"))
638 .no_wrap(true)
639 .overflow(Overflow::Ellipsis);
640 let mut options = options.clone();
641 options.height = None;
642 console.render_lines(&text, &options, false)
643 }
644
645 fn render_lines(&self, console: &Console, options: &ConsoleOptions) -> Vec<Vec<Segment>> {
646 let page = self.page();
647 let table = self.build(console, &page);
648 let mut lines = table_lines(&table, console, options);
649 if self.position {
650 lines.extend(self.note_lines(console, options, page.position(console.ascii_only())));
651 }
652 if let Some(note) = &self.footnote {
653 lines.extend(self.note_lines(console, options, note.clone()));
654 }
655 lines
656 }
657}
658
659impl<S: VirtualRows> Renderable for VirtualTable<S> {
660 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
661 crate::event::flatten(self.render_lines(console, options))
662 }
663
664 fn measure(&self, console: &Console, options: &ConsoleOptions) -> rich::measure::Measurement {
667 let page = self.page();
668 let mut measurement = self.build(console, &page).measure(console, options);
669 let notes = self
670 .position
671 .then(|| page.position(console.ascii_only()))
672 .into_iter()
673 .chain(self.footnote.clone());
674 for note in notes {
675 measurement.maximum = measurement.maximum.max(Text::new(note).cell_len());
676 }
677 measurement
678 }
679}
680
681impl<S: VirtualRows> crate::a11y::AccessibleText for VirtualTable<S> {
682 fn accessible_text(&self, width: usize) -> String {
683 let console = Console::builder().width(width.max(1)).build();
684 self.to_table(&console).accessible_text(width)
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 fn render(table: &impl Renderable, width: usize) -> String {
693 Console::builder()
694 .width(width)
695 .color_system(None)
696 .build()
697 .render_export(table)
698 }
699
700 fn numbered(count: Option<usize>) -> FnRows<impl Fn(usize) -> Option<Vec<Value>>> {
701 FnRows::new(count, |i| {
702 Some(vec![Value::from(i), format!("r{i}").into()])
703 })
704 }
705
706 #[test]
707 fn an_offset_past_the_end_shows_the_last_window() {
708 let rows: Vec<Vec<Value>> = (0..10).map(|i| vec![Value::from(i)]).collect();
709 let table = VirtualTable::new([Column::new("n")], rows)
710 .offset(50)
711 .height(3);
712 let page = table.page();
713 assert_eq!(page.range(), 7..10);
714 assert_eq!(page.total, Some(10));
715 assert!(!page.more);
716 assert_eq!(page.position(false), "rows 8–10 of 10");
717 }
718
719 #[test]
720 fn an_uncounted_source_reports_what_it_knows() {
721 let rows = FnRows::new(None, |i| (i < 5).then(|| vec![Value::from(i)]));
722 let table = VirtualTable::new([Column::new("n")], rows).height(2);
723 assert_eq!(table.page().position(false), "rows 1–2 of 3+");
724 let mut table = table;
725 table.set_offset(3);
726 let page = table.page();
727 assert_eq!((page.total, page.more), (Some(5), false));
728 assert_eq!(page.position(true), "rows 4-5 of 5");
729 table.set_offset(9);
730 assert_eq!(table.page().position(false), "no rows from 10");
731 }
732
733 #[test]
734 fn ragged_rows_are_padded_and_cut() {
735 let rows = vec![
736 vec![Value::from("a")],
737 vec![Value::from("b"), Value::from(2), Value::from("extra")],
738 vec![],
739 ];
740 let table = VirtualTable::new([Column::new("x"), Column::new("y")], rows);
741 let page = table.page();
742 assert!(page.rows.iter().all(|row| row.len() == 2));
743 let out = render(&table, 30);
744 assert!(out.contains("│ b │ 2 │"), "{out}");
745 }
746
747 #[test]
748 fn widths_hold_still_while_scrolling() {
749 let mut table = VirtualTable::new([Column::new("n"), Column::new("s")], numbered(None))
750 .sample(10)
751 .height(2);
752 let first = render(&table, 40);
753 table.scroll_by(500);
754 let later = render(&table, 40);
755 let width = |out: &str| out.lines().next().unwrap().chars().count();
756 assert_eq!(width(&first), width(&later));
757 assert!(later.contains("│ … │ r… │"), "{later}");
759 assert_eq!(table.column_widths(), [1, 2]);
760 }
761
762 #[test]
763 fn fixed_widths_win_and_the_cap_bounds_samples() {
764 let long = vec![vec![Value::from("x".repeat(500)), Value::from("y")]];
765 let table = VirtualTable::new([Column::new("a"), Column::new("b")], long.clone())
766 .max_column_width(8);
767 assert_eq!(table.column_widths(), [8, 1]);
768 let table =
769 VirtualTable::new([Column::new("a"), Column::new("b")], long).widths([None, Some(5)]);
770 assert_eq!(table.column_widths(), [40, 5]);
771 }
772
773 #[test]
774 fn scrolling_stops_at_both_ends() {
775 let mut table = VirtualTable::new([Column::new("n")], numbered(Some(100))).height(10);
776 table.scroll_by(-5);
777 assert_eq!(table.window_offset(), 0);
778 table.scroll_by(1_000);
779 assert_eq!(table.window_offset(), 90);
780 assert_eq!(table.max_offset(), Some(90));
781 }
782
783 #[test]
784 fn nulls_can_be_marked_apart_from_empty_text() {
785 let rows = vec![vec![Value::Null], vec![Value::from("")]];
786 let table = VirtualTable::new([Column::new("v")], rows)
787 .null_marker("NULL")
788 .show_position(false);
789 assert_eq!(
790 render(&table, 20),
791 "┏━━━━━━┓\n┃ v ┃\n┡━━━━━━┩\n│ NULL │\n│ │\n└──────┘\n"
792 );
793 }
794
795 #[test]
796 fn row_numbers_and_footnote() {
797 let table = VirtualTable::new([Column::new("s")], numbered(Some(1_000)))
798 .offset(998)
799 .height(2)
800 .row_numbers(true)
801 .widths([3])
802 .footnote("(1,000 rows)");
803 let out = render(&table, 40);
804 assert!(out.contains("│ 999 │ 998 │"), "{out}");
805 assert!(
806 out.ends_with("rows 999–1,000 of 1,000\n(1,000 rows)\n"),
807 "{out}"
808 );
809 }
810
811 #[test]
812 fn chrome_lines_count_everything_but_rows() {
813 let table = VirtualTable::new([Column::new("n")], numbered(Some(100))).height(5);
814 let console = Console::builder().width(40).build();
815 let out = render(&table, 40);
816 assert_eq!(out.lines().count(), 5 + table.chrome_lines(&console, 40));
817 }
818
819 #[test]
820 fn an_empty_source_renders_a_header_and_no_rows() {
821 let table = VirtualTable::new([Column::new("n")], Vec::<Vec<Value>>::new());
822 let out = render(&table, 20);
823 assert!(out.ends_with("no rows\n"), "{out}");
824 let table = VirtualTable::new(Vec::<Column>::new(), numbered(Some(3)));
825 render(&table, 20);
826 }
827}