radicle-heartwood-lfs/radicle-term/src/table.rs

228 lines
5.9 KiB
Rust

//! Print column-aligned text to the console.
//!
//! Example:
//! ```
//! use radicle_term::table::*;
//!
//! let mut t = Table::new(TableOptions::default());
//! t.push(["pest", "biological control"]);
//! t.push(["aphid", "lacewing"]);
//! t.push(["spider mite", "ladybug"]);
//! t.render();
//! ```
//! Output:
//! ``` plain
//! pest biological control
//! aphid ladybug
//! spider mite persimilis
//! ```
use std::io;
use crate as term;
use crate::cell::Cell;
/// Used to specify maximum width or height.
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub struct Max {
width: Option<usize>,
height: Option<usize>,
}
#[derive(Debug, Default)]
pub struct TableOptions {
/// Whether the table should be allowed to overflow.
pub overflow: bool,
/// The maximum width and height.
pub max: Max,
}
#[derive(Debug)]
pub struct Table<const W: usize> {
rows: Vec<[String; W]>,
widths: [usize; W],
opts: TableOptions,
}
impl<const W: usize> Default for Table<W> {
fn default() -> Self {
Self {
rows: Vec::new(),
widths: [0; W],
opts: TableOptions::default(),
}
}
}
impl<const W: usize> Table<W> {
pub fn new(opts: TableOptions) -> Self {
Self {
rows: Vec::new(),
widths: [0; W],
opts,
}
}
pub fn push(&mut self, row: [impl Cell; W]) {
let row = row.map(|s| s.to_string());
for (i, cell) in row.iter().enumerate() {
self.widths[i] = self.widths[i].max(cell.width());
}
self.rows.push(row);
}
pub fn render(self) {
self.write(io::stdout()).ok();
}
pub fn write<T: io::Write>(self, mut writer: T) -> io::Result<()> {
let width = self.opts.max.width.or_else(term::columns);
for row in &self.rows {
let mut output = String::new();
let cells = row.len();
for (i, cell) in row.iter().enumerate() {
if i == cells - 1 || self.opts.overflow {
output.push_str(cell.to_string().as_str());
} else {
output.push_str(cell.pad(self.widths[i]).as_str());
output.push(' ');
}
}
let output = output.trim_end();
writeln!(
writer,
"{}",
if let Some(width) = width {
output.truncate(width, "")
} else {
output.into()
}
)?;
}
Ok(())
}
pub fn render_tree(self) {
for (r, row) in self.rows.iter().enumerate() {
if r != self.rows.len() - 1 {
print!("├── ");
} else {
print!("└── ");
}
for (i, cell) in row.iter().enumerate() {
print!("{} ", cell.pad(self.widths[i]));
}
println!();
}
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_truncate() {
assert_eq!("🍍".truncate(1, ""), String::from(""));
assert_eq!("🍍".truncate(1, ""), String::from(""));
assert_eq!("🍍🍍".truncate(2, ""), String::from(""));
assert_eq!("🍍🍍".truncate(3, ""), String::from("🍍…"));
assert_eq!("🍍".truncate(1, "🍎"), String::from(""));
assert_eq!("🍍".truncate(2, "🍎"), String::from("🍍"));
assert_eq!("🍍🍍".truncate(3, "🍎"), String::from("🍎"));
assert_eq!("🍍🍍🍍".truncate(4, "🍎"), String::from("🍍🍎"));
assert_eq!("hello".truncate(3, ""), String::from("he…"));
}
#[test]
fn test_table() {
let mut s = Vec::new();
let mut t = Table::new(TableOptions::default());
t.push(["pineapple", "rosemary"]);
t.push(["apples", "pears"]);
t.write(&mut s).unwrap();
#[rustfmt::skip]
assert_eq!(
String::from_utf8_lossy(&s),
[
"pineapple rosemary\n",
"apples pears\n"
].join("")
);
}
#[test]
fn test_table_truncate() {
let mut s = Vec::new();
let mut t = Table::new(TableOptions {
max: Max {
width: Some(16),
height: None,
},
..TableOptions::default()
});
t.push(["pineapple", "rosemary"]);
t.push(["apples", "pears"]);
t.write(&mut s).unwrap();
#[rustfmt::skip]
assert_eq!(
String::from_utf8_lossy(&s),
[
"pineapple rosem…\n",
"apples pears\n"
].join("")
);
}
#[test]
fn test_table_unicode() {
let mut s = Vec::new();
let mut t = Table::new(TableOptions::default());
t.push(["🍍pineapple", "__rosemary", "__sage"]);
t.push(["__pears", "🍎apples", "🍌bananas"]);
t.write(&mut s).unwrap();
#[rustfmt::skip]
assert_eq!(
String::from_utf8_lossy(&s),
[
"🍍pineapple __rosemary __sage\n",
"__pears 🍎apples 🍌bananas\n"
].join("")
);
}
#[test]
fn test_table_unicode_truncate() {
let mut s = Vec::new();
let mut t = Table::new(TableOptions {
max: Max {
width: Some(16),
height: None,
},
..TableOptions::default()
});
t.push(["🍍pineapple", "__rosemary"]);
t.push(["__pears", "🍎apples"]);
t.write(&mut s).unwrap();
#[rustfmt::skip]
assert_eq!(
String::from_utf8_lossy(&s),
[
"🍍pineapple __r…\n",
"__pears 🍎a…\n"
].join("")
);
}
}