1//! The module contains a [`CompactGrid`] structure,
2//! which is a relatively strict grid.
3
4use core::{
5 borrow::Borrow,
6 fmt::{self, Display, Write},
7};
8
9use crate::{
10 ansi::{ANSIFmt, ANSIStr},
11 colors::{Colors, NoColors},
12 config::{AlignmentHorizontal, Borders, HorizontalLine, Indent, Sides},
13 dimension::Dimension,
14 records::{IntoRecords, Records},
15 util::string::string_width,
16};
17
18use crate::config::compact::CompactConfig;
19
20/// Grid provides a set of methods for building a text-based table.
21#[derive(Debug, Clone)]
22pub struct CompactGrid<R, D, G, C> {
23 records: R,
24 config: G,
25 dimension: D,
26 colors: C,
27}
28
29impl<R, D, G> CompactGrid<R, D, G, NoColors> {
30 /// The new method creates a grid instance with default styles.
31 pub fn new(records: R, dimension: D, config: G) -> Self {
32 CompactGrid {
33 records,
34 config,
35 dimension,
36 colors: NoColors,
37 }
38 }
39}
40
41impl<R, D, G, C> CompactGrid<R, D, G, C> {
42 /// Sets colors map.
43 pub fn with_colors<Colors>(self, colors: Colors) -> CompactGrid<R, D, G, Colors> {
44 CompactGrid {
45 records: self.records,
46 config: self.config,
47 dimension: self.dimension,
48 colors,
49 }
50 }
51
52 /// Builds a table.
53 pub fn build<F>(self, mut f: F) -> fmt::Result
54 where
55 R: Records,
56 <R::Iter as IntoRecords>::Cell: AsRef<str>,
57 D: Dimension,
58 C: Colors,
59 G: Borrow<CompactConfig>,
60 F: Write,
61 {
62 if self.records.count_columns() == 0 {
63 return Ok(());
64 }
65
66 let config = self.config.borrow();
67 print_grid(&mut f, self.records, config, &self.dimension, &self.colors)
68 }
69
70 /// Builds a table into string.
71 ///
72 /// Notice that it consumes self.
73 #[cfg(feature = "std")]
74 #[allow(clippy::inherent_to_string)]
75 pub fn to_string(self) -> String
76 where
77 R: Records,
78 <R::Iter as IntoRecords>::Cell: AsRef<str>,
79 D: Dimension,
80 G: Borrow<CompactConfig>,
81 C: Colors,
82 {
83 let mut buf = String::new();
84 self.build(&mut buf).expect("It's guaranteed to never happen otherwise it's considered an stdlib error or impl error");
85 buf
86 }
87}
88
89impl<R, D, G, C> Display for CompactGrid<R, D, G, C>
90where
91 for<'a> &'a R: Records,
92 for<'a> <<&'a R as Records>::Iter as IntoRecords>::Cell: AsRef<str>,
93 D: Dimension,
94 G: Borrow<CompactConfig>,
95 C: Colors,
96{
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 let records: &R = &self.records;
99 let config: &CompactConfig = self.config.borrow();
100
101 print_grid(f, records, cfg:config, &self.dimension, &self.colors)
102 }
103}
104
105fn print_grid<F, R, D, C>(
106 f: &mut F,
107 records: R,
108 cfg: &CompactConfig,
109 dims: &D,
110 colors: &C,
111) -> fmt::Result
112where
113 F: Write,
114 R: Records,
115 <R::Iter as IntoRecords>::Cell: AsRef<str>,
116 D: Dimension,
117 C: Colors,
118{
119 let count_columns = records.count_columns();
120 let count_rows = records.hint_count_rows();
121
122 if count_columns == 0 || matches!(count_rows, Some(0)) {
123 return Ok(());
124 }
125
126 let mut records = records.iter_rows().into_iter();
127 let records_first = match records.next() {
128 Some(row) => row,
129 None => return Ok(()),
130 };
131
132 let wtotal = total_width(cfg, dims, count_columns);
133
134 let borders_chars = cfg.get_borders();
135 let borders_colors = cfg.get_borders_color();
136
137 let horizontal_borders = create_horizontal(borders_chars);
138 let horizontal_colors = create_horizontal_colors(borders_colors);
139
140 let vertical_borders = create_vertical_borders(borders_chars, borders_colors);
141
142 let margin = create_margin(cfg);
143 let padding = create_padding(cfg);
144 let alignment = cfg.get_alignment_horizontal();
145
146 let mut new_line = false;
147
148 if margin.top.space.size > 0 {
149 let width_total = wtotal + margin.left.space.size + margin.right.space.size;
150 let indent = ColoredIndent::new(width_total, margin.top.space.fill, margin.top.color);
151 print_indent_lines(f, indent)?;
152 new_line = true;
153 }
154
155 if borders_chars.has_top() {
156 if new_line {
157 f.write_char('\n')?
158 }
159
160 let borders = create_horizontal_top(borders_chars);
161 let borders_colors = create_horizontal_top_colors(borders_colors);
162 print_horizontal_line(f, dims, &borders, &borders_colors, &margin, count_columns)?;
163
164 new_line = true;
165 }
166
167 if borders_chars.has_horizontal() {
168 if new_line {
169 f.write_char('\n')?;
170 }
171
172 let cells = records_first.into_iter();
173 print_grid_row(
174 f,
175 cells,
176 count_columns,
177 dims,
178 colors,
179 &margin,
180 &padding,
181 &vertical_borders,
182 alignment,
183 0,
184 )?;
185
186 for (row, cells) in records.enumerate() {
187 f.write_char('\n')?;
188
189 print_horizontal_line(
190 f,
191 dims,
192 &horizontal_borders,
193 &horizontal_colors,
194 &margin,
195 count_columns,
196 )?;
197
198 f.write_char('\n')?;
199
200 let cells = cells.into_iter();
201 print_grid_row(
202 f,
203 cells,
204 count_columns,
205 dims,
206 colors,
207 &margin,
208 &padding,
209 &vertical_borders,
210 alignment,
211 row + 1,
212 )?;
213 }
214 } else {
215 if new_line {
216 f.write_char('\n')?;
217 }
218
219 print_grid_row(
220 f,
221 records_first.into_iter(),
222 count_columns,
223 dims,
224 colors,
225 &margin,
226 &padding,
227 &vertical_borders,
228 alignment,
229 0,
230 )?;
231
232 for (row, cells) in records.enumerate() {
233 f.write_char('\n')?;
234
235 print_grid_row(
236 f,
237 cells.into_iter(),
238 count_columns,
239 dims,
240 colors,
241 &margin,
242 &padding,
243 &vertical_borders,
244 alignment,
245 row + 1,
246 )?;
247 }
248 }
249
250 if borders_chars.has_bottom() {
251 f.write_char('\n')?;
252
253 let borders = create_horizontal_bottom(borders_chars);
254 let colors = create_horizontal_bottom_colors(borders_colors);
255 print_horizontal_line(f, dims, &borders, &colors, &margin, count_columns)?;
256 }
257
258 if cfg.get_margin().bottom.size > 0 {
259 f.write_char('\n')?;
260
261 let width_total = wtotal + margin.left.space.size + margin.right.space.size;
262 let indent = ColoredIndent::new(width_total, margin.bottom.space.fill, margin.bottom.color);
263 print_indent_lines(f, indent)?;
264 }
265
266 Ok(())
267}
268
269fn create_margin(cfg: &CompactConfig) -> Sides<ColoredIndent> {
270 let margin: &Sides = cfg.get_margin();
271 let margin_color: &Sides> = cfg.get_margin_color();
272 Sides::new(
273 left:ColoredIndent::from_indent(margin.left, margin_color.left),
274 right:ColoredIndent::from_indent(margin.right, margin_color.right),
275 top:ColoredIndent::from_indent(margin.top, margin_color.top),
276 bottom:ColoredIndent::from_indent(indent:margin.bottom, color:margin_color.bottom),
277 )
278}
279
280fn create_vertical_borders(
281 borders: &Borders<char>,
282 colors: &Borders<ANSIStr<'static>>,
283) -> HorizontalLine<ColoredIndent> {
284 let intersect: Option = bordersOption
285 .vertical
286 .map(|c: char| ColoredIndent::new(width:0, c, color:colors.vertical));
287 let left: Option = borders.left.map(|c: char| ColoredIndent::new(width:0, c, color:colors.left));
288 let right: Option = bordersOption
289 .right
290 .map(|c: char| ColoredIndent::new(width:0, c, color:colors.right));
291
292 HorizontalLine::new(main:None, intersection:intersect, left, right)
293}
294
295fn print_horizontal_line<F, D>(
296 f: &mut F,
297 dims: &D,
298 borders: &HorizontalLine<char>,
299 borders_colors: &HorizontalLine<ANSIStr<'static>>,
300 margin: &Sides<ColoredIndent>,
301 count_columns: usize,
302) -> fmt::Result
303where
304 F: fmt::Write,
305 D: Dimension,
306{
307 let is_not_colored: bool = borders_colors.is_empty();
308
309 print_indent(f, indent:margin.left)?;
310
311 if is_not_colored {
312 print_split_line(f, dims, chars:borders, count_columns)?;
313 } else {
314 print_split_line_colored(f, dimension:dims, borders, borders_colors, count_columns)?;
315 }
316
317 print_indent(f, indent:margin.right)?;
318
319 Ok(())
320}
321
322#[allow(clippy::too_many_arguments)]
323fn print_grid_row<F, I, T, C, D>(
324 f: &mut F,
325 data: I,
326 size: usize,
327 dims: &D,
328 colors: &C,
329 margin: &Sides<ColoredIndent>,
330 padding: &Sides<ColoredIndent>,
331 borders: &HorizontalLine<ColoredIndent>,
332 alignemnt: AlignmentHorizontal,
333 row: usize,
334) -> fmt::Result
335where
336 F: Write,
337 I: Iterator<Item = T>,
338 T: AsRef<str>,
339 C: Colors,
340 D: Dimension,
341{
342 for _ in 0..padding.top.space.size {
343 print_indent(f, indent:margin.left)?;
344 print_columns_empty_colored(f, dims, borders, padding.top.color, count_columns:size)?;
345 print_indent(f, indent:margin.right)?;
346
347 f.write_char('\n')?;
348 }
349
350 print_indent(f, indent:margin.left)?;
351 print_row_columns_one_line(f, data, dims, colors, borders, padding, alignement:alignemnt, row)?;
352 print_indent(f, indent:margin.right)?;
353
354 for _ in 0..padding.top.space.size {
355 f.write_char('\n')?;
356
357 print_indent(f, indent:margin.left)?;
358 print_columns_empty_colored(f, dims, borders, padding.bottom.color, count_columns:size)?;
359 print_indent(f, indent:margin.right)?;
360 }
361
362 Ok(())
363}
364
365fn create_padding(cfg: &CompactConfig) -> Sides<ColoredIndent> {
366 let pad: &Sides = cfg.get_padding();
367 let colors: &Sides> = cfg.get_padding_color();
368 Sides::new(
369 left:ColoredIndent::new(pad.left.size, pad.left.fill, create_color(colors.left)),
370 right:ColoredIndent::new(pad.right.size, pad.right.fill, create_color(colors.right)),
371 top:ColoredIndent::new(pad.top.size, pad.top.fill, create_color(colors.top)),
372 bottom:ColoredIndent::new(
373 width:pad.bottom.size,
374 c:pad.bottom.fill,
375 create_color(colors.bottom),
376 ),
377 )
378}
379
380fn create_horizontal(b: &Borders<char>) -> HorizontalLine<char> {
381 HorizontalLine::new(main:b.horizontal, b.intersection, b.left, b.right)
382}
383
384fn create_horizontal_top(b: &Borders<char>) -> HorizontalLine<char> {
385 HorizontalLine::new(main:b.top, b.top_intersection, b.top_left, b.top_right)
386}
387
388fn create_horizontal_bottom(b: &Borders<char>) -> HorizontalLine<char> {
389 HorizontalLine::new(
390 main:b.bottom,
391 b.bottom_intersection,
392 b.bottom_left,
393 b.bottom_right,
394 )
395}
396
397fn create_horizontal_colors(b: &Borders<ANSIStr<'static>>) -> HorizontalLine<ANSIStr<'static>> {
398 HorizontalLine::new(main:b.horizontal, b.intersection, b.left, b.right)
399}
400
401fn create_horizontal_top_colors(b: &Borders<ANSIStr<'static>>) -> HorizontalLine<ANSIStr<'static>> {
402 HorizontalLine::new(main:b.top, b.top_intersection, b.top_left, b.top_right)
403}
404
405fn create_horizontal_bottom_colors(
406 b: &Borders<ANSIStr<'static>>,
407) -> HorizontalLine<ANSIStr<'static>> {
408 HorizontalLine::new(
409 main:b.bottom,
410 b.bottom_intersection,
411 b.bottom_left,
412 b.bottom_right,
413 )
414}
415
416fn total_width<D>(cfg: &CompactConfig, dims: &D, count_columns: usize) -> usize
417where
418 D: Dimension,
419{
420 let content_width: usize = total_columns_width(count_columns, dims);
421 let count_verticals: usize = count_verticals(cfg, count_columns);
422
423 content_width + count_verticals
424}
425
426fn total_columns_width<D>(count_columns: usize, dims: &D) -> usize
427where
428 D: Dimension,
429{
430 (0..count_columns).map(|i: usize| dims.get_width(column:i)).sum::<usize>()
431}
432
433fn count_verticals(cfg: &CompactConfig, count_columns: usize) -> usize {
434 assert!(count_columns > 0);
435
436 let count_verticals: usize = count_columns - 1;
437 let borders: &Borders = cfg.get_borders();
438 borders.has_vertical() as usize * count_verticals
439 + borders.has_left() as usize
440 + borders.has_right() as usize
441}
442
443#[allow(clippy::too_many_arguments)]
444fn print_row_columns_one_line<F, I, T, D, C>(
445 f: &mut F,
446 mut data: I,
447 dims: &D,
448 colors: &C,
449 borders: &HorizontalLine<ColoredIndent>,
450 padding: &Sides<ColoredIndent>,
451 alignement: AlignmentHorizontal,
452 row: usize,
453) -> fmt::Result
454where
455 F: Write,
456 I: Iterator<Item = T>,
457 T: AsRef<str>,
458 D: Dimension,
459 C: Colors,
460{
461 if let Some(indent) = borders.left {
462 print_char(f, indent.space.fill, indent.color)?;
463 }
464
465 let text = data
466 .next()
467 .expect("we check in the beggining that size must be at least 1 column");
468 let width = dims.get_width(0);
469 let color = colors.get_color((row, 0));
470
471 let text = text.as_ref();
472 let text = text.lines().next().unwrap_or("");
473 print_cell(f, text, color, padding, alignement, width)?;
474
475 match borders.intersection {
476 Some(indent) => {
477 for (col, text) in data.enumerate() {
478 let col = col + 1;
479
480 let width = dims.get_width(col);
481 let color = colors.get_color((row, col));
482 let text = text.as_ref();
483 let text = text.lines().next().unwrap_or("");
484
485 print_char(f, indent.space.fill, indent.color)?;
486 print_cell(f, text, color, padding, alignement, width)?;
487 }
488 }
489 None => {
490 for (col, text) in data.enumerate() {
491 let col = col + 1;
492
493 let width = dims.get_width(col);
494 let color = colors.get_color((row, col));
495 let text = text.as_ref();
496 let text = text.lines().next().unwrap_or("");
497
498 print_cell(f, text, color, padding, alignement, width)?;
499 }
500 }
501 }
502
503 if let Some(indent) = borders.right {
504 print_char(f, indent.space.fill, indent.color)?;
505 }
506
507 Ok(())
508}
509
510fn print_columns_empty_colored<F, D>(
511 f: &mut F,
512 dims: &D,
513 borders: &HorizontalLine<ColoredIndent>,
514 color: Option<ANSIStr<'static>>,
515 count_columns: usize,
516) -> fmt::Result
517where
518 F: Write,
519 D: Dimension,
520{
521 if let Some(indent) = borders.left {
522 print_char(f, indent.space.fill, indent.color)?;
523 }
524
525 let width = dims.get_width(0);
526 print_indent(f, ColoredIndent::new(width, ' ', color))?;
527
528 match borders.intersection {
529 Some(indent) => {
530 for column in 1..count_columns {
531 let width = dims.get_width(column);
532
533 print_char(f, indent.space.fill, indent.color)?;
534 print_indent(f, ColoredIndent::new(width, ' ', color))?;
535 }
536 }
537 None => {
538 for column in 1..count_columns {
539 let width = dims.get_width(column);
540 print_indent(f, ColoredIndent::new(width, ' ', color))?;
541 }
542 }
543 }
544
545 if let Some(indent) = borders.right {
546 print_char(f, indent.space.fill, indent.color)?;
547 }
548
549 Ok(())
550}
551
552fn print_cell<F, C>(
553 f: &mut F,
554 text: &str,
555 color: Option<C>,
556 padding: &Sides<ColoredIndent>,
557 alignment: AlignmentHorizontal,
558 width: usize,
559) -> fmt::Result
560where
561 F: Write,
562 C: ANSIFmt,
563{
564 let available: usize = width - (padding.left.space.size + padding.right.space.size);
565
566 let text_width: usize = string_width(text);
567 let (left: usize, right: usize) = if available > text_width {
568 calculate_indent(alignment, text_width, available)
569 } else {
570 (0, 0)
571 };
572
573 print_indent(f, indent:padding.left)?;
574
575 repeat_char(f, c:' ', n:left)?;
576 print_text(f, text, color)?;
577 repeat_char(f, c:' ', n:right)?;
578
579 print_indent(f, indent:padding.right)?;
580
581 Ok(())
582}
583
584fn print_split_line_colored<F, D>(
585 f: &mut F,
586 dimension: &D,
587 borders: &HorizontalLine<char>,
588 borders_colors: &HorizontalLine<ANSIStr<'static>>,
589 count_columns: usize,
590) -> fmt::Result
591where
592 F: Write,
593 D: Dimension,
594{
595 let mut used_color = ANSIStr::default();
596 let chars_main = borders.main.unwrap_or(' ');
597
598 if let Some(c) = borders.left {
599 if let Some(color) = &borders_colors.right {
600 prepare_coloring(f, color, &mut used_color)?;
601 }
602
603 f.write_char(c)?;
604 }
605
606 let width = dimension.get_width(0);
607 if width > 0 {
608 if let Some(color) = borders_colors.main {
609 prepare_coloring(f, &color, &mut used_color)?;
610 }
611
612 repeat_char(f, chars_main, width)?;
613 }
614
615 for col in 1..count_columns {
616 if let Some(c) = borders.intersection {
617 if let Some(color) = borders_colors.intersection {
618 prepare_coloring(f, &color, &mut used_color)?;
619 }
620
621 f.write_char(c)?;
622 }
623
624 let width = dimension.get_width(col);
625 if width > 0 {
626 if let Some(color) = borders_colors.main {
627 prepare_coloring(f, &color, &mut used_color)?;
628 }
629
630 repeat_char(f, chars_main, width)?;
631 }
632 }
633
634 if let Some(c) = borders.right {
635 if let Some(color) = &borders_colors.right {
636 prepare_coloring(f, color, &mut used_color)?;
637 }
638
639 f.write_char(c)?;
640 }
641
642 used_color.fmt_ansi_suffix(f)?;
643
644 Ok(())
645}
646
647fn print_split_line<F, D>(
648 f: &mut F,
649 dims: &D,
650 chars: &HorizontalLine<char>,
651 count_columns: usize,
652) -> fmt::Result
653where
654 F: Write,
655 D: Dimension,
656{
657 let chars_main = chars.main.unwrap_or(' ');
658
659 if let Some(c) = chars.left {
660 f.write_char(c)?;
661 }
662
663 let width = dims.get_width(0);
664 if width > 0 {
665 repeat_char(f, chars_main, width)?;
666 }
667
668 for col in 1..count_columns {
669 if let Some(c) = chars.intersection {
670 f.write_char(c)?;
671 }
672
673 let width = dims.get_width(col);
674 if width > 0 {
675 repeat_char(f, chars_main, width)?;
676 }
677 }
678
679 if let Some(c) = chars.right {
680 f.write_char(c)?;
681 }
682
683 Ok(())
684}
685
686fn print_text<F, C>(f: &mut F, text: &str, color: Option<C>) -> fmt::Result
687where
688 F: Write,
689 C: ANSIFmt,
690{
691 match color {
692 Some(color: C) => {
693 color.fmt_ansi_prefix(f)?;
694 f.write_str(text)?;
695 color.fmt_ansi_suffix(f)?;
696 }
697 None => {
698 f.write_str(text)?;
699 }
700 };
701
702 Ok(())
703}
704
705fn prepare_coloring<F>(
706 f: &mut F,
707 clr: &ANSIStr<'static>,
708 used: &mut ANSIStr<'static>,
709) -> fmt::Result
710where
711 F: Write,
712{
713 if *used != *clr {
714 used.fmt_ansi_suffix(f)?;
715 clr.fmt_ansi_prefix(f)?;
716 *used = *clr;
717 }
718
719 Ok(())
720}
721
722fn calculate_indent(
723 alignment: AlignmentHorizontal,
724 text_width: usize,
725 available: usize,
726) -> (usize, usize) {
727 let diff: usize = available - text_width;
728 match alignment {
729 AlignmentHorizontal::Left => (0, diff),
730 AlignmentHorizontal::Right => (diff, 0),
731 AlignmentHorizontal::Center => {
732 let left: usize = diff / 2;
733 let rest: usize = diff - left;
734 (left, rest)
735 }
736 }
737}
738
739fn repeat_char<F>(f: &mut F, c: char, n: usize) -> fmt::Result
740where
741 F: Write,
742{
743 for _ in 0..n {
744 f.write_char(c)?;
745 }
746
747 Ok(())
748}
749
750// todo: replace Option<StaticColor> to StaticColor and check performance
751fn print_char<F>(f: &mut F, c: char, color: Option<ANSIStr<'static>>) -> fmt::Result
752where
753 F: Write,
754{
755 match color {
756 Some(color: ANSIStr<'static>) => {
757 color.fmt_ansi_prefix(f)?;
758 f.write_char(c)?;
759 color.fmt_ansi_suffix(f)
760 }
761 None => f.write_char(c),
762 }
763}
764
765fn print_indent_lines<F>(f: &mut F, indent: ColoredIndent) -> fmt::Result
766where
767 F: Write,
768{
769 print_indent(f, indent)?;
770 f.write_char('\n')?;
771
772 for _ in 1..indent.space.size {
773 f.write_char('\n')?;
774 print_indent(f, indent)?;
775 }
776
777 Ok(())
778}
779
780fn print_indent<F>(f: &mut F, indent: ColoredIndent) -> fmt::Result
781where
782 F: Write,
783{
784 match indent.color {
785 Some(color: ANSIStr<'static>) => {
786 color.fmt_ansi_prefix(f)?;
787 repeat_char(f, c:indent.space.fill, n:indent.space.size)?;
788 color.fmt_ansi_suffix(f)?;
789 }
790 None => {
791 repeat_char(f, c:indent.space.fill, n:indent.space.size)?;
792 }
793 }
794
795 Ok(())
796}
797
798#[derive(Debug, Clone, Copy)]
799struct ColoredIndent {
800 space: Indent,
801 color: Option<ANSIStr<'static>>,
802}
803
804impl ColoredIndent {
805 fn new(width: usize, c: char, color: Option<ANSIStr<'static>>) -> Self {
806 Self {
807 space: Indent::new(size:width, fill:c),
808 color,
809 }
810 }
811
812 fn from_indent(indent: Indent, color: ANSIStr<'static>) -> Self {
813 Self {
814 space: indent,
815 color: create_color(color),
816 }
817 }
818}
819
820fn create_color(color: ANSIStr<'static>) -> Option<ANSIStr<'static>> {
821 if color.is_empty() {
822 None
823 } else {
824 Some(color)
825 }
826}
827