qpprint 0.4.0

Simple console printing/formatting.
Documentation
pub mod tbl;

use terminal_size::{terminal_size, Height, Width};

pub use yansi::{Color, Painted};

pub enum Types {
  Paragraph(String)
}

#[derive(Copy, Clone, Debug)]
pub enum Align {
  Left,
  Center,
  Right
}

pub struct Column {
  pub title: String,
  pub title_align: Align,
  pub align: Align
}

impl Column {
  #[allow(clippy::needless_pass_by_value)]
  pub fn new(heading: impl ToString) -> Self {
    Self {
      title: heading.to_string(),
      title_align: Align::Left,
      align: Align::Left
    }
  }

  #[must_use]
  pub const fn title_align(mut self, align: Align) -> Self {
    self.title_align = align;
    self
  }

  #[must_use]
  pub const fn align(mut self, align: Align) -> Self {
    self.align = align;
    self
  }
}


fn wordify(s: &str) -> Vec<String> {
  let mut words = Vec::new();
  let splt = s.split_whitespace();

  for w in splt {
    words.push(w.to_string());
  }
  words
}

pub struct PPrint {
  indent: u16,
  hang: i16,
  maxwidth: u16
}

impl Default for PPrint {
  fn default() -> Self {
    Self::new()
  }
}

impl PPrint {
  #[must_use]
  pub fn new() -> Self {
    let size = terminal_size();

    let maxwidth = if let Some((Width(w), Height(_h))) = size {
      w
    } else {
      80
    };

    Self {
      indent: 0,
      hang: 0,
      maxwidth
    }
  }

  pub const fn set_indent(&mut self, indent: u16) -> &mut Self {
    self.indent = indent;
    self
  }

  /// Set a relative offset for the first line in a paragraph.
  pub const fn set_hang(&mut self, hang: i16) -> &mut Self {
    self.hang = hang;
    self
  }

  /*
  pub(crate) fn set_maxwidth(&mut self, maxwidth: u16) -> &mut Self {
    self.maxwidth = maxwidth;
    self
  }
  */

  /// # Panics
  /// Writes must be successful.
  #[allow(clippy::cast_possible_truncation)]
  #[allow(clippy::cast_possible_wrap)]
  #[allow(clippy::cast_sign_loss)]
  pub fn print_words<I, S>(&self, out: &mut dyn std::io::Write, words: I)
  where
    I: IntoIterator<Item = S>,
    S: AsRef<str>
  {
    let mut firstline = true;
    let mut newline = true;
    let mut space: u16 = 0;
    let mut col: u16 = 0;

    for w in words {
      let w = w.as_ref();
      if col + space + w.len() as u16 > self.maxwidth {
        out.write_all(b"\n").unwrap();
        newline = true;
      }

      if newline {
        let mut indent: i16 = 0;

        indent += self.indent as i16;
        if firstline {
          indent += self.hang;
        }

        out
          .write_all(" ".repeat(indent as usize).as_bytes())
          .unwrap();
        col = indent as u16;

        newline = false;
        space = 0;
        firstline = false;
      }

      out
        .write_all(" ".repeat(space as usize).as_bytes())
        .unwrap();
      col += space;

      out.write_all(w.as_bytes()).unwrap();
      col += w.len() as u16;

      let ch = w.chars().last().unwrap();
      match ch {
        '.' | '?' | '!' => {
          space = 2;
        }
        _ => {
          space = 1;
        }
      }
    }
    out.write_all(b"\n").unwrap();
  }

  pub fn print_p(&self, out: &mut dyn std::io::Write, para: &str) {
    let words = wordify(para);
    self.print_words(out, &words);
  }

  pub fn print_plist<I, S>(&self, out: &mut dyn std::io::Write, parit: I)
  where
    I: IntoIterator<Item = S>,
    S: AsRef<str>
  {
    for p in parit {
      self.print_p(out, p.as_ref());
    }
  }
}


#[allow(clippy::similar_names)]
pub fn print_table(cols: &[Column], body: &Vec<Vec<String>>) {
  // Used to keep track of the maximum column width.
  let mut colw: Vec<usize> = cols.iter().map(|col| col.title.len()).collect();

  // Iterate over body cells
  for row in body {
    for (i, col) in row.iter().enumerate() {
      // Ignore any body colums which are not in the column list
      if i == colw.len() {
        break;
      }
      if col.len() > colw[i] {
        colw[i] = col.len();
      }
    }
  }

  // print header
  let mut fields = Vec::new();
  for (i, col) in cols.iter().enumerate() {
    match col.title_align {
      Align::Left => {
        fields.push(format!("{1:<0$}", colw[i], col.title));
      }
      Align::Center => {
        fields.push(format!("{1:^0$}", colw[i], col.title));
      }
      Align::Right => {
        fields.push(format!("{1:>0$}", colw[i], col.title));
      }
    }
  }
  println!("{}", fields.join("  "));

  // print heading underline
  fields.clear();
  for w in &colw {
    fields.push("~".repeat(*w).clone());
  }
  println!("{}", fields.join("  "));

  for row in body {
    fields.clear();

    for (i, cell) in row.iter().enumerate() {
      match cols[i].align {
        Align::Left => {
          fields.push(format!("{1:<0$}", colw[i], cell));
        }
        Align::Center => {
          fields.push(format!("{1:^0$}", colw[i], cell));
        }
        Align::Right => {
          fields.push(format!("{1:>0$}", colw[i], cell));
        }
      }
    }
    println!("{}", fields.join("  "));
  }
}

// vim: set ft=rust et sw=2 ts=2 sts=2 cinoptions=2 tw=79 :