display the text of long notes nicely

This commit is contained in:
Abraham Toriz 2021-06-28 19:32:29 -05:00
parent 5b37b63e99
commit e59211f1f9
No known key found for this signature in database
GPG Key ID: D5B4A746DB5DD42A
4 changed files with 205 additions and 15 deletions

55
Cargo.lock generated
View File

@ -13,6 +13,15 @@ dependencies = [
"version_check",
]
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "ansi_term"
version = "0.11.0"
@ -83,7 +92,7 @@ dependencies = [
"atty",
"bitflags",
"strsim",
"textwrap",
"textwrap 0.11.0",
"unicode-width",
"vec_map",
]
@ -312,6 +321,23 @@ dependencies = [
"redox_syscall",
]
[[package]]
name = "regex"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "rusqlite"
version = "0.25.3"
@ -366,6 +392,12 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
name = "smawk"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
[[package]]
name = "strsim"
version = "0.8.0"
@ -392,6 +424,17 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"smawk",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.25"
@ -424,6 +467,7 @@ dependencies = [
"rusqlite",
"serde",
"serde_yaml",
"textwrap 0.14.2",
"thiserror",
"toml",
]
@ -448,6 +492,15 @@ dependencies = [
"serde",
]
[[package]]
name = "unicode-linebreak"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05a31f45d18a3213b918019f78fe6a73a14ab896807f0aaf5622aa0684749455"
dependencies = [
"regex",
]
[[package]]
name = "unicode-width"
version = "0.1.8"

View File

@ -16,6 +16,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8"
toml = "0.5"
itertools = "0.10"
textwrap = "0.14"
[dev-dependencies]
pretty_assertions = "0.7.2"

View File

@ -76,6 +76,6 @@ impl<'a> Command<'a> for DisplayCommand {
}
};
args.format.print_formatted(sheets_to_display, out, Utc::now(), Local.offset_from_utc_datetime(&Utc::now().naive_utc()), args.ids)
args.format.print_formatted(sheets_to_display, out, Utc::now(), Local.offset_from_utc_datetime(&Utc::now().naive_utc()), args.ids, 100)
}
}

View File

@ -1,5 +1,6 @@
use std::str::FromStr;
use std::io::Write;
use std::borrow::Cow;
use serde::{Serialize, Deserialize};
use itertools::Itertools;
@ -44,6 +45,11 @@ fn format_end(start: NaiveDateTime, end: NaiveDateTime) -> String {
)
}
fn constrained_lines(text: &str, width: usize) -> Vec<Cow<'_, str>> {
textwrap::wrap(text, width)
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Formatter {
@ -58,9 +64,9 @@ impl Formatter {
/// to the local timezone is given in `offset` to prevent this function from
/// using a secondary effect to retrieve the time and conver dates. This
/// also makes it easier to test.
pub fn print_formatted<W: Write, O: Offset>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, offset: O, ids: bool) -> error::Result<()> {
pub fn print_formatted<W: Write, O: Offset>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, offset: O, ids: bool, term_width: usize) -> error::Result<()> {
match &self {
Formatter::Text => self.print_formatted_text(entries, out, now, offset, ids)?,
Formatter::Text => self.print_formatted_text(entries, out, now, offset, ids, term_width)?,
Formatter::Custom(name) => {
}
}
@ -70,7 +76,7 @@ impl Formatter {
/// Print in the default text format. Assume entries are sorted by sheet and
/// then by start
fn print_formatted_text<W: Write, O: Offset>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, offset: O, ids: bool) -> error::Result<()> {
fn print_formatted_text<W: Write, O: Offset>(&self, entries: Vec<Entry>, out: &mut W, now: DateTime<Utc>, offset: O, ids: bool, term_width: usize) -> error::Result<()> {
let grouped_entries = entries.into_iter().group_by(|e| e.sheet.to_string());
// Build a timezone based on the offset given to this function. This
@ -138,17 +144,32 @@ impl Formatter {
"Notes",
)?;
let mut max_note_length = 0;
for [id, date, start, end, duration, note] in lines {
writeln!(
out,
"{} {} {} {} {} {}",
let first_line = format!(
"{} {} {} {} {} ",
lpad(&id, 3.max(lengths[0])),
rpad(&date, 18),
rpad(&start, 10),
rpad(&end, 10.max(lengths[3])),
lpad(&duration, 8.max(lengths[4])),
note,
)?;
);
let space_left = term_width.saturating_sub(first_line.len()).max(40);
let note_lines = constrained_lines(&note, space_left);
for (i, note_line) in note_lines.into_iter().enumerate() {
if i == 0 {
writeln!(out, "{}{}", first_line, note_line)?;
} else {
writeln!(out, "{}{}", " ".repeat(first_line.len()), note_line)?;
}
if note_line.len() > max_note_length {
max_note_length = note_line.len();
}
}
}
writeln!(out,
@ -158,7 +179,7 @@ impl Formatter {
"-".repeat(10),
"-".repeat(10.max(lengths[3])),
"-".repeat(8.max(lengths[4])),
"-".repeat(4.max(lengths[5])),
"-".repeat(4.max(max_note_length)),
)?;
writeln!(out,
"{} {} {} {} {}",
@ -195,6 +216,8 @@ mod tests {
use pretty_assertions::assert_eq;
use chrono::TimeZone;
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.";
#[derive(PartialEq, Eq)]
pub struct PrettyString<'a>(pub &'a str);
@ -205,6 +228,20 @@ mod tests {
}
}
#[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_text_output() {
let formatter = Formatter::Text;
@ -219,7 +256,7 @@ mod tests {
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, false).unwrap();
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
@ -245,7 +282,7 @@ mod tests {
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, false).unwrap();
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
@ -268,7 +305,7 @@ mod tests {
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, false).unwrap();
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
@ -295,7 +332,7 @@ mod tests {
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, true).unwrap();
formatter.print_formatted(entries, &mut output, now, offset, true, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
ID Day Start End Duration Notes
@ -307,6 +344,105 @@ mod tests {
4:00:00
---------------------------------------------------------
Total 8:00:00
"));
}
#[test]
fn test_text_output_long_note_default_with() {
let formatter = Formatter::Text;
let mut output = Vec::new();
let entries = vec![
Entry {
id: 1,
sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
note: LONG_NOTE.into(),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
Day Start End Duration Notes
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_long_note_with_ids() {
let formatter = Formatter::Text;
let mut output = Vec::new();
let entries = vec![
Entry {
id: 60000,
sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
note: LONG_NOTE.into(),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, true, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
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 formatter = Formatter::Text;
let mut output = Vec::new();
let entries = vec![
Entry {
id: 1,
sheet: "default".into(),
start: Utc.ymd(2008, 10, 5).and_hms(16, 0, 0),
end: Some(Utc.ymd(2008, 10, 5).and_hms(18, 0, 0)),
note: "first line\nand a second line".into(),
},
];
let now = Utc.ymd(2008, 10, 5).and_hms(20, 0, 0);
let offset = Utc;
formatter.print_formatted(entries, &mut output, now, offset, false, 100).unwrap();
assert_eq!(PrettyString(&String::from_utf8_lossy(&output)), PrettyString("Timesheet: default
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
"));
}
}