Merge branch 'dev/wrapCells'

This commit is contained in:
Nikolas Schmidt-Voigt 2021-10-17 22:10:29 +02:00
commit 5705b4dce5
7 changed files with 493 additions and 135 deletions

105
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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;

View file

@ -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));
}

View file

@ -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();

View file

@ -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]

View file

@ -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 },
]
}
}