Merge branch 'dev/list-last-tasks-with-index'

This commit is contained in:
Nikolas Schmidt-Voigt 2021-09-23 17:41:19 +02:00
commit e1f06a0270
6 changed files with 227 additions and 114 deletions

View file

@ -24,7 +24,9 @@ Print help information:
bartib -h
```
### Start a new activity
### Tracking activities
#### Start a new activity
Start a new activity with a short description and an associated project:
@ -40,7 +42,7 @@ The `-t/--time` option specifies at which time of the current day the new activi
bartib start -p "The name of the associated project" -d "A description of the activity" -t 13:45
```
### Stop a running activity
#### Stop a running activity
Stops the currently running activity:
@ -50,17 +52,47 @@ bartib stop
The `-t/--time` option specifies at which time of the current day the activities stop.
### Continue the last activity
#### Continue a recent activity
Instead of typing a project and a description for each new activity, it is possible to continue a recent activity. The `last` subcommand prints a list of recently
used projects and descriptions. Per default it prints 10 lines. The `-n/--number` option may be used to print a longer list.
```
bartib continue [-p "Another project"] [-d "Another description"]
bartib last [-n 25]
```
This continues the last activity. If an activity is currently tracked, bartib stops and restarts this activity. The associated project and description may be overwritten by setting a `-p / --project` or `-d / --description` option.
In the list each activity will be asigned an index. This index can be used with the `continue` subcommand to restart the selected activitiy. The description
and or the project of this activity can be overwritten for the new activity with the help of the `-d/--description` respectively the `-p/--project` argument.
The `-t/--time` option may be used to specify at which time of the current day the activity should be restarted. If an activity is currently tracked, bartib
stops it at the time the new activity starts.
The `-t/--time` option specifies at which time of the current day the activity (re-)starts.
```
bartib continue [3] [-p "Another project"] [-d "Another description"] [-t 8:45]
```
### List activities
The default value for the index parameter is `0` which always points to the most recently tracked activity. Therefore `bartib continue` without any parameters
or options may be used to continue the most recently tracked activity.
### Reporting and listing activities
#### Create a report
This will create a report of how much time has been spent on which projects and activities:
```
bartib report
```
The `report` subcommand accepts most of the arguments that the `list` subcommand accepts:
```
bartib report --today
bartib report --yesterday
bartib report --from 2021-09-01 --to 2021-09-05
bartib report --date 2021-09-03
```
#### List activities
All activities:
@ -93,30 +125,10 @@ bartib list --today
bartib list --yesterday
```
### List all currently running activities
### Miscellaneous commands
```
bartib current
```
### Create a report
This will create a report of how much time has been spent on which projects and activities:
```
bartib report
```
The `report` subcommand accepts most of the arguments that the `list` subcommand accepts:
```
bartib report --today
bartib report --yesterday
bartib report --from 2021-09-01 --to 2021-09-05
bartib report --date 2021-09-03
```
### Edit activities
#### 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:
@ -130,13 +142,15 @@ This will open your log in the editor you have defined in your `EDITOR` environm
bartib edit -e vim
```
### Show last activity
#### List all currently running activities
```
bartib last
bartib current
```
### List all projects
#### List all projects
This command lists all projects for which an activity has ever been logged:

View file

