diff --git a/README.md b/README.md index 561fecd..bd2f46f 100644 --- a/README.md +++ b/README.md @@ -60,12 +60,6 @@ This continues the last activity. If an activity is currently tracked, bartib st The `-t/--time` option specifies at which time of the current day the activity (re-)starts. -### List all currently running activities - -``` -bartib current -``` - ### List activities All activities: @@ -99,6 +93,12 @@ bartib list --today bartib list --yesterday ``` +### List all currently running activities + +``` +bartib current +``` + ### Edit activities To change tracked activities, just open the file with your activities log in any text editor. To facilitate this, bartib offers the `edit` subcommand: diff --git a/src/controller/list.rs b/src/controller/list.rs index eb3c1ae..a60affc 100644 --- a/src/controller/list.rs +++ b/src/controller/list.rs @@ -25,8 +25,7 @@ pub fn list(file_name: &str, filter: getter::ActivityFilter, do_group_activities filtered_activities.sort_by_key(|activity| activity.start); - let first_element = - get_index_of_first_element(filtered_activities.len(), filter.number_of_activities); + let first_element =filtered_activities.len().saturating_sub(filter.number_of_activities.unwrap_or(filtered_activities.len())); if do_group_activities { list::list_activities_grouped_by_date( @@ -74,12 +73,4 @@ pub fn display_last_activity(file_name: &str) -> Result<()> { } Ok(()) -} - -fn get_index_of_first_element(length: usize, sub: Option) -> usize { - if let Some(s) = sub { - length.saturating_sub(s) - } else { - 0 - } } \ No newline at end of file diff --git a/src/controller/mod.rs b/src/controller/mod.rs index 8b10b58..14204ab 100644 --- a/src/controller/mod.rs +++ b/src/controller/mod.rs @@ -1,2 +1,3 @@ pub mod list; -pub mod manipulation; \ No newline at end of file +pub mod manipulation; +pub mod report; \ No newline at end of file diff --git a/src/controller/report.rs b/src/controller/report.rs new file mode 100644 index 0000000..9b9840c --- /dev/null +++ b/src/controller/report.rs @@ -0,0 +1,22 @@ +use anyhow::Result; + +use crate::data::activity; +use crate::data::bartib_file; +use crate::data::getter; +use crate::view::report; + +pub fn show_report(file_name: &str, filter: getter::ActivityFilter) -> Result<()> { + let file_content = bartib_file::get_file_content(file_name)?; + let activities = getter::get_activities(&file_content); + let mut filtered_activities: Vec<&activity::Activity> = + getter::filter_activities(activities, &filter).collect(); + + filtered_activities.sort_by_key(|activity| activity.start); + + + let first_element = filtered_activities.len().saturating_sub(filter.number_of_activities.unwrap_or(filtered_activities.len())); + + report::show_activities(&filtered_activities[first_element..filtered_activities.len()]); + + Ok(()) +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 46933ab..a956de3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,41 @@ fn main() -> Result<()> { .help("the time for changing the activity status (HH:MM)") .takes_value(true); + let arg_from_date = Arg::with_name("from_date") + .long("from") + .value_name("FROM_DATE") + .help("begin of date range (inclusive)") + .takes_value(true); + + let arg_to_date = Arg::with_name("to_date") + .long("to") + .value_name("TO_DATE") + .help("end of date range (inclusive)") + .takes_value(true); + + let arg_date = Arg::with_name("date") + .short("d") + .long("date") + .value_name("DATE") + .help("show activities of a certain date only") + .required(false) + .conflicts_with_all(&["from_date", "to_date"]) + .takes_value(true); + + let arg_today = Arg::with_name("today") + .long("today") + .help("show activities of the current day") + .required(false) + .conflicts_with_all(&["from_date", "to_date", "date", "yesterday"]) + .takes_value(false); + + let arg_yesterday = Arg::with_name("yesterday") + .long("yesterday") + .help("show yesterdays activities") + .required(false) + .conflicts_with_all(&["from_date", "to_date", "date", "today"]) + .takes_value(false); + let matches = App::new("bartib") .version("0.1") .author("Nikolas Schmidt-Voigt ") @@ -83,6 +118,16 @@ fn main() -> Result<()> { .subcommand( SubCommand::with_name("list") .about("list recent activities") + .arg(&arg_from_date) + .arg(&arg_to_date) + .arg(&arg_date) + .arg(&arg_today) + .arg(&arg_yesterday) + .arg( + Arg::with_name("no_grouping") + .long("no_grouping") + .help("do not group activities by date in list"), + ) .arg( Arg::with_name("number") .short("n") @@ -91,53 +136,17 @@ fn main() -> Result<()> { .help("maximum number of activities to display") .required(false) .takes_value(true), - ) - .arg( - Arg::with_name("from_date") - .long("from") - .value_name("FROM_DATE") - .help("begin of date range (inclusive)") - .takes_value(true), - ) - .arg( - Arg::with_name("to_date") - .long("to") - .value_name("TO_DATE") - .help("end of date range (inclusive)") - .takes_value(true), - ) - .arg( - Arg::with_name("date") - .short("d") - .long("date") - .value_name("DATE") - .help("show activities of a certain date only") - .required(false) - .conflicts_with_all(&["from_date", "to_date"]) - .takes_value(true), - ) - .arg( - Arg::with_name("today") - .long("today") - .help("show activities of the current day") - .required(false) - .conflicts_with_all(&["from_date", "to_date", "date", "yesterday"]) - .takes_value(false), - ) - .arg( - Arg::with_name("yesterday") - .long("yesterday") - .help("show yesterdays activities") - .required(false) - .conflicts_with_all(&["from_date", "to_date", "date", "today"]) - .takes_value(false), - ) - .arg( - Arg::with_name("no_grouping") - .long("no_grouping") - .help("do not group activities by date in list"), ), ) + .subcommand( + SubCommand::with_name("report") + .about("reports duration of tracked activities") + .arg(&arg_from_date) + .arg(&arg_to_date) + .arg(&arg_date) + .arg(&arg_today) + .arg(&arg_yesterday), + ) .subcommand(SubCommand::with_name("last").about("displays last finished acitivity")) .subcommand(SubCommand::with_name("projects").about("list all projects")) .subcommand( @@ -186,6 +195,25 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> { } ("current", Some(_)) => bartib::controller::list::list_running(file_name), ("list", Some(sub_m)) => { + let mut filter = bartib::data::getter::ActivityFilter { + number_of_activities: None, + from_date: get_date_argument_or_ignore(sub_m.value_of("from_date"), "--from"), + to_date: get_date_argument_or_ignore(sub_m.value_of("to_date"), "--to"), + date: get_date_argument_or_ignore(sub_m.value_of("date"), "-d/--date"), + }; + + if sub_m.is_present("today") { + filter.date = Some(Local::now().naive_local().date()); + } + + if sub_m.is_present("yesterday") { + filter.date = Some(Local::now().naive_local().date() - Duration::days(1)); + } + + let do_group_activities = !sub_m.is_present("no_grouping") && !filter.date.is_some(); + bartib::controller::list::list(file_name, filter, do_group_activities) + } + ("report", Some(sub_m)) => { let mut filter = bartib::data::getter::ActivityFilter { number_of_activities: get_number_argument_or_ignore( sub_m.value_of("number"), @@ -204,10 +232,9 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> { filter.date = Some(Local::now().naive_local().date() - Duration::days(1)); } - let do_group_activities = !sub_m.is_present("no_grouping") && !filter.date.is_some(); - bartib::controller::list::list(file_name, filter, do_group_activities) + bartib::controller::report::show_report(file_name, filter) } - ("projects", Some(_)) => bartib::controller::list::list_projects(file_name), + ("projects", Some(_)) => bartib::controller::list::list_projects(file_name), ("last", Some(_)) => bartib::controller::list::display_last_activity(file_name), ("edit", Some(sub_m)) => { let optional_editor_command = sub_m.value_of("editor"); diff --git a/src/view/report.rs b/src/view/report.rs index 4c48019..506ac59 100644 --- a/src/view/report.rs +++ b/src/view/report.rs @@ -1,19 +1,19 @@ use std::collections::BTreeMap; use std::fmt; +use std::ops::Add; +use chrono::Duration; +use nu_ansi_term::Style; use crate::data::activity; +use crate::view::format_util; struct Report<'a> { - activities : Vec<&'a activity::Activity> + activities : &'a[&'a activity::Activity] } impl<'a> Report<'a> { - fn new() -> Report<'a> { - Report { activities : Vec::new() } - } - - fn add(&mut self, a : &'a activity::Activity) { - self.activities.push(&a); + fn new(activities : &'a[&'a activity::Activity]) -> Report<'a> { + Report { activities } } fn get_project_map(&self) -> BTreeMap<&str, Vec<&'a activity::Activity>> { @@ -33,20 +33,62 @@ impl<'a> fmt::Display for Report<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let project_map = self.get_project_map(); - let longest_line = get_longest_line(&project_map); + let longest_line = get_longest_line(&project_map).unwrap_or(0); + let mut total_duration = Duration::seconds(0); for (project, activities) in project_map.iter() { - write!(f, "{}", project)?; + let project_duration = sum_duration(&activities); + total_duration = total_duration.add(project_duration); + + writeln!(f, "{prefix}{project:.(activities : &'a[&'a activity::Activity]) { + let report = Report::new(activities); + println!("\n{}", report); +} + +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 { 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(); @@ -68,6 +110,26 @@ fn get_max_option(o1 : Option, o2: Option) -> Option { #[cfg(test)] mod tests { use super::*; + use chrono::NaiveDateTime; + + #[test] + fn sum_duration_test() { + 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())); + a1.end = Some(NaiveDateTime::parse_from_str("2021-09-01 15:20:00", "%Y-%m-%d %H:%M:%S").unwrap()); // 20 * 60 = 1,200 seconds + let mut a2 = activity::Activity::start("p1".to_string(), "d2".to_string(), Some(NaiveDateTime::parse_from_str("2021-09-01 15:21:00", "%Y-%m-%d %H:%M:%S").unwrap())); + a2.end = Some(NaiveDateTime::parse_from_str("2021-09-01 16:21:00", "%Y-%m-%d %H:%M:%S").unwrap()); // 60 * 60 = 3,600 seconds + let mut a3 = activity::Activity::start("p2".to_string(), "d1".to_string(), Some(NaiveDateTime::parse_from_str("2021-09-01 16:21:00", "%Y-%m-%d %H:%M:%S").unwrap())); + a3.end = Some(NaiveDateTime::parse_from_str("2021-09-02 16:21:00", "%Y-%m-%d %H:%M:%S").unwrap()); // 24 * 60 * 60 = 86,400 seconds + + activities.push(&a1); + activities.push(&a2); + activities.push(&a3); + + assert_eq!(sum_duration(&activities).num_seconds(), 91200); + } #[test] fn get_project_map() { @@ -75,10 +137,11 @@ 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 r = Report::new(); - r.add(&a1); - r.add(&a2); - r.add(&a3); + let mut activities : Vec<&activity::Activity> = Vec::new(); + activities.push(&a1); + activities.push(&a2); + activities.push(&a3); + let r = Report::new(&activities); let m = r.get_project_map(); @@ -87,7 +150,8 @@ mod tests { #[test] fn get_longest_line_test() { - let mut r = Report::new(); + let mut activities : Vec<&activity::Activity> = Vec::new(); + let mut r = Report::new(&activities); let project_map1 = r.get_project_map(); // keine Einträge -> keine Längste Zeile @@ -99,11 +163,12 @@ mod tests { let a4 = activity::Activity::start("p2".to_string(), "d1".to_string(), None); let a5 = activity::Activity::start("p2".to_string(), "d1".to_string(), None); - r.add(&a1); - r.add(&a2); - r.add(&a3); - r.add(&a4); - r.add(&a5); + activities.push(&a1); + activities.push(&a2); + activities.push(&a3); + activities.push(&a4); + activities.push(&a5); + r = Report::new(&activities); // längste Zeile ist Description + 4 let project_map2 = r.get_project_map(); @@ -111,7 +176,8 @@ mod tests { // längste Zeile ist Projektname mit 8 Zeichen let a6 = activity::Activity::start("p1234567".to_string(), "d1".to_string(), None); - r.add(&a6); + activities.push(&a6); + r = Report::new(&activities); let project_map3 = r.get_project_map(); assert_eq!(get_longest_line(&project_map3).unwrap(), 8);