diff --git a/Cargo.lock b/Cargo.lock index 539d7eb..3c4f3b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 8548136..10c0a9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,6 @@ chrono = "0.4" clap = "~2.33" thiserror = "1.0" anyhow = "1.0.42" -nu-ansi-term = "0.34.0" \ No newline at end of file +nu-ansi-term = "0.34.0" +term_size = "1.0.0-beta1" +textwrap = "0.14.2" \ No newline at end of file diff --git a/src/conf.rs b/src/conf.rs index 9f78458..f5f1d5a 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -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; diff --git a/src/view/format_util.rs b/src/view/format_util.rs index c44a272..ae8084e 100644 --- a/src/view/format_util.rs +++ b/src/view/format_util.rs @@ -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)); } diff --git a/src/view/list.rs b/src/view/list.rs index 0e69bb9..48dbd35 100644 --- a/src/view/list.rs +++ b/src/view/list.rs @@ -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(); diff --git a/src/view/report.rs b/src/view/report.rs index 4fc7768..560c15b 100644 --- a/src/view/report.rs +++ b/src/view/report.rs @@ -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:.(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:.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:.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:.(f: &mut fmt::Formatter<'_>, total_duration: Duration, line_width: usize) -> fmt::Result { + writeln!(f, "{prefix}{total:.(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 { +fn get_longest_line(project_map: &ProjectMap) -> Option { 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, o2: Option) -> Option { +fn get_longest_duration_string(report: &Report) -> Option { + 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, o2: Option) -> Option { 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, o2: Option) -> Option { #[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] diff --git a/src/view/table.rs b/src/view/table.rs index 754c2b2..a1026d3 100644 --- a/src/view/table.rs +++ b/src/view/table.rs @@ -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, @@ -15,7 +29,7 @@ pub struct Group { } pub struct Table { - header: Vec, + columns: Vec, rows: Vec, groups: Vec, } @@ -40,9 +54,9 @@ impl Group { } impl Table { - pub fn new(header: Vec) -> Table { + pub fn new(columns: Vec) -> Table { Table { - header, + columns, groups: Vec::new(), rows: Vec::new(), } @@ -64,8 +78,75 @@ impl Table { .collect() } - fn get_column_width(&self) -> Vec { - let mut column_width: Vec = 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 { + 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 = 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 { + let mut max_column_width: Vec = 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 + std::fmt::Display>( column_width: &[usize], style: Option