mirror of
https://github.com/nikolassv/bartib
synced 2024-12-03 16:59:12 +00:00
Merge branch 'dev/wrapCells'
This commit is contained in:
commit
5705b4dce5
7 changed files with 493 additions and 135 deletions
105
Cargo.lock
generated
105
Cargo.lock
generated
|
@ -2,13 +2,22 @@
|
|||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -25,7 +34,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
|||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -42,6 +51,8 @@ dependencies = [
|
|||
"chrono",
|
||||
"clap",
|
||||
"nu-ansi-term",
|
||||
"term_size",
|
||||
"textwrap 0.14.2",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
|
@ -61,7 +72,7 @@ dependencies = [
|
|||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -74,7 +85,7 @@ dependencies = [
|
|||
"atty",
|
||||
"bitflags",
|
||||
"strsim",
|
||||
"textwrap",
|
||||
"textwrap 0.11.0",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
]
|
||||
|
@ -103,12 +114,28 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||
dependencies = [
|
||||
"winapi 0.2.8",
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7282d924be3275cec7f6756ff4121987bc6481325397dde6ba3e7802b1a8b1c"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.34.0"
|
||||
|
@ -117,7 +144,7 @@ checksum = "cccf5ce0705f83c8afb776d00fe071ab994148efdd8e060909c6614b0fe740af"
|
|||
dependencies = [
|
||||
"itertools",
|
||||
"overload",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -163,6 +190,29 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[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 = "smawk"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.8.0"
|
||||
|
@ -180,6 +230,17 @@ dependencies = [
|
|||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term_size"
|
||||
version = "1.0.0-beta1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8a17d8699e154863becdf18e4fd28bd0be27ca72856f54daf75c00f2566898f"
|
||||
dependencies = [
|
||||
"kernel32-sys",
|
||||
"libc",
|
||||
"winapi 0.2.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.11.0"
|
||||
|
@ -189,6 +250,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.24"
|
||||
|
@ -217,7 +289,16 @@ checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-linebreak"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -244,6 +325,12 @@ version = "0.10.0+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
@ -254,6 +341,12 @@ dependencies = [
|
|||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-build"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
|
|
|
@ -11,4 +11,6 @@ chrono = "0.4"
|
|||
clap = "~2.33"
|
||||
thiserror = "1.0"
|
||||
anyhow = "1.0.42"
|
||||
nu-ansi-term = "0.34.0"
|
||||
nu-ansi-term = "0.34.0"
|
||||
term_size = "1.0.0-beta1"
|
||||
textwrap = "0.14.2"
|
|
@ -1,3 +1,5 @@
|
|||
pub static FORMAT_DATETIME: &str = "%F %R";
|
||||
pub static FORMAT_TIME: &str = "%R";
|
||||
pub static FORMAT_DATE: &str = "%F";
|
||||
pub static DEFAULT_WIDTH : usize = 80;
|
||||
pub static REPORT_INDENTATION : usize = 4;
|
||||
|
|
|
@ -12,7 +12,7 @@ pub fn format_duration(duration: &Duration) -> String {
|
|||
}
|
||||
|
||||
if duration.num_minutes() > 0 {
|
||||
duration_string.push_str(&format!("{}m", duration.num_minutes() % 60));
|
||||
duration_string.push_str(&format!("{:0>2}m", duration.num_minutes() % 60));
|
||||
} else {
|
||||
duration_string.push_str(&format!("{}s", duration.num_seconds() % 60));
|
||||
}
|
||||
|
|
|
@ -14,13 +14,7 @@ pub fn list_activities(activities: &[&activity::Activity], with_start_dates: boo
|
|||
return;
|
||||
}
|
||||
|
||||
let mut activity_table = table::Table::new(vec![
|
||||
"Started".to_string(),
|
||||
"Stopped".to_string(),
|
||||
"Description".to_string(),
|
||||
"Project".to_string(),
|
||||
"Duration".to_string(),
|
||||
]);
|
||||
let mut activity_table = create_activity_table();
|
||||
|
||||
activities
|
||||
.iter()
|
||||
|
@ -37,13 +31,7 @@ pub fn list_activities_grouped_by_date(activities: &[&activity::Activity]) {
|
|||
return;
|
||||
}
|
||||
|
||||
let mut activity_table = table::Table::new(vec![
|
||||
"Started".to_string(),
|
||||
"Stopped".to_string(),
|
||||
"Description".to_string(),
|
||||
"Project".to_string(),
|
||||
"Duration".to_string(),
|
||||
]);
|
||||
let mut activity_table = create_activity_table();
|
||||
|
||||
group_activities_by_date(activities)
|
||||
.iter()
|
||||
|
@ -55,6 +43,16 @@ pub fn list_activities_grouped_by_date(activities: &[&activity::Activity]) {
|
|||
println!("\n{}", activity_table);
|
||||
}
|
||||
|
||||
fn create_activity_table() -> table::Table {
|
||||
table::Table::new(vec![
|
||||
table::Column{ label: "Started".to_string(), wrap: table::Wrap::NoWrap },
|
||||
table::Column{ label: "Stopped".to_string(), wrap: table::Wrap::NoWrap },
|
||||
table::Column{ label: "Description".to_string(), wrap: table::Wrap::Wrap },
|
||||
table::Column{ label: "Project".to_string(), wrap: table::Wrap::Wrap },
|
||||
table::Column{ label: "Duration".to_string(), wrap: table::Wrap::NoWrap },
|
||||
])
|
||||
}
|
||||
|
||||
fn create_activites_group(title: &str, activities: &[&activity::Activity]) -> table::Group {
|
||||
let rows = activities
|
||||
.iter()
|
||||
|
@ -69,10 +67,10 @@ pub fn list_running_activities(running_activities: &[&activity::Activity]) {
|
|||
println!("No Activity is currently running");
|
||||
} else {
|
||||
let mut activity_table = table::Table::new(vec![
|
||||
"Started At".to_string(),
|
||||
"Description".to_string(),
|
||||
"Project".to_string(),
|
||||
"Duration".to_string(),
|
||||
table::Column{ label: "Started At".to_string(), wrap: table::Wrap::NoWrap },
|
||||
table::Column{ label: "Description".to_string(), wrap: table::Wrap::Wrap },
|
||||
table::Column{ label: "Project".to_string(), wrap: table::Wrap::Wrap },
|
||||
table::Column{ label: "Duration".to_string(), wrap: table::Wrap::NoWrap },
|
||||
]);
|
||||
|
||||
running_activities
|
||||
|
@ -97,9 +95,9 @@ pub fn list_descriptions_and_projects(descriptions_and_projects : &[(&String, &S
|
|||
println!("No activities have been tracked yet");
|
||||
} else {
|
||||
let mut descriptions_and_projects_table = table::Table::new(vec![
|
||||
" # ".to_string(),
|
||||
"Description".to_string(),
|
||||
"Project".to_string()
|
||||
table::Column{ label: " # ".to_string(), wrap: table::Wrap::NoWrap },
|
||||
table::Column{ label: "Description".to_string(), wrap: table::Wrap::Wrap },
|
||||
table::Column{ label: "Project".to_string(), wrap: table::Wrap::Wrap },
|
||||
]);
|
||||
|
||||
let mut i = descriptions_and_projects.len();
|
||||
|
|
|
@ -1,104 +1,150 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Add;
|
||||
|
||||
use chrono::Duration;
|
||||
use nu_ansi_term::Style;
|
||||
use textwrap;
|
||||
|
||||
use crate::conf;
|
||||
use crate::data::activity;
|
||||
use crate::view::format_util;
|
||||
|
||||
type ProjectMap<'a> = BTreeMap<&'a str, (Vec<&'a activity::Activity>, Duration)>;
|
||||
|
||||
struct Report<'a> {
|
||||
activities : &'a[&'a activity::Activity]
|
||||
project_map: ProjectMap<'a>,
|
||||
total_duration: Duration
|
||||
}
|
||||
|
||||
impl<'a> Report<'a> {
|
||||
fn new(activities : &'a[&'a activity::Activity]) -> Report<'a> {
|
||||
Report { activities }
|
||||
fn new(activities: &'a [&'a activity::Activity]) -> Report<'a> {
|
||||
Report {
|
||||
project_map: create_project_map(&activities),
|
||||
total_duration: sum_duration(&activities)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for Report<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut longest_line = get_longest_line(&self.project_map).unwrap_or(0);
|
||||
let longest_duration_string = get_longest_duration_string(&self).unwrap_or(0);
|
||||
|
||||
let project_map = group_activities_by_project(self.activities);
|
||||
let longest_line = get_longest_line(&project_map).unwrap_or(0);
|
||||
let terminal_width = term_size::dimensions_stdout().map(|d| d.0)
|
||||
.unwrap_or(conf::DEFAULT_WIDTH);
|
||||
|
||||
for (project, activities) in project_map.iter() {
|
||||
let project_duration = sum_duration(activities);
|
||||
if terminal_width < longest_line + longest_duration_string + 1 {
|
||||
longest_line = terminal_width - longest_duration_string - 1;
|
||||
}
|
||||
|
||||
writeln!(f, "{prefix}{project:.<width$} {duration}{suffix}",
|
||||
prefix = Style::new().bold().prefix(),
|
||||
project = project,
|
||||
width = longest_line,
|
||||
duration = format_util::format_duration(&project_duration),
|
||||
suffix = Style::new().bold().infix(Style::new())
|
||||
)?;
|
||||
for (project, (activities, duration)) in self.project_map.iter() {
|
||||
print_project_heading(f, project, duration, longest_line, longest_duration_string)?;
|
||||
|
||||
print_descriptions_with_durations(f, activities, longest_line)?;
|
||||
print_descriptions_with_durations(f, activities, longest_line, longest_duration_string)?;
|
||||
writeln!(f, "")?;
|
||||
}
|
||||
|
||||
print_total_duration(f, self.activities, longest_line)?;
|
||||
print_total_duration(f, self.total_duration, longest_line)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub fn show_activities<'a>(activities : &'a[&'a activity::Activity]) {
|
||||
pub fn show_activities<'a>(activities: &'a [&'a activity::Activity]) {
|
||||
let report = Report::new(activities);
|
||||
println!("\n{}", report);
|
||||
}
|
||||
|
||||
fn print_descriptions_with_durations<'a>(f: &mut fmt::Formatter<'_>, activities : &'a[&'a activity::Activity], line_width : usize) -> fmt::Result {
|
||||
fn create_project_map<'a>(activities: &'a [&'a activity::Activity]) -> ProjectMap {
|
||||
let mut project_map: ProjectMap = BTreeMap::new();
|
||||
|
||||
activities.iter().for_each(|a| {
|
||||
project_map.entry(&a.project)
|
||||
.or_insert_with(|| (Vec::<&'a activity::Activity>::new(), Duration::seconds(0)))
|
||||
.0.push(a);
|
||||
});
|
||||
|
||||
for (_project, (activities, duration)) in project_map.iter_mut() {
|
||||
*duration = sum_duration(activities);
|
||||
}
|
||||
|
||||
project_map
|
||||
}
|
||||
|
||||
fn sum_duration(activities: &[&activity::Activity]) -> Duration {
|
||||
let mut duration = Duration::seconds(0);
|
||||
|
||||
for activity in activities {
|
||||
duration = duration.add(activity.get_duration());
|
||||
}
|
||||
|
||||
duration
|
||||
}
|
||||
|
||||
fn print_project_heading(f: &mut Formatter, project: &&str, duration: &Duration, longest_line: usize, duration_width: usize) -> fmt::Result {
|
||||
write!(f, "{}", Style::new().bold().prefix())?;
|
||||
let project_lines = textwrap::wrap(project, textwrap::Options::new(longest_line));
|
||||
|
||||
for (i, line) in project_lines.iter().enumerate() {
|
||||
if i + 1 < project_lines.len() {
|
||||
writeln!(f, "{}", line)?;
|
||||
} else {
|
||||
write!(f, "{line:.<width$} {duration:>duration_width$}",
|
||||
line = line,
|
||||
width = longest_line,
|
||||
duration = format_util::format_duration(duration),
|
||||
duration_width = duration_width
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(f, "{}", Style::new().bold().infix(Style::new()))
|
||||
}
|
||||
|
||||
fn print_descriptions_with_durations<'a>(f: &mut fmt::Formatter<'_>, activities: &'a [&'a activity::Activity], line_width: usize, duration_width: usize) -> fmt::Result {
|
||||
let description_map = group_activities_by_description(activities);
|
||||
|
||||
let indent_string = " ".repeat(conf::REPORT_INDENTATION);
|
||||
let wrapping_options = textwrap::Options::new(line_width)
|
||||
.initial_indent(&indent_string)
|
||||
.subsequent_indent(&indent_string);
|
||||
|
||||
for (description, activities) in description_map.iter() {
|
||||
let description_duration = sum_duration(activities);
|
||||
let description_lines = textwrap::wrap(description, &wrapping_options);
|
||||
|
||||
writeln!(f, " {description:.<width$} {duration}",
|
||||
description = description,
|
||||
width = line_width - 4,
|
||||
duration = format_util::format_duration(&description_duration)
|
||||
)?;
|
||||
for (i, line) in description_lines.iter().enumerate() {
|
||||
if i + 1 < description_lines.len() {
|
||||
writeln!(f, "{}", line)?;
|
||||
} else {
|
||||
writeln!(f, "{line:.<width$} {duration:>duration_width$}",
|
||||
line = line,
|
||||
width = line_width,
|
||||
duration = format_util::format_duration(&description_duration),
|
||||
duration_width = duration_width
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_total_duration<'a>(f: &mut fmt::Formatter<'_>, activities : &'a[&'a activity::Activity], line_width : usize) -> fmt::Result {
|
||||
let total_duration = sum_duration(activities);
|
||||
|
||||
if activities.is_empty() {
|
||||
writeln!(f, "You have not tracked any activities in the given time range")?;
|
||||
} else {
|
||||
writeln!(f, "{prefix}{total:.<width$} {duration}{suffix}",
|
||||
prefix = Style::new().bold().prefix(),
|
||||
total = "Total",
|
||||
width = line_width,
|
||||
duration = format_util::format_duration(&total_duration),
|
||||
suffix = Style::new().bold().infix(Style::new())
|
||||
)?;
|
||||
}
|
||||
fn print_total_duration<'a>(f: &mut fmt::Formatter<'_>, total_duration: Duration, line_width: usize) -> fmt::Result {
|
||||
writeln!(f, "{prefix}{total:.<width$} {duration}{suffix}",
|
||||
prefix = Style::new().bold().prefix(),
|
||||
total = "Total",
|
||||
width = line_width,
|
||||
duration = format_util::format_duration(&total_duration),
|
||||
suffix = Style::new().bold().infix(Style::new())
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
fn group_activities_by_project<'a>(activities : &'a[&'a activity::Activity]) -> BTreeMap<&str, Vec<&'a activity::Activity>> {
|
||||
let mut project_map : BTreeMap<&str, Vec<&activity::Activity>> = BTreeMap::new();
|
||||
|
||||
activities.iter().for_each(|a| {
|
||||
project_map.entry(&a.project)
|
||||
.or_insert_with(Vec::<&'a activity::Activity>::new)
|
||||
.push(a);
|
||||
});
|
||||
|
||||
project_map
|
||||
}
|
||||
|
||||
fn group_activities_by_description<'a>(activities : &'a[&'a activity::Activity]) -> BTreeMap<&str, Vec<&'a activity::Activity>> {
|
||||
let mut activity_map : BTreeMap<&str, Vec<&'a activity::Activity>> = BTreeMap::new();
|
||||
fn group_activities_by_description<'a>(activities: &'a [&'a activity::Activity]) -> BTreeMap<&str, Vec<&'a activity::Activity>> {
|
||||
let mut activity_map: BTreeMap<&str, Vec<&'a activity::Activity>> = BTreeMap::new();
|
||||
|
||||
activities.iter().for_each(|a| {
|
||||
activity_map.entry(&a.description)
|
||||
|
@ -109,23 +155,34 @@ fn group_activities_by_description<'a>(activities : &'a[&'a activity::Activity])
|
|||
activity_map
|
||||
}
|
||||
|
||||
fn sum_duration(activities : &[&activity::Activity]) -> Duration {
|
||||
let mut duration = Duration::seconds(0);
|
||||
|
||||
for activity in activities {
|
||||
duration = duration.add(activity.get_duration());
|
||||
}
|
||||
|
||||
duration
|
||||
}
|
||||
|
||||
fn get_longest_line(project_map : &BTreeMap<&str, Vec<&activity::Activity>>) -> Option<usize> {
|
||||
fn get_longest_line(project_map: &ProjectMap) -> Option<usize> {
|
||||
let longest_project_line = project_map.keys().map(|p| p.chars().count()).max();
|
||||
let longest_activity_line = project_map.values().flatten().map(|a| a.description.chars().count() + 4).max();
|
||||
let longest_activity_line = project_map.values()
|
||||
.map(|(a, _d)| a)
|
||||
.flatten()
|
||||
.map(|a| a.description.chars().count() + conf::REPORT_INDENTATION).max();
|
||||
get_max_option(longest_project_line, longest_activity_line)
|
||||
}
|
||||
|
||||
fn get_max_option(o1 : Option<usize>, o2: Option<usize>) -> Option<usize> {
|
||||
fn get_longest_duration_string(report: &Report) -> Option<usize> {
|
||||
let longest_project_duration = report.project_map.values()
|
||||
.map(|(_a, d)| format_util::format_duration(&d))
|
||||
.map(|s| s.chars().count())
|
||||
.max();
|
||||
let longest_activity_duration = report.project_map.values()
|
||||
.map(|(a, _d)| a)
|
||||
.flatten()
|
||||
.map(|a| format_util::format_duration(&a.get_duration()))
|
||||
.map(|s| s.chars().count())
|
||||
.max();
|
||||
|
||||
let longest_single_duration = get_max_option(longest_project_duration, longest_activity_duration);
|
||||
let length_of_total_duration = format_util::format_duration(&report.total_duration).chars().count();
|
||||
|
||||
get_max_option(longest_single_duration, Some(length_of_total_duration))
|
||||
}
|
||||
|
||||
fn get_max_option(o1: Option<usize>, o2: Option<usize>) -> Option<usize> {
|
||||
if let Some(s1) = o1 {
|
||||
if let Some(s2) = o2 {
|
||||
if s1 > s2 { o1 } else { o2 }
|
||||
|
@ -139,12 +196,13 @@ fn get_max_option(o1 : Option<usize>, o2: Option<usize>) -> Option<usize> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sum_duration_test() {
|
||||
let mut activities : Vec<&activity::Activity> = Vec::new();
|
||||
let mut activities: Vec<&activity::Activity> = Vec::new();
|
||||
assert_eq!(sum_duration(&activities).num_seconds(), 0);
|
||||
|
||||
let mut a1 = activity::Activity::start("p1".to_string(), "d1".to_string(), Some(NaiveDateTime::parse_from_str("2021-09-01 15:00:00", "%Y-%m-%d %H:%M:%S").unwrap()));
|
||||
|
@ -167,16 +225,16 @@ mod tests {
|
|||
let a2 = activity::Activity::start("p1".to_string(), "d2".to_string(), None);
|
||||
let a3 = activity::Activity::start("p2".to_string(), "d1".to_string(), None);
|
||||
|
||||
let mut activities : Vec<&activity::Activity> = Vec::new();
|
||||
let mut activities: Vec<&activity::Activity> = Vec::new();
|
||||
activities.push(&a1);
|
||||
activities.push(&a2);
|
||||
activities.push(&a3);
|
||||
let m = group_activities_by_project(&activities);
|
||||
let m = create_project_map(&activities);
|
||||
|
||||
assert_eq!(m.len(), 2);
|
||||
assert_eq!(m.get("p1").unwrap().len(), 2);
|
||||
assert_eq!(m.get("p2").unwrap().len(), 1);
|
||||
}
|
||||
assert_eq!(m.get("p1").unwrap().0.len(), 2);
|
||||
assert_eq!(m.get("p2").unwrap().0.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn group_activities_by_description_test() {
|
||||
|
@ -185,7 +243,7 @@ mod tests {
|
|||
let a3 = activity::Activity::start("p2".to_string(), "d1".to_string(), None);
|
||||
let a4 = activity::Activity::start("p2".to_string(), "d1".to_string(), None);
|
||||
|
||||
let mut activities : Vec<&activity::Activity> = Vec::new();
|
||||
let mut activities: Vec<&activity::Activity> = Vec::new();
|
||||
activities.push(&a1);
|
||||
activities.push(&a2);
|
||||
activities.push(&a3);
|
||||
|
@ -199,8 +257,8 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn get_longest_line_test() {
|
||||
let mut activities : Vec<&activity::Activity> = Vec::new();
|
||||
let project_map1 = group_activities_by_project(&activities);
|
||||
let mut activities: Vec<&activity::Activity> = Vec::new();
|
||||
let project_map1 = create_project_map(&activities);
|
||||
|
||||
// keine Einträge -> keine Längste Zeile
|
||||
assert_eq!(get_longest_line(&project_map1), None);
|
||||
|
@ -218,15 +276,14 @@ mod tests {
|
|||
activities.push(&a5);
|
||||
|
||||
// längste Zeile ist Description + 4
|
||||
let project_map2 = group_activities_by_project(&activities);
|
||||
let project_map2 = create_project_map(&activities);
|
||||
assert_eq!(get_longest_line(&project_map2).unwrap(), 6);
|
||||
|
||||
// längste Zeile ist Projektname mit 8 Zeichen
|
||||
let a6 = activity::Activity::start("p1234567".to_string(), "d1".to_string(), None);
|
||||
activities.push(&a6);
|
||||
let project_map3 = group_activities_by_project(&activities);
|
||||
let project_map3 = create_project_map(&activities);
|
||||
assert_eq!(get_longest_line(&project_map3).unwrap(), 8);
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
use std::cmp;
|
||||
use std::fmt;
|
||||
use std::str;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use nu_ansi_term::Style;
|
||||
use textwrap;
|
||||
|
||||
use crate::conf;
|
||||
|
||||
pub enum Wrap {
|
||||
Wrap,
|
||||
NoWrap
|
||||
}
|
||||
|
||||
pub struct Column {
|
||||
pub label: String,
|
||||
pub wrap: Wrap
|
||||
}
|
||||
|
||||
pub struct Row {
|
||||
content: Vec<String>,
|
||||
|
@ -15,7 +29,7 @@ pub struct Group {
|
|||
}
|
||||
|
||||
pub struct Table {
|
||||
header: Vec<String>,
|
||||
columns: Vec<Column>,
|
||||
rows: Vec<Row>,
|
||||
groups: Vec<Group>,
|
||||
}
|
||||
|
@ -40,9 +54,9 @@ impl Group {
|
|||
}
|
||||
|
||||
impl Table {
|
||||
pub fn new(header: Vec<String>) -> Table {
|
||||
pub fn new(columns: Vec<Column>) -> Table {
|
||||
Table {
|
||||
header,
|
||||
columns,
|
||||
groups: Vec::new(),
|
||||
rows: Vec::new(),
|
||||
}
|
||||
|
@ -64,8 +78,75 @@ impl Table {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn get_column_width(&self) -> Vec<usize> {
|
||||
let mut column_width: Vec<usize> = self.header.iter().map(|e| e.chars().count()).collect();
|
||||
/* Calculates the widths for the columns in a table
|
||||
|
||||
If the width of the longest line in the table exceeds the maximum width for the output
|
||||
all the wrapable columns shrink to an acceptable size.
|
||||
*/
|
||||
fn get_column_width(&self, max_width: usize) -> Vec<usize> {
|
||||
let mut max_column_width = self.get_max_column_width();
|
||||
|
||||
let width : usize = max_column_width.iter().sum();
|
||||
let columns_wrap : Vec<&Wrap> = self.columns.iter().map(|c| &c.wrap).collect();
|
||||
let mut number_of_wrappable_columns : usize = columns_wrap.iter().filter(|w| matches!(w, Wrap::Wrap)).count();
|
||||
|
||||
if width <= max_width || number_of_wrappable_columns == 0 {
|
||||
// we do not need to or can not wrap
|
||||
return max_column_width;
|
||||
}
|
||||
|
||||
// the total width of the columns that we may not wrap
|
||||
let unwrapable_width : usize = max_column_width.iter().zip(columns_wrap.iter())
|
||||
.filter(|(_, wrap)| matches!(wrap, Wrap::NoWrap))
|
||||
.map(|(width, _)| width)
|
||||
.sum();
|
||||
|
||||
if unwrapable_width > max_width {
|
||||
// In this case we can not get any decent layout with wrapping. We rather do not wrap at all
|
||||
return max_column_width;
|
||||
}
|
||||
|
||||
// we start with a width of 0 for all the wrapable columns
|
||||
let mut column_width : Vec<usize> = max_column_width.iter().zip(columns_wrap.iter())
|
||||
.map(|(width, wrap)| if matches!(wrap, Wrap::NoWrap) { width.clone() } else { 0 })
|
||||
.collect();
|
||||
|
||||
// then we distribute the available width to the wrappable columns
|
||||
let mut available_width_for_wrappable_columns = max_width - unwrapable_width;
|
||||
|
||||
while available_width_for_wrappable_columns > 0 && number_of_wrappable_columns > 0 {
|
||||
|
||||
// the maximum additional width we give each column in this round
|
||||
let additional_width_for_each = cmp::max(1, available_width_for_wrappable_columns / number_of_wrappable_columns);
|
||||
let width_data = column_width.iter_mut().zip(max_column_width.iter_mut()).zip(columns_wrap.iter());
|
||||
|
||||
for ((width, max_width), wrap) in width_data {
|
||||
if available_width_for_wrappable_columns > 0 && matches!(wrap, Wrap::Wrap) && width < max_width {
|
||||
if max_width > &mut width.saturating_add(additional_width_for_each) {
|
||||
// While the maximum width for this column will not be reached, we add all
|
||||
// the additional width
|
||||
available_width_for_wrappable_columns -= additional_width_for_each;
|
||||
*width = width.saturating_add(additional_width_for_each);
|
||||
} else {
|
||||
// The column does not need all the additional width. We give it only the
|
||||
// additional width it needs
|
||||
available_width_for_wrappable_columns -= *max_width - *width;
|
||||
*width = *max_width;
|
||||
|
||||
// this column won't need any more width
|
||||
number_of_wrappable_columns -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
column_width
|
||||
}
|
||||
|
||||
fn get_max_column_width(&self) -> Vec<usize> {
|
||||
let mut max_column_width: Vec<usize> = self.columns.iter()
|
||||
.map(|c| c.label.chars().count())
|
||||
.collect();
|
||||
|
||||
for row in self.get_all_rows() {
|
||||
row.content
|
||||
|
@ -73,25 +154,30 @@ impl Table {
|
|||
.map(|cell| cell.chars().count())
|
||||
.enumerate()
|
||||
.for_each(|(i, char_count)| {
|
||||
if let Some(old_w) = column_width.get(i) {
|
||||
column_width[i] = cmp::max(char_count, *old_w);
|
||||
if let Some(old_w) = max_column_width.get(i) {
|
||||
max_column_width[i] = cmp::max(char_count, *old_w);
|
||||
} else {
|
||||
column_width.push(char_count);
|
||||
max_column_width.push(char_count);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
column_width
|
||||
max_column_width
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Table {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let column_width = self.get_column_width();
|
||||
|
||||
let terminal_width = term_size::dimensions_stdout().map(|d| d.0)
|
||||
.unwrap_or(conf::DEFAULT_WIDTH);
|
||||
|
||||
let column_width = self.get_column_width(terminal_width - self.columns.len());
|
||||
|
||||
let labels : Vec<&String> = self.columns.iter().map(|c| &c.label).collect();
|
||||
|
||||
write_cells(
|
||||
f,
|
||||
&self.header,
|
||||
&labels,
|
||||
&column_width,
|
||||
Some(Style::new().underline()),
|
||||
)?;
|
||||
|
@ -139,15 +225,33 @@ fn write_cells<T: AsRef<str> + std::fmt::Display>(
|
|||
column_width: &[usize],
|
||||
style: Option<Style>,
|
||||
) -> fmt::Result {
|
||||
let cells_with_width: Vec<(Option<&usize>, &str)> = cells
|
||||
|
||||
let wrapped_cells : Vec<Vec<Cow<str>>> = cells
|
||||
.iter()
|
||||
.map(|cell| cell.as_ref())
|
||||
.enumerate()
|
||||
.map(|(i, cell)| (column_width.get(i), cell))
|
||||
.map(|(i, c)| match column_width.get(i) {
|
||||
Some(s) => textwrap::wrap(c.as_ref(), textwrap::Options::new(*s)),
|
||||
None => {
|
||||
let mut lines = Vec::new();
|
||||
lines.push(Cow::from(c.as_ref()));
|
||||
lines
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (width, cell) in cells_with_width {
|
||||
write_with_width_and_style(f, cell, width, style)?;
|
||||
let most_lines : usize = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1);
|
||||
|
||||
for line in 0..most_lines {
|
||||
for (width, wrapped_cell) in column_width.iter().zip(wrapped_cells.iter()) {
|
||||
|
||||
match wrapped_cell.get(line) {
|
||||
Some(c) => write_with_width_and_style(f, c, width, style)?,
|
||||
None => write!(f, "{} ", "\u{a0}".repeat(*width))?
|
||||
}
|
||||
}
|
||||
|
||||
let is_last_line = line + 1 < most_lines;
|
||||
if is_last_line { writeln!(f)?; }
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -156,17 +260,17 @@ fn write_cells<T: AsRef<str> + std::fmt::Display>(
|
|||
fn write_with_width_and_style(
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
content: &str,
|
||||
opt_width: Option<&usize>,
|
||||
width: &usize,
|
||||
opt_style: Option<Style>,
|
||||
) -> fmt::Result {
|
||||
let content_length = content.chars().count();
|
||||
let style_prefix = opt_style.map_or("".to_string(), |style| style.prefix().to_string());
|
||||
let style_suffix = opt_style.map_or("".to_string(), |style| style.suffix().to_string());
|
||||
let width = opt_width.unwrap_or(&content_length);
|
||||
|
||||
// cells are filled with non-breaking white space. Contrary to normal spaces non-breaking white
|
||||
// space will be styled (e.g. underlined)
|
||||
write!(
|
||||
f,
|
||||
"{prefix}{content:<width$}{suffix} ",
|
||||
"{prefix}{content:\u{a0}<width$}{suffix} ",
|
||||
prefix = style_prefix,
|
||||
content = content,
|
||||
width = width,
|
||||
|
@ -179,24 +283,118 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn get_column_width() {
|
||||
let mut t = Table::new(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
|
||||
fn get_column_width_without_wrapping() {
|
||||
let mut t = Table::new(get_columns());
|
||||
let row1 = Row::new(vec!["abc".to_string(), "defg".to_string()]);
|
||||
let row2 = Row::new(vec!["a".to_string(), "b".to_string(), "cdef".to_string()]);
|
||||
|
||||
t.add_row(row1);
|
||||
t.add_row(row2);
|
||||
|
||||
let column_width = t.get_column_width();
|
||||
let column_width = t.get_column_width(100);
|
||||
|
||||
assert_eq!(column_width[0], 3);
|
||||
assert_eq!(column_width[1], 4);
|
||||
assert_eq!(column_width[2], 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_column_width_with_wrapping() {
|
||||
let mut t = Table::new(vec![
|
||||
Column{ label: "a".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "b".to_string(), wrap: Wrap::Wrap },
|
||||
Column{ label: "c".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "d".to_string(), wrap: Wrap::Wrap },
|
||||
Column{ label: "e".to_string(), wrap: Wrap::Wrap },
|
||||
Column{ label: "e".to_string(), wrap: Wrap::Wrap },
|
||||
]);
|
||||
let row1 = Row::new(vec![
|
||||
"abcdefg".to_string(), // 7
|
||||
"abcdefghijkl".to_string(), // 12 -> muss gewrapt werden
|
||||
"abcde".to_string(), // 5
|
||||
"abc".to_string(), // 3 -> muss nicht gewrapt werden
|
||||
"abcdefghijklmno".to_string(), // 15 -> muss gewrapt werden
|
||||
"abcdefg".to_string() // 7 -> muss nicht gewrapt werden
|
||||
]);
|
||||
|
||||
t.add_row(row1);
|
||||
|
||||
let column_width = t.get_column_width(7 + 5 + 25);
|
||||
|
||||
assert_eq!(column_width[0], 7);
|
||||
assert_eq!(column_width[1], 8);
|
||||
assert_eq!(column_width[2], 5);
|
||||
assert_eq!(column_width[3], 3);
|
||||
assert_eq!(column_width[4], 7);
|
||||
assert_eq!(column_width[5], 7);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn get_column_width_with_wrapping_not_possible() {
|
||||
let mut t = Table::new(vec![
|
||||
Column{ label: "a".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "b".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "c".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "d".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "e".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "e".to_string(), wrap: Wrap::NoWrap },
|
||||
]);
|
||||
let row1 = Row::new(vec![
|
||||
"abcdefg".to_string(), // 7
|
||||
"abcdefghijkl".to_string(), // 12
|
||||
"abcde".to_string(), // 5
|
||||
"abc".to_string(), // 3
|
||||
"abcdefghijklmno".to_string(), // 15
|
||||
"abcdefg".to_string() // 7
|
||||
]);
|
||||
|
||||
t.add_row(row1);
|
||||
|
||||
let column_width = t.get_column_width(10);
|
||||
|
||||
assert_eq!(column_width[0], 7);
|
||||
assert_eq!(column_width[1], 12);
|
||||
assert_eq!(column_width[2], 5);
|
||||
assert_eq!(column_width[3], 3);
|
||||
assert_eq!(column_width[4], 15);
|
||||
assert_eq!(column_width[5], 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_column_width_with_wrapping_not_enough_wrappable_space() {
|
||||
let mut t = Table::new(vec![
|
||||
Column{ label: "a".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "b".to_string(), wrap: Wrap::Wrap },
|
||||
Column{ label: "c".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "d".to_string(), wrap: Wrap::Wrap },
|
||||
Column{ label: "e".to_string(), wrap: Wrap::Wrap },
|
||||
Column{ label: "e".to_string(), wrap: Wrap::Wrap },
|
||||
]);
|
||||
let row1 = Row::new(vec![
|
||||
"abcdefg".to_string(), // 7
|
||||
"abcdefghijkl".to_string(), // 12
|
||||
"abcde".to_string(), // 5
|
||||
"abc".to_string(), // 3
|
||||
"abcdefghijklmno".to_string(), // 15
|
||||
"abcdefg".to_string() // 7
|
||||
]);
|
||||
|
||||
t.add_row(row1);
|
||||
|
||||
let column_width = t.get_column_width(10);
|
||||
|
||||
assert_eq!(column_width[0], 7);
|
||||
assert_eq!(column_width[1], 12);
|
||||
assert_eq!(column_width[2], 5);
|
||||
assert_eq!(column_width[3], 3);
|
||||
assert_eq!(column_width[4], 15);
|
||||
assert_eq!(column_width[5], 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display() {
|
||||
let mut t = Table::new(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
|
||||
let mut t = Table::new(get_columns());
|
||||
let row1 = Row::new(vec!["abc".to_string(), "defg".to_string()]);
|
||||
let row2 = Row::new(vec!["a".to_string(), "b".to_string(), "cdef".to_string()]);
|
||||
|
||||
|
@ -205,7 +403,15 @@ mod tests {
|
|||
|
||||
assert_eq!(
|
||||
format!("{}", t),
|
||||
"\u{1b}[4ma \u{1b}[0m \u{1b}[4mb \u{1b}[0m \u{1b}[4mc \u{1b}[0m \nabc defg \na b cdef \n"
|
||||
"\u{1b}[4ma\u{a0}\u{a0}\u{1b}[0m \u{1b}[4mb\u{a0}\u{a0}\u{a0}\u{1b}[0m \u{1b}[4mc\u{a0}\u{a0}\u{a0}\u{1b}[0m \nabc defg \na\u{a0}\u{a0} b\u{a0}\u{a0}\u{a0} cdef \n"
|
||||
);
|
||||
}
|
||||
|
||||
fn get_columns() -> Vec<Column> {
|
||||
vec![
|
||||
Column{ label: "a".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "b".to_string(), wrap: Wrap::NoWrap },
|
||||
Column{ label: "c".to_string(), wrap: Wrap::NoWrap },
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue