429 lines
16 KiB
Rust
429 lines
16 KiB
Rust
#![allow(clippy::type_complexity)]
|
|
use std::borrow::Cow;
|
|
|
|
use ansi_term::Style;
|
|
|
|
fn lpad(s: &str, len: usize) -> String {
|
|
let padding = " ".repeat(len.saturating_sub(s.chars().count()));
|
|
|
|
padding + s
|
|
}
|
|
|
|
fn rpad(s: &str, len: usize) -> String {
|
|
let padding = " ".repeat(len.saturating_sub(s.chars().count()));
|
|
|
|
s.to_string() + &padding
|
|
}
|
|
|
|
fn constrained_lines(text: &str, width: usize) -> Vec<Cow<'_, str>> {
|
|
textwrap::wrap(text, width)
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
pub enum Align {
|
|
Left,
|
|
Right,
|
|
}
|
|
|
|
use Align::*;
|
|
|
|
#[derive(Clone)]
|
|
pub struct Col {
|
|
min_width: usize,
|
|
max_width: Option<usize>,
|
|
align: Align,
|
|
conditonal_styles: Vec<(Style, fn(&str) -> bool)>,
|
|
}
|
|
|
|
impl Col {
|
|
pub fn new() -> Col {
|
|
Col {
|
|
min_width: 0,
|
|
align: Align::Left,
|
|
max_width: None,
|
|
conditonal_styles: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn min_width(size: usize) -> Col {
|
|
Col {
|
|
min_width: size,
|
|
align: Align::Left,
|
|
max_width: None,
|
|
conditonal_styles: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn and_alignment(self, align: Align) -> Col {
|
|
Col {
|
|
align,
|
|
..self
|
|
}
|
|
}
|
|
|
|
pub fn max_width(self, size: usize) -> Col {
|
|
Col {
|
|
max_width: Some(size),
|
|
..self
|
|
}
|
|
}
|
|
|
|
pub fn color_if(self, style: Style, f: fn(&str) -> bool) -> Col {
|
|
let mut conditonal_styles = self.conditonal_styles;
|
|
|
|
conditonal_styles.push((style, f));
|
|
|
|
Col {
|
|
conditonal_styles,
|
|
..self
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Col {
|
|
fn default() -> Col {
|
|
Col::new()
|
|
}
|
|
}
|
|
|
|
enum DataOrSep {
|
|
Data(Vec<String>),
|
|
Sep(char),
|
|
}
|
|
|
|
pub struct Tabulate {
|
|
cols: Vec<Col>,
|
|
widths: Vec<usize>,
|
|
data: Vec<DataOrSep>,
|
|
}
|
|
|
|
impl Tabulate {
|
|
pub fn with_columns(cols: Vec<Col>) -> Tabulate {
|
|
Tabulate {
|
|
widths: cols.iter().map(|c| c.min_width).collect(),
|
|
cols,
|
|
data: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn feed(&mut self, data: Vec<String>) {
|
|
let mut lines: Vec<Vec<String>> = Vec::new();
|
|
|
|
for (col, ((w, d), c)) in self.widths.iter_mut().zip(data.iter()).zip(self.cols.iter()).enumerate() {
|
|
for (r1, dl) in d.split('\n').enumerate() {
|
|
for (r2, l) in constrained_lines(dl, c.max_width.unwrap_or(usize::MAX)).into_iter().enumerate() {
|
|
let count = l.chars().count();
|
|
|
|
if count > *w {
|
|
*w = count;
|
|
}
|
|
|
|
if let Some(line) = lines.get_mut(r1 + r2) {
|
|
if let Some(pos) = line.get_mut(col) {
|
|
*pos = l.into();
|
|
} else {
|
|
line.push(l.into());
|
|
}
|
|
} else {
|
|
lines.push({
|
|
let mut prev = if (r1 + r2) == 0 {
|
|
data[..col].to_vec()
|
|
} else {
|
|
(0..col).map(|_| "".into()).collect()
|
|
};
|
|
|
|
prev.push(l.into());
|
|
|
|
prev
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for line in lines {
|
|
self.data.push(DataOrSep::Data(line));
|
|
}
|
|
}
|
|
|
|
pub fn separator(&mut self, c: char) {
|
|
self.data.push(DataOrSep::Sep(c));
|
|
}
|
|
|
|
pub fn print(self) -> String {
|
|
let widths = self.widths;
|
|
let cols = self.cols;
|
|
|
|
self.data.into_iter().map(|row| match row {
|
|
DataOrSep::Sep(c) => {
|
|
if c == ' ' {
|
|
"\n".into()
|
|
} else {
|
|
c.to_string().repeat(widths.iter().sum::<usize>() + widths.len() -1) + "\n"
|
|
}
|
|
},
|
|
DataOrSep::Data(d) => {
|
|
d.into_iter().zip(widths.iter()).zip(cols.iter()).map(|((d, &w), c)| {
|
|
let style = c.conditonal_styles.iter().find(|(_s, f)| {
|
|
f(&d)
|
|
}).map(|(s, _f)| s);
|
|
|
|
let s = match c.align {
|
|
Left => rpad(&d, w),
|
|
Right => lpad(&d, w),
|
|
};
|
|
|
|
if let Some(style) = style {
|
|
style.paint(s).to_string()
|
|
} else {
|
|
s
|
|
}
|
|
}).collect::<Vec<_>>().join(" ").trim_end().to_string() + "\n"
|
|
},
|
|
}).collect::<Vec<_>>().join("")
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use crate::test_utils::Ps;
|
|
|
|
use super::*;
|
|
|
|
const LONG_NOTE: &'static str = "chatting with bob about upcoming task, district sharing of images, how the user settings currently works etc. Discussing the fingerprinting / cache busting issue with CKEDITOR, suggesting perhaps looking into forking the rubygem and seeing if we can work in our own changes, however hard that might be.";
|
|
|
|
#[test]
|
|
fn test_constrained_lines_long_text() {
|
|
assert_eq!(constrained_lines(LONG_NOTE, 46), vec![
|
|
"chatting with bob about upcoming task,",
|
|
"district sharing of images, how the user",
|
|
"settings currently works etc. Discussing the",
|
|
"fingerprinting / cache busting issue with",
|
|
"CKEDITOR, suggesting perhaps looking into",
|
|
"forking the rubygem and seeing if we can work",
|
|
"in our own changes, however hard that might",
|
|
"be.",
|
|
]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_constrained_lines_nowrap() {
|
|
assert_eq!(constrained_lines(LONG_NOTE, LONG_NOTE.len()), vec![
|
|
LONG_NOTE,
|
|
]);
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_output() {
|
|
let mut tabs = Tabulate::with_columns(vec![
|
|
Col::min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
|
|
Col::min_width("12:00:00 - 14:00:00 ".len()).and_alignment(Left),
|
|
Col::min_width("Duration".len()).and_alignment(Right),
|
|
Col::min_width("Notes".len()).and_alignment(Left),
|
|
]);
|
|
|
|
tabs.feed(vec!["Day".into(), "Start End".into(), "Duration".into(), "Notes".into()]);
|
|
tabs.feed(vec!["Fri Oct 03, 2008".into(), "12:00:00 - 14:00:00".into(), "2:00:00".into(), "entry 1".into()]);
|
|
tabs.feed(vec!["".into(), "16:00:00 - 18:00:00".into(), "2:00:00".into(), "entry 2".into()]);
|
|
tabs.feed(vec!["".into(), "".into(), "4:00:00".into(), "".into()]);
|
|
tabs.feed(vec!["Sun Oct 05, 2008".into(), "16:00:00 - 18:00:00".into(), "2:00:00".into(), "entry 3".into()]);
|
|
tabs.feed(vec!["".into(), "18:00:00 - ".into(), "2:00:00".into(), "entry 4".into()]);
|
|
tabs.feed(vec!["".into(), "".into(), "4:00:00".into(), "".into()]);
|
|
tabs.separator('-');
|
|
tabs.feed(vec!["Total".into(), "".into(), "8:00:00".into(), "".into()]);
|
|
|
|
assert_eq!(Ps(&tabs.print()), Ps("\
|
|
Day Start End Duration Notes
|
|
Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1
|
|
16:00:00 - 18:00:00 2:00:00 entry 2
|
|
4:00:00
|
|
Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3
|
|
18:00:00 - 2:00:00 entry 4
|
|
4:00:00
|
|
---------------------------------------------------------
|
|
Total 8:00:00
|
|
"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_output_long_duration() {
|
|
let mut tabs = Tabulate::with_columns(vec![
|
|
Col::min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
|
|
Col::min_width("12:00:00 - 14:00:00".len()).and_alignment(Left),
|
|
Col::min_width("Duration".len()).and_alignment(Right),
|
|
Col::min_width("Notes".len()).and_alignment(Left),
|
|
]);
|
|
|
|
tabs.feed(vec!["Day".into(), "Start End".into(), "Duration".into(), "Notes".into()]);
|
|
tabs.feed(vec!["Wed Oct 01, 2008".into(), "12:00:00 - 14:00:00+2d".into(), "50:00:00".into(), "entry 1".into()]);
|
|
tabs.feed(vec!["".into(), "".into(), "50:00:00".into(), "".into()]);
|
|
tabs.feed(vec!["Fri Oct 03, 2008".into(), "12:00:00 - 14:00:00".into(), "2:00:00".into(), "entry 2".into()]);
|
|
tabs.feed(vec!["".into(), "".into(), "2:00:00".into(), "".into()]);
|
|
tabs.separator('-');
|
|
tabs.feed(vec!["Total".into(), "".into(), "52:00:00".into(), "".into()]);
|
|
|
|
assert_eq!(Ps(&tabs.print()), Ps("\
|
|
Day Start End Duration Notes
|
|
Wed Oct 01, 2008 12:00:00 - 14:00:00+2d 50:00:00 entry 1
|
|
50:00:00
|
|
Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 2
|
|
2:00:00
|
|
----------------------------------------------------------
|
|
Total 52:00:00
|
|
"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_output_with_ids() {
|
|
let mut tabs = Tabulate::with_columns(vec![
|
|
Col::min_width(3).and_alignment(Right),
|
|
Col::min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
|
|
Col::min_width("12:00:00 - 14:00:00 ".len()).and_alignment(Left),
|
|
Col::min_width("Duration".len()).and_alignment(Right),
|
|
Col::min_width("Notes".len()).and_alignment(Left),
|
|
]);
|
|
|
|
tabs.feed(vec!["ID".into(), "Day".into(), "Start End".into(), "Duration".into(), "Notes".into()]);
|
|
tabs.feed(vec!["1".into(), "Fri Oct 03, 2008".into(), "12:00:00 - 14:00:00".into(), "2:00:00".into(), "entry 1".into()]);
|
|
tabs.feed(vec!["2".into(), "".into(), "16:00:00 - 18:00:00".into(), "2:00:00".into(), "entry 2".into()]);
|
|
tabs.feed(vec!["".into(), "".into(), "".into(), "4:00:00".into(), "".into()]);
|
|
tabs.feed(vec!["3".into(), "Sun Oct 05, 2008".into(), "16:00:00 - 18:00:00".into(), "2:00:00".into(), "entry 3".into()]);
|
|
tabs.feed(vec!["4".into(), "".into(), "18:00:00 -".into(), "2:00:00".into(), "entry 4".into()]);
|
|
tabs.feed(vec!["".into(), "".into(), "".into(), "4:00:00".into(), "".into()]);
|
|
tabs.separator('-');
|
|
tabs.feed(vec!["".into(), "Total".into(), "".into(), "8:00:00".into()]);
|
|
|
|
assert_eq!(Ps(&tabs.print()), Ps(" ID Day Start End Duration Notes
|
|
1 Fri Oct 03, 2008 12:00:00 - 14:00:00 2:00:00 entry 1
|
|
2 16:00:00 - 18:00:00 2:00:00 entry 2
|
|
4:00:00
|
|
3 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 entry 3
|
|
4 18:00:00 - 2:00:00 entry 4
|
|
4:00:00
|
|
-------------------------------------------------------------
|
|
Total 8:00:00
|
|
"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_output_long_note_with_ids() {
|
|
let mut tabs = Tabulate::with_columns(vec![
|
|
Col::min_width(2).and_alignment(Right),
|
|
Col::min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
|
|
Col::min_width("12:00:00 - 14:00:00 ".len()).and_alignment(Left),
|
|
Col::min_width("Duration".len()).and_alignment(Right),
|
|
Col::min_width("Notes".len()).max_width(44).and_alignment(Left),
|
|
]);
|
|
|
|
tabs.feed(vec!["ID".into(), "Day".into(), "Start End".into(), "Duration".into(), "Notes".into()]);
|
|
tabs.feed(vec!["60000".into(), "Sun Oct 05, 2008".into(), "16:00:00 - 18:00:00".into(), "2:00:00".into(), LONG_NOTE.into()]);
|
|
tabs.feed(vec!["".into(), "".into(), "".into(), "2:00:00".into(), "".into()]);
|
|
tabs.separator('-');
|
|
tabs.feed(vec!["".into(), "Total".into(), "".into(), "2:00:00".into()]);
|
|
|
|
assert_eq!(Ps(&tabs.print()), Ps(" ID Day Start End Duration Notes
|
|
60000 Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 chatting with bob about upcoming task,
|
|
district sharing of images, how the user
|
|
settings currently works etc. Discussing the
|
|
fingerprinting / cache busting issue with
|
|
CKEDITOR, suggesting perhaps looking into
|
|
forking the rubygem and seeing if we can
|
|
work in our own changes, however hard that
|
|
might be.
|
|
2:00:00
|
|
----------------------------------------------------------------------------------------------------
|
|
Total 2:00:00
|
|
"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_output_note_with_line_breaks() {
|
|
let mut tabs = Tabulate::with_columns(vec![
|
|
Col::min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
|
|
Col::min_width("12:00:00 - 14:00:00 ".len()).and_alignment(Left),
|
|
Col::min_width("Duration".len()).and_alignment(Right),
|
|
Col::min_width("Notes".len()).and_alignment(Left),
|
|
]);
|
|
|
|
tabs.feed(vec!["Day".into(), "Start End".into(), "Duration".into(), "Notes".into()]);
|
|
tabs.feed(vec!["Sun Oct 05, 2008".into(), "16:00:00 - 18:00:00".into(), "2:00:00".into(), "first line\nand a second line".into()]);
|
|
tabs.feed(vec!["".into(), "".into(), "2:00:00".into(), "".into()]);
|
|
tabs.separator('-');
|
|
tabs.feed(vec!["Total".into(), "".into(), "2:00:00".into(), "".into()]);
|
|
|
|
assert_eq!(Ps(&tabs.print()), Ps("\
|
|
Day Start End Duration Notes
|
|
Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 first line
|
|
and a second line
|
|
2:00:00
|
|
-------------------------------------------------------------------
|
|
Total 2:00:00
|
|
"));
|
|
}
|
|
|
|
#[test]
|
|
fn note_with_accents() {
|
|
let mut tabs = Tabulate::with_columns(vec![
|
|
Col::min_width("Fri Oct 03, 2008 ".len()).and_alignment(Left),
|
|
Col::min_width("12:00:00 - 14:00:00 ".len()).and_alignment(Left),
|
|
Col::min_width("Duration".len()).and_alignment(Right),
|
|
Col::min_width("Notes".len()).and_alignment(Left),
|
|
]);
|
|
|
|
tabs.feed(vec!["Day".into(), "Start End".into(), "Duration".into(), "Notes".into()]);
|
|
tabs.feed(vec!["Sun Oct 05, 2008".into(), "16:00:00 - 18:00:00".into(), "2:00:00".into(), "quiúbole".into()]);
|
|
tabs.feed(vec!["".into(), "".into(), "2:00:00".into(), "".into()]);
|
|
tabs.separator('-');
|
|
tabs.feed(vec!["Total".into(), "".into(), "2:00:00".into(), "".into()]);
|
|
|
|
assert_eq!(Ps(&tabs.print()), Ps("\
|
|
Day Start End Duration Notes
|
|
Sun Oct 05, 2008 16:00:00 - 18:00:00 2:00:00 quiúbole
|
|
2:00:00
|
|
----------------------------------------------------------
|
|
Total 2:00:00
|
|
"));
|
|
}
|
|
|
|
#[test]
|
|
fn tabulate_a_blank_row() {
|
|
let mut tabs = Tabulate::with_columns(vec![
|
|
Col::new()
|
|
]);
|
|
|
|
tabs.feed(vec!["Hola".into()]);
|
|
tabs.separator(' ');
|
|
tabs.feed(vec!["adiós".into()]);
|
|
tabs.separator('-');
|
|
tabs.feed(vec!["ta güeno".into()]);
|
|
|
|
assert_eq!(Ps(&tabs.print()), Ps("\
|
|
Hola
|
|
|
|
adiós
|
|
--------
|
|
ta güeno
|
|
"));
|
|
}
|
|
|
|
#[test]
|
|
fn add_a_color_condition() {
|
|
let mut tabs = Tabulate::with_columns(vec![
|
|
Col::new().color_if(Style::new().dimmed(), |val| {
|
|
val == "key"
|
|
}),
|
|
Col::new(),
|
|
]);
|
|
|
|
tabs.feed(vec!["foo".into(), "key".into()]);
|
|
tabs.feed(vec!["key".into(), "foo".into()]);
|
|
|
|
assert_eq!(tabs.print(), format!("\
|
|
foo key
|
|
{} foo
|
|
", Style::new().dimmed().paint("key")));
|
|
}
|
|
}
|