From e59211f1f90a3e458174ebde5dc7b76b8be83e39 Mon Sep 17 00:00:00 2001 From: Abraham Toriz Date: Mon, 28 Jun 2021 19:32:29 -0500 Subject: [PATCH] display the text of long notes nicely --- Cargo.lock | 55 +++++++++++++- Cargo.toml | 1 + src/commands/display.rs | 2 +- src/formatters.rs | 162 ++++++++++++++++++++++++++++++++++++---- 4 files changed, 205 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31560dc..181f0b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 7ae8f1a..c2ef8f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/commands/display.rs b/src/commands/display.rs index 601cab9..48ac3d5 100644 --- a/src/commands/display.rs +++ b/src/commands/display.rs @@ -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) } } diff --git a/src/formatters.rs b/src/formatters.rs index 19e9dab..7280c3a 100644 --- a/src/formatters.rs +++ b/src/formatters.rs @@ -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> { + 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(&self, entries: Vec, out: &mut W, now: DateTime, offset: O, ids: bool) -> error::Result<()> { + pub fn print_formatted(&self, entries: Vec, out: &mut W, now: DateTime, 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(&self, entries: Vec, out: &mut W, now: DateTime, offset: O, ids: bool) -> error::Result<()> { + fn print_formatted_text(&self, entries: Vec, out: &mut W, now: DateTime, 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(¬e, 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 ")); } }