crud_tidy_viewer/
lib.rs

1//! # Crud tidy viewer
2//!
3//! Pretty printer for arrays.
4//!
5//! Some parts of the code is copied and refactored from [Tidy-viewer](https://github.com/alexhallam/tv) (released under public domain)
6//!
7//! ## Examples
8//!
9//! ```rust
10//! use crud_tidy_viewer::{display_table, TableConfig};
11//! # use miette::Result;
12//! # fn main() -> Result<()> {
13//!   let rdr = vec![
14//!     vec!["a".to_string(), "b".to_string()],
15//!     vec!["1".to_string(), "b".to_string()],
16//!     vec!["4.1453".to_string(), "c".to_string()],
17//!     vec!["2.4".to_string(), "f".to_string()],
18//!     vec!["5".to_string(), "e".to_string()],
19//!   ];
20//!   display_table(&rdr, TableConfig::default());
21//! #  Ok(())
22//! # }
23//! ```
24
25mod datatype;
26use calm_io::stdout;
27use calm_io::stdoutln;
28use owo_colors::OwoColorize;
29
30pub struct TableConfig {
31  std_color: [u8; 3],
32  neg_num_color: [u8; 3],
33  na_color: [u8; 3],
34  meta_color: [u8; 3],
35  header_color: [u8; 3],
36  title_option: String,
37  footer_option: String,
38  display_meta: bool,
39  extend_option: bool,
40  line_counter: bool,
41  debug_mode: bool,
42  is_tty: bool,
43  is_force_color: bool,
44  term_tuple: (u16, u16),
45  sigfig: i64,
46  lower_column_width: usize,
47  upper_column_width: usize,
48  row_display_option: usize,
49}
50
51const NORD_META_COLOR: [u8; 3] = [143, 188, 187];
52const NORD_HEADER_COLOR: [u8; 3] = [94, 129, 172];
53const NORD_STD_COLOR: [u8; 3] = [216, 222, 233];
54const NORD_NA_COLOR: [u8; 3] = [191, 97, 106];
55const NORD_NEG_NUM_COLOR: [u8; 3] = [208, 135, 112];
56
57impl Default for TableConfig {
58  fn default() -> Self {
59    Self {
60      std_color: NORD_STD_COLOR,
61      neg_num_color: NORD_NEG_NUM_COLOR,
62      na_color: NORD_NA_COLOR,
63      meta_color: NORD_META_COLOR,
64      header_color: NORD_HEADER_COLOR,
65      title_option: Default::default(),
66      footer_option: Default::default(),
67      display_meta: false,
68      extend_option: true,
69      line_counter: false,
70      debug_mode: false,
71      is_tty: atty::is(atty::Stream::Stdout),
72      is_force_color: false,
73      term_tuple: crossterm::terminal::size().unwrap(),
74      sigfig: 3,
75      lower_column_width: 2,
76      upper_column_width: 50,
77      row_display_option: 25,
78    }
79  }
80}
81
82pub fn display_table(rdr: &[Vec<String>], config: TableConfig) {
83  /*
84      This piece of code is copied and refactored from Tidy-viewer (released under public domain)
85      Original source: https://github.com/alexhallam/tv
86      commit: 973d88f7ed05a394b5309c5e8b6f3e3a52a39b17 /  May 14, 2022
87  */
88
89  let cols: usize = rdr[0].len();
90  let rows_in_file: usize = rdr.len();
91  let rows: usize = if config.extend_option {
92    rdr.len().min(rows_in_file + 1)
93  } else {
94    rdr.len().min(config.row_display_option + 1)
95  };
96
97  let rows_remaining: usize = rows_in_file - rows;
98  let ellipsis = '\u{2026}'.to_string();
99  let row_remaining_text: String = format!("{ellipsis} with {rows_remaining} more rows");
100
101  // csv gets records in rows. This makes them cols
102  let mut v: Vec<Vec<&str>> = Vec::new(); //vec![vec!["#"; rows as usize]; cols as usize];
103  for col in 0..cols {
104    let column = rdr
105      .iter()
106      .take(rows)
107      .map(|row| row.get(col).unwrap().as_str())
108      .collect();
109    v.push(column)
110  }
111
112  if config.debug_mode {
113    println!("{:?}", "v");
114    println!("{v:?}");
115  }
116
117  if config.debug_mode {
118    // make datatypes vector
119    let mut vec_datatypes = Vec::with_capacity(cols);
120    for column in &v {
121      vec_datatypes.push(datatype::get_col_data_type(column))
122    }
123    println!("{:?}", "vec_datatypes");
124    println!("{vec_datatypes:?}");
125  }
126
127  // vector of formatted values
128  let vf: Vec<Vec<String>> = v
129    .iter()
130    .map(|col| {
131      datatype::format_strings(
132        col,
133        config.lower_column_width,
134        config.upper_column_width,
135        config.sigfig,
136      )
137    })
138    .collect();
139
140  if config.debug_mode {
141    println!("{:?}", "Transposed Vector of Elements");
142    println!("{v:?}");
143    println!("{:?}", "Formatted: Vector of Elements");
144    println!("{vf:?}");
145  }
146
147  //  println!();
148  let mut vp = Vec::new();
149  for r in 0..rows {
150    let row = vf.iter().map(|col| col[r].to_string()).collect();
151    vp.push(row);
152  }
153
154  let num_cols_to_print = if config.extend_option {
155    cols
156  } else {
157    get_num_cols_to_print(cols, vp.clone(), config.term_tuple)
158  };
159
160  // color
161  if config.display_meta {
162    let meta_text = "tv dim:";
163    let div = "x";
164    let _ = match stdout!("{: <6}", "") {
165      Ok(_) => Ok(()),
166      Err(e) => match e.kind() {
167        std::io::ErrorKind::BrokenPipe => Ok(()),
168        _ => Err(e),
169      },
170    };
171    if config.is_tty || config.is_force_color {
172      let _ = match stdoutln!(
173        "{} {} {} {}",
174        meta_text.truecolor(
175          config.meta_color[0],
176          config.meta_color[1],
177          config.meta_color[2]
178        ),
179        (rows_in_file - 1).truecolor(
180          config.meta_color[0],
181          config.meta_color[1],
182          config.meta_color[2]
183        ),
184        div.truecolor(
185          config.meta_color[0],
186          config.meta_color[1],
187          config.meta_color[2]
188        ),
189        (cols).truecolor(
190          config.meta_color[0],
191          config.meta_color[1],
192          config.meta_color[2]
193        ),
194      ) {
195        Ok(_) => Ok(()),
196        Err(e) => match e.kind() {
197          std::io::ErrorKind::BrokenPipe => Ok(()),
198          _ => Err(e),
199        },
200      };
201    } else {
202      let _ = match stdoutln!("{} {} {} {}", meta_text, rows_in_file - 1, div, cols) {
203        Ok(_) => Ok(()),
204        Err(e) => match e.kind() {
205          std::io::ErrorKind::BrokenPipe => Ok(()),
206          _ => Err(e),
207        },
208      };
209    }
210  }
211  // title
212  if !datatype::is_na(&config.title_option) {
213    let _ = match stdout!("{: <6}", "") {
214      Ok(_) => Ok(()),
215      Err(e) => match e.kind() {
216        std::io::ErrorKind::BrokenPipe => Ok(()),
217        _ => Err(e),
218      },
219    };
220    if config.is_tty || config.is_force_color {
221      let _ = match stdoutln!(
222        "{}",
223        config
224          .title_option
225          .truecolor(
226            config.meta_color[0],
227            config.meta_color[1],
228            config.meta_color[2]
229          )
230          .underline()
231          .bold()
232      ) {
233        Ok(_) => Ok(()),
234        Err(e) => match e.kind() {
235          std::io::ErrorKind::BrokenPipe => Ok(()),
236          _ => Err(e),
237        },
238      };
239    } else {
240      let _ = match stdoutln!("{}", config.title_option) {
241        Ok(_) => Ok(()),
242        Err(e) => match e.kind() {
243          std::io::ErrorKind::BrokenPipe => Ok(()),
244          _ => Err(e),
245        },
246      };
247    }
248  }
249
250  // header
251  if config.line_counter {
252    let _ = match stdout!("{: <6}", "") {
253      Ok(_) => Ok(()),
254      Err(e) => match e.kind() {
255        std::io::ErrorKind::BrokenPipe => Ok(()),
256        _ => Err(e),
257      },
258    };
259  }
260  //for col in 0..cols {
261  for col in 0..num_cols_to_print {
262    let text = vp[0].get(col).unwrap().to_string();
263    if config.is_tty || config.is_force_color {
264      let _ = match stdout!(
265        "{}",
266        text
267          .truecolor(
268            config.header_color[0],
269            config.header_color[1],
270            config.header_color[2]
271          )
272          .bold()
273      ) {
274        Ok(_) => Ok(()),
275        Err(e) => match e.kind() {
276          std::io::ErrorKind::BrokenPipe => Ok(()),
277          _ => Err(e),
278        },
279      };
280    } else {
281      let _ = match stdout!("{}", text) {
282        Ok(_) => Ok(()),
283        Err(e) => match e.kind() {
284          std::io::ErrorKind::BrokenPipe => Ok(()),
285          _ => Err(e),
286        },
287      };
288    }
289  }
290  //println!();
291  // datatypes
292  //print!("{: <6}", "");
293  //for col in 0..cols{
294  //    let add_space = vec_datatypes[col].len() - col_largest_width[col];
295  //    let mut owned_string: String = vec_datatypes[col].to_string();
296  //    let borrowed_string: &str = &" ".repeat(add_space);
297  //    owned_string.push_str(borrowed_string);
298  //    print!("{}",owned_string.truecolor(143, 188, 187).bold());
299  //}
300  let _ = match stdoutln!() {
301    Ok(_) => Ok(()),
302    Err(e) => match e.kind() {
303      std::io::ErrorKind::BrokenPipe => Ok(()),
304      _ => Err(e),
305    },
306  };
307  vp.iter()
308    .enumerate()
309    .take(rows)
310    .skip(1)
311    .for_each(|(i, row)| {
312      if config.line_counter {
313        if config.is_tty || config.is_force_color {
314          let _ = match stdout!(
315            "{: <6}",
316            i.truecolor(
317              config.meta_color[0],
318              config.meta_color[1],
319              config.meta_color[2]
320            )
321          ) {
322            Ok(_) => Ok(()),
323            Err(e) => match e.kind() {
324              std::io::ErrorKind::BrokenPipe => Ok(()),
325              _ => Err(e),
326            },
327          };
328        } else {
329          let _ = match stdout!("{: <6}", i) {
330            Ok(_) => Ok(()),
331            Err(e) => match e.kind() {
332              std::io::ErrorKind::BrokenPipe => Ok(()),
333              _ => Err(e),
334            },
335          };
336        }
337      }
338      row.iter().take(num_cols_to_print).for_each(|col| {
339        if config.is_tty || config.is_force_color {
340          let _ = match stdout!(
341            "{}",
342            if datatype::is_na_string_padded(col) {
343              col.truecolor(config.na_color[0], config.na_color[1], config.na_color[2])
344            } else if datatype::is_number(col) && datatype::is_negative_number(col) {
345              col.truecolor(
346                config.neg_num_color[0],
347                config.neg_num_color[1],
348                config.neg_num_color[2],
349              )
350            } else {
351              col.truecolor(
352                config.std_color[0],
353                config.std_color[1],
354                config.std_color[2],
355              )
356            }
357          ) {
358            Ok(_) => Ok(()),
359            Err(e) => match e.kind() {
360              std::io::ErrorKind::BrokenPipe => Ok(()),
361              _ => Err(e),
362            },
363          };
364        } else {
365          let _ = match stdout!("{}", col) {
366            Ok(_) => Ok(()),
367            Err(e) => match e.kind() {
368              std::io::ErrorKind::BrokenPipe => Ok(()),
369              _ => Err(e),
370            },
371          };
372        }
373      });
374      let _ = match stdoutln!() {
375        Ok(_) => Ok(()),
376        Err(e) => match e.kind() {
377          std::io::ErrorKind::BrokenPipe => Ok(()),
378          _ => Err(e),
379        },
380      };
381    });
382
383  // additional row info
384  if rows_remaining > 0 {
385    let _ = match stdout!("{: <6}", "") {
386      Ok(_) => Ok(()),
387      Err(e) => match e.kind() {
388        std::io::ErrorKind::BrokenPipe => Ok(()),
389        _ => Err(e),
390      },
391    };
392    if config.is_tty || config.is_force_color {
393      let _ = match stdout!(
394        "{}",
395        row_remaining_text.truecolor(
396          config.meta_color[0],
397          config.meta_color[1],
398          config.meta_color[2]
399        )
400      ) {
401        Ok(_) => Ok(()),
402        Err(e) => match e.kind() {
403          std::io::ErrorKind::BrokenPipe => Ok(()),
404          _ => Err(e),
405        },
406      };
407    } else {
408      let _ = match stdout!("{}", row_remaining_text) {
409        Ok(_) => Ok(()),
410        Err(e) => match e.kind() {
411          std::io::ErrorKind::BrokenPipe => Ok(()),
412          _ => Err(e),
413        },
414      };
415    }
416    let extra_cols_to_mention = num_cols_to_print;
417    let remainder_cols = cols - extra_cols_to_mention;
418    if extra_cols_to_mention < cols {
419      let meta_text_and = "and";
420      let meta_text_var = "more variables";
421      let meta_text_comma = ",";
422      let meta_text_colon = ":";
423      if config.is_tty || config.is_force_color {
424        let _ = match stdout!(
425          " {} {} {}{}",
426          meta_text_and.truecolor(
427            config.meta_color[0],
428            config.meta_color[1],
429            config.meta_color[2]
430          ),
431          remainder_cols.truecolor(
432            config.meta_color[0],
433            config.meta_color[1],
434            config.meta_color[2]
435          ),
436          meta_text_var.truecolor(
437            config.meta_color[0],
438            config.meta_color[1],
439            config.meta_color[2]
440          ),
441          meta_text_colon.truecolor(
442            config.meta_color[0],
443            config.meta_color[1],
444            config.meta_color[2]
445          )
446        ) {
447          Ok(_) => Ok(()),
448          Err(e) => match e.kind() {
449            std::io::ErrorKind::BrokenPipe => Ok(()),
450            _ => Err(e),
451          },
452        };
453      } else {
454        let _ = match stdout!(
455          " {} {} {}{}",
456          meta_text_and,
457          remainder_cols,
458          meta_text_var,
459          meta_text_colon
460        ) {
461          Ok(_) => Ok(()),
462          Err(e) => match e.kind() {
463            std::io::ErrorKind::BrokenPipe => Ok(()),
464            _ => Err(e),
465          },
466        };
467      }
468      for col in extra_cols_to_mention..cols {
469        let text = rdr[0].get(col).unwrap();
470        if config.is_tty || config.is_force_color {
471          let _ = match stdout!(
472            " {}",
473            text.truecolor(
474              config.meta_color[0],
475              config.meta_color[1],
476              config.meta_color[2]
477            )
478          ) {
479            Ok(_) => Ok(()),
480            Err(e) => match e.kind() {
481              std::io::ErrorKind::BrokenPipe => Ok(()),
482              _ => Err(e),
483            },
484          };
485        } else {
486          let _ = match stdout!(" {}", text) {
487            Ok(_) => Ok(()),
488            Err(e) => match e.kind() {
489              std::io::ErrorKind::BrokenPipe => Ok(()),
490              _ => Err(e),
491            },
492          };
493        }
494
495        // The last column mentioned in foot should not be followed by a comma
496        if col + 1 < cols {
497          if config.is_tty || config.is_force_color {
498            let _ = match stdout!(
499              "{}",
500              meta_text_comma.truecolor(
501                config.meta_color[0],
502                config.meta_color[1],
503                config.meta_color[2]
504              )
505            ) {
506              Ok(_) => Ok(()),
507              Err(e) => match e.kind() {
508                std::io::ErrorKind::BrokenPipe => Ok(()),
509                _ => Err(e),
510              },
511            };
512          } else {
513            let _ = match stdout!("{}", meta_text_comma) {
514              Ok(_) => Ok(()),
515              Err(e) => match e.kind() {
516                std::io::ErrorKind::BrokenPipe => Ok(()),
517                _ => Err(e),
518              },
519            };
520          }
521        }
522      } // end extra cols mentioned in footer
523    }
524  }
525
526  // footer
527  if !datatype::is_na(&config.footer_option) {
528    let _ = match stdout!("{: <6}", "") {
529      Ok(_) => Ok(()),
530      Err(e) => match e.kind() {
531        std::io::ErrorKind::BrokenPipe => Ok(()),
532        _ => Err(e),
533      },
534    };
535    if config.is_tty || config.is_force_color {
536      let _ = match stdoutln!(
537        "{}",
538        config.footer_option.truecolor(
539          config.meta_color[0],
540          config.meta_color[1],
541          config.meta_color[2]
542        )
543      ) {
544        Ok(_) => Ok(()),
545        Err(e) => match e.kind() {
546          std::io::ErrorKind::BrokenPipe => Ok(()),
547          _ => Err(e),
548        },
549      };
550    } else {
551      let _ = match stdoutln!("{}", config.footer_option) {
552        Ok(_) => Ok(()),
553        Err(e) => match e.kind() {
554          std::io::ErrorKind::BrokenPipe => Ok(()),
555          _ => Err(e),
556        },
557      };
558    }
559  }
560}
561
562// how wide will the print be?
563fn get_num_cols_to_print(cols: usize, vp: Vec<Vec<String>>, term_tuple: (u16, u16)) -> usize {
564  let mut last = 0;
565  let mut j = format!("{: <6}", "");
566  for col in 0..cols {
567    let text = vp[0].get(col).unwrap().to_string();
568    j.push_str(&text);
569    let total_width = j.chars().count();
570    let term_width = term_tuple.0 as usize;
571    if total_width > term_width {
572      break;
573    }
574    last = col + 1;
575  }
576  last
577}