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