@ -29,12 +29,12 @@ pub fn list(file_name: &str, filter: getter::ActivityFilter, do_group_activities
if do_group_activities {
list::list_activities_grouped_by_date(
&filtered_activities[first_element..filtered_activities.len()],
&filtered_activities[first_element..],
);
} else {
let with_start_dates = !filter.date.is_some();
list::list_activities(
&filtered_activities[first_element..filtered_activities.len()],
&filtered_activities[first_element..],
with_start_dates,
);
}
@ -61,16 +61,13 @@ pub fn list_projects(file_name: &str) -> Result<()> {
}
// return last finished activity
pub fn display_last_activity(file_name: &str) -> Result<()> {
pub fn list_last_activities(file_name: &str, number: usize) -> Result<()> {
let file_content = bartib_file::get_file_content(file_name)?;
let last_activity = getter::get_last_activity_by_end(&file_content);
let descriptions_and_projects : Vec<(&String, &String)> = getter::get_descriptions_and_projects(&file_content);
let first_element = descriptions_and_projects.len().saturating_sub(number);
if let Some(activity) = last_activity {
list::display_single_activity(&activity);
} else {
println!("No activity has been finished yet.")
}
list::list_descriptions_and_projects(&descriptions_and_projects[first_element..]);
Ok(())
}

View file

@ -67,24 +67,33 @@ pub fn continue_last_activity(
project_name: Option<&str>,
activity_description: Option<&str>,
time: Option<NaiveDateTime>,
number: usize
) -> Result<()> {
let mut file_content = bartib_file::get_file_content(file_name)?;
let optional_last_activity =
getter::get_last_activity_by_start(&file_content).or(getter::get_last_activity_by_end(&file_content));
let descriptions_and_projects : Vec<(&String, &String)> = getter::get_descriptions_and_projects(&file_content);
if let Some(last_activity) = optional_last_activity {
if descriptions_and_projects.is_empty() {
bail!("No activity has been started before.")
}
if number > descriptions_and_projects.len() {
bail!(format!("Less than {} distinct activities have been logged yet", number));
}
let i = descriptions_and_projects.len().saturating_sub(number).saturating_sub(1);
let optional_description_and_project = descriptions_and_projects.get(i);
if let Some((description, project)) = optional_description_and_project {
let new_activity = activity::Activity::start(
project_name.unwrap_or(&last_activity.project).to_string(),
activity_description
.unwrap_or(&last_activity.description)
.to_string(),
project_name.unwrap_or(project).to_string(),
activity_description.unwrap_or(description).to_string(),
time,
);
stop_all_running_activities(&mut file_content, time);
save_new_activity(file_name, &mut file_content, new_activity)
} else {
bail!("No activity has been started before.")
bail!(format!("Less than {} distinct activities have been logged yet", number));
}
}

View file

@ -1,7 +1,9 @@
use std::collections::HashSet;
use chrono::{naive, NaiveDate};
use crate::data::activity;
use crate::data::bartib_file;
use crate::data::activity::Activity;
pub struct ActivityFilter {
pub number_of_activities: Option<usize>,
@ -10,6 +12,38 @@ pub struct ActivityFilter {
pub date: Option<NaiveDate>,
}
pub fn get_descriptions_and_projects(file_content: &[bartib_file::Line]) -> Vec<(&String, &String)> {
let mut activities : Vec<&activity::Activity> = get_activities(file_content).collect();
get_descriptions_and_projects_from_activities(&mut activities)
}
fn get_descriptions_and_projects_from_activities<'a>(activities: &mut [&'a Activity]) -> Vec<(&'a String, &'a String)> {
activities.sort_by_key(|activity| activity.start);
/* each activity should be placed in the list in the descending order of when it had been
started last. To achieve this we reverse the order of the activities before we extract the
set of descriptions and activities. Afterwards we also reverse the list of descriptions and
activities.
e.g. if tasks have been started in this order: a, b, c, a, c the list of descriptions and
activities should have this order: b, a, c
*/
activities.reverse();
let mut known_descriptions_and_projects: HashSet<(&String, &String)> = HashSet::new();
let mut descriptions_and_projects: Vec<(&String, &String)> = Vec::new();
for description_and_project in activities.iter().map(|a| (&a.description, &a.project)) {
if !known_descriptions_and_projects.contains(&description_and_project) {
known_descriptions_and_projects.insert(description_and_project);
descriptions_and_projects.push(description_and_project);
}
}
descriptions_and_projects.reverse();
descriptions_and_projects
}
pub fn get_running_activities(file_content: &[bartib_file::Line]) -> Vec<&activity::Activity> {
get_activities(file_content)
.filter(|activity| !activity.is_stopped())
@ -43,7 +77,7 @@ pub fn filter_activities<'a>(
})
}
pub fn get_last_activity_by_end(file_content: &Vec<bartib_file::Line>) -> Option<&activity::Activity> {
pub fn get_last_activity_by_end(file_content: &[bartib_file::Line]) -> Option<&activity::Activity> {
get_activities(&file_content)
.filter(|activity| activity.is_stopped())
.max_by_key(|activity| activity.end.unwrap_or(naive::MIN_DATE.and_hms(0, 0, 0)))
@ -52,3 +86,36 @@ pub fn get_last_activity_by_end(file_content: &Vec<bartib_file::Line>) -> Option
pub fn get_last_activity_by_start(file_content: &Vec<bartib_file::Line>) -> Option<&activity::Activity> {
get_activities(&file_content).max_by_key(|activity| activity.start)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn get_descriptions_and_projects_test_simple() {
let a1 = activity::Activity::start("p1".to_string(), "d1".to_string(), None);
let a2 = activity::Activity::start("p1".to_string(), "d1".to_string(), None);
let a3 = activity::Activity::start("p2".to_string(), "d1".to_string(), None);
let mut activities = vec![&a1, &a2, &a3];
let descriptions_and_projects = get_descriptions_and_projects_from_activities(&mut activities);
assert_eq!(descriptions_and_projects.len(), 2);
assert_eq!(*descriptions_and_projects.get(0).unwrap(), (&"d1".to_string(), &"p1".to_string()));
assert_eq!(*descriptions_and_projects.get(1).unwrap(), (&"d1".to_string(), &"p2".to_string()));
}
#[test]
fn get_descriptions_and_projects_test_restarted_activitiy() {
let a1 = activity::Activity::start("p1".to_string(), "d1".to_string(), None);
let a2 = activity::Activity::start("p2".to_string(), "d1".to_string(), None);
let a3 = activity::Activity::start("p1".to_string(), "d1".to_string(), None);
let mut activities = vec![&a1, &a2, &a3];
let descriptions_and_projects = get_descriptions_and_projects_from_activities(&mut activities);
assert_eq!(descriptions_and_projects.len(), 2);
assert_eq!(*descriptions_and_projects.get(0).unwrap(), (&"d1".to_string(), &"p2".to_string()));
assert_eq!(*descriptions_and_projects.get(1).unwrap(), (&"d1".to_string(), &"p1".to_string()));
}
}

View file

@ -53,6 +53,18 @@ fn main() -> Result<()> {
.conflicts_with_all(&["from_date", "to_date", "date", "today"])
.takes_value(false);
let arg_description = Arg::with_name("description")
.short("d")
.value_name("DESCRIPTION")
.help("the description of the new activity")
.takes_value(true);
let arg_project = Arg::with_name("project")
.short("p")
.value_name("PROJECT")
.help("the project to which the new activity belongs")
.takes_value(true);
let matches = App::new("bartib")
.version("0.1")
.author("Nikolas Schmidt-Voigt <nikolas.schmidt-voigt@posteo.de>")
@ -70,40 +82,22 @@ fn main() -> Result<()> {
.subcommand(
SubCommand::with_name("start")
.about("starts a new activity")
.arg(
Arg::with_name("project")
.short("p")
.value_name("PROJECT")
.help("the project to which the new activity belong")
.required(true)
.takes_value(true),
)
.arg(
Arg::with_name("description")
.short("d")
.value_name("DESCRIPTION")
.help("a description of the new activity")
.required(true)
.takes_value(true),
)
.arg(&arg_description)
.arg(&arg_project)
.arg(&arg_time),
)
.subcommand(
SubCommand::with_name("continue")
.about("continues the last activity")
.about("continues a previous activity")
.arg(&arg_description)
.arg(&arg_project)
.arg(
Arg::with_name("project")
.short("p")
.value_name("PROJECT")
.help("the project to which the new activity belong")
.takes_value(true),
)
.arg(
Arg::with_name("description")
.short("d")
.value_name("DESCRIPTION")
.help("a description of the new activity")
.takes_value(true),
Arg::with_name("number")
.value_name("NUMBER")
.help("the number of the activity to continue (see subcommand `last`)")
.required(false)
.takes_value(true)
.default_value("0")
)
.arg(&arg_time),
)
@ -147,7 +141,20 @@ fn main() -> Result<()> {
.arg(&arg_today)
.arg(&arg_yesterday),
)
.subcommand(SubCommand::with_name("last").about("displays last finished acitivity"))
.subcommand(
SubCommand::with_name("last")
.about("displays the descriptions and projects of recent activities")
.arg(
Arg::with_name("number")
.short("n")
.long("number")
.value_name("NUMBER")
.help("maximum number of lines to display")
.required(false)
.takes_value(true)
.default_value("10")
)
)
.subcommand(SubCommand::with_name("projects").about("list all projects"))
.subcommand(
SubCommand::with_name("edit")
@ -184,8 +191,12 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> {
let activity_description = sub_m.value_of("description");
let time = get_time_argument_or_ignore(sub_m.value_of("time"), "-t/--time")
.map(|t| Local::today().naive_local().and_time(t));
let number = get_number_argument_or_ignore(
sub_m.value_of("number"),
"-n/--number",
).unwrap_or(0);
bartib::controller::manipulation::continue_last_activity(file_name, project_name, activity_description, time)
bartib::controller::manipulation::continue_last_activity(file_name, project_name, activity_description, time, number)
}
("stop", Some(sub_m)) => {
let time = get_time_argument_or_ignore(sub_m.value_of("time"), "-t/--time")
@ -195,25 +206,6 @@ 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"),
@ -232,10 +224,35 @@ 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)
}
("report", 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));
}
bartib::controller::report::show_report(file_name, filter)
}
("projects", Some(_)) => bartib::controller::list::list_projects(file_name),
("last", Some(_)) => bartib::controller::list::display_last_activity(file_name),
("projects", Some(_)) => bartib::controller::list::list_projects(file_name),
("last", Some(sub_m)) => {
let number = get_number_argument_or_ignore(
sub_m.value_of("number"),
"-n/--number",
).unwrap_or(10);
bartib::controller::list::list_last_activities(file_name, number)
}
("edit", Some(sub_m)) => {
let optional_editor_command = sub_m.value_of("editor");
bartib::controller::manipulation::start_editor(file_name, optional_editor_command)

View file

@ -91,20 +91,29 @@ pub fn list_running_activities(running_activities: &[&activity::Activity]) {
}
}
// displays a single activity
pub fn display_single_activity(activity: &activity::Activity) {
println!("Begin: {}", activity.start.format(conf::FORMAT_DATETIME));
// display a list of projects and descriptions with index number
pub fn list_descriptions_and_projects(descriptions_and_projects : &[(&String, &String)]) {
if descriptions_and_projects.is_empty() {
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()
]);
if let Some(end) = activity.end {
println!("End: {}", end.format(conf::FORMAT_DATETIME));
println!(
"Duration: {}",
format_util::format_duration(&activity.get_duration())
);
let mut i = descriptions_and_projects.len();
for (description, project) in descriptions_and_projects {
i = i.saturating_sub(1);
descriptions_and_projects_table.add_row(
table::Row::new(vec![format!("[{}]", i), description.to_string(), project.to_string()])
);
}
println!("\n{}", descriptions_and_projects_table);
}
println!("Project: {}", activity.project);
println!("Description: {}", activity.description);
}
// create a row for a activity