issue-18: fix some typos

This commit is contained in:
meinbaumm 2023-03-29 23:18:25 +07:00
parent 7a4c258a08
commit 8bd6d97718
15 changed files with 464 additions and 235 deletions

View file

@ -1,6 +1,6 @@
# Bartib
![Illustration of the White Rabbit from Alice in Wonderland](misc/white-rabbit.png "Oh dear! Oh dear! I shall be too late")
![Illustration of the White Rabbit from Alice in Wonderland](misc/white-rabbit.png "Oh dear! Oh dear! I shall be too late")
Bartib is an easy to use time tracking tool for the command line. It saves a log of all tracked activities as a plaintext file and allows you to create flexible reports.
@ -12,19 +12,27 @@ Bartib is an easy to use time tracking tool for the command line. It saves a log
## Contents
1. [Tutorial](#tutorial)
1. [How To ...](#how-to-)
1. [How to install Bartib](#how-to-install-bartib)
1. [How to build Bartib](#how-to-build-bartib)
1. [How to define in which file to save the log of your activities](#how-to-define-in-which-file-to-save-the-log-of-your-activities)
1. [How to edit or delete tracked activities](#how-to-edit-or-delete-tracked-activities)
1. [How to activate auto completion](#how-to-activate-auto-completion)
1. [Command overview](#command-overview)
1. [The essentials](#the-essentials)
1. [Getting Help](#getting-help)
1. [Tracking activities](#tracking-activities)
1. [Reporting and listing activities](#reporting-and-listing-activities)
1. [Doing other stuff](#doing-other-stuff)
- [Bartib](#bartib)
- [Contents](#contents)
- [Tutorial](#tutorial)
- [How To ...](#how-to-)
- [How to install Bartib](#how-to-install-bartib)
- [Download an executable](#download-an-executable)
- [With Cargo](#with-cargo)
- [From the AUR (Arch Linux)](#from-the-aur-arch-linux)
- [Via homebrew](#via-homebrew)
- [Via apk (Alpine Linux)](#via-apk-alpine-linux)
- [How to build Bartib](#how-to-build-bartib)
- [How to define in which file to save the log of your activities](#how-to-define-in-which-file-to-save-the-log-of-your-activities)
- [How to edit or delete tracked activities](#how-to-edit-or-delete-tracked-activities)
- [How to activate auto completion](#how-to-activate-auto-completion)
- [Command overview](#command-overview)
- [The essentials](#the-essentials)
- [Getting Help](#getting-help)
- [Tracking activities](#tracking-activities)
- [Reporting and listing activities](#reporting-and-listing-activities)
- [Edit activities](#edit-activities)
- [Doing other stuff](#doing-other-stuff)
## Tutorial
@ -68,7 +76,7 @@ Started activity: "More Urgent Task Y" (Just Another Project B) at 2021-10-29 10
See how Bartib just stops the running activity when another one starts? No need to stop it manually.
It is a productive morning. After _More Urgent Task Y_ Alice workes on other projects and other tasks, but now it is time for lunch and Alice lets Bartib list all the activities she has tracked today until now:
It is a productive morning. After _More Urgent Task Y_ Alice works on other projects and other tasks, but now it is time for lunch and Alice lets Bartib list all the activities she has tracked today until now:
```console
alice@work: ~ $ bartib list --today
@ -136,7 +144,7 @@ Do you want to be as happy as Alice? Use Bartib!
#### Download an executable
Simply download a suitable executable from https://github.com/nikolassv/bartib/releases and copy it in some directory that is listed in your `PATH` (e.g. ~/bin).
Simply download a suitable executable from <https://github.com/nikolassv/bartib/releases> and copy it in some directory that is listed in your `PATH` (e.g. ~/bin).
#### With Cargo
@ -192,7 +200,7 @@ If the specified log file does not exist yet Bartib creates it.
### How to edit or delete tracked activities
Just open your activitiy log in your favorite text editor to edit or delete former activities. You may even add new activities manually in this file. The format is self explanatory.
Just open your activity log in your favorite text editor to edit or delete former activities. You may even add new activities manually in this file. The format is self explanatory.
Bartib even offers the `bartib edit` command which opens the log in the editor defined by your `EDITOR` environment variable. If you are unsure whether your edits are readable by bartib, use the `bartib check` command. It will inform you about any parsing errors.
@ -205,6 +213,7 @@ Bartib offers a simple auto completion for project names. This saves you from ty
All these commands require that you have set the `BARTIB_FILE` environment variable to the file path of your activity log. Otherwise they require an additional `-f/--file` parameter between `bartib` and the subcommand (see above: [How to define in which file to save the log of your activities](#how-to-define-in-which-file-to-save-the-log-of-your-activities)).
### The essentials
```bash
bartib -h # get help
bartib start -p "name of the project" -d "description of the activity" # start a new activity
@ -214,12 +223,14 @@ bartib report --today # create a report for today
```
### Getting Help
```bash
bartib -h # Print a concise help
bartib start -h # Print a help for any subcommand
```
### Tracking activities
### Tracking activities
```bash
bartib start -p "The name of the associated project" -d "A description of the activity" # Start a new activity with a short description and an associated project
bartib start -p "The name of the associated project" -d "A description of the activity" -t 13:45 # Start a new activity at a given time
@ -230,7 +241,7 @@ bartib stop -t 14:00 # Stop the currently running activity at a given time
bartib last # Print a list of the ten most recently used projects and descriptions
bartib last -n 25 # Prints a list of recently used projects and descriptions with more entries
# All numbers used with the following commands refer to the indizees in the list created with `bartib last`
# All numbers used with the following commands refer to the indexes in the list created with `bartib last`
bartib continue 5 # Start an activity with a recently used project and description
bartib continue # Continue the latest activity
bartib continue 3 -d "Another description" # Continue activity number 3 but overwrite the description
@ -254,7 +265,7 @@ bartib report --project "The most exciting project" # create a report for a g
bartib list # list all activities grouped by day
bartib list --no_grouping # list all activities but do not group them by day
bartib list --today # list todays' activites
bartib list --today # list todays' activities
bartib list --yesterday # list yesterdays' activities
bartib list --current_week # list activities of the current week (since monday)
bartib list --last_week # list activities of the last week

View file

@ -1,5 +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 = usize::MAX;
pub static REPORT_INDENTATION : usize = 4;
pub static DEFAULT_WIDTH: usize = usize::MAX;
pub static REPORT_INDENTATION: usize = 4;

View file

@ -8,7 +8,7 @@ use crate::data::bartib_file;
use crate::data::getter;
use crate::view::list;
// lists all currently runninng activities.
// lists all currently running activities.
pub fn list_running(file_name: &str) -> Result<()> {
let file_content = bartib_file::get_file_content(file_name)?;
let running_activities = getter::get_running_activities(&file_content);
@ -52,19 +52,17 @@ pub fn list(
// checks the file content for sanity
pub fn sanity_check(file_name: &str) -> Result<()> {
let file_content = bartib_file::get_file_content(file_name)?;
let mut lines_with_activities : Vec<(Option<usize>, Activity)> = file_content
let mut lines_with_activities: Vec<(Option<usize>, Activity)> = file_content
.into_iter()
.filter_map(|line| {
match line.activity {
Ok(a) => Some((line.line_number, a)),
Err(_) => None
}
.filter_map(|line| match line.activity {
Ok(a) => Some((line.line_number, a)),
Err(_) => None,
})
.collect();
lines_with_activities.sort_unstable_by_key(|(_, activity)| activity.start);
let mut has_finding : bool = false;
let mut last_end : Option<NaiveDateTime> = None;
let mut has_finding: bool = false;
let mut last_end: Option<NaiveDateTime> = None;
for (line_number, activity) in lines_with_activities {
has_finding = !check_sanity(last_end, &activity, line_number) || has_finding;
@ -87,7 +85,11 @@ pub fn sanity_check(file_name: &str) -> Result<()> {
Ok(())
}
fn check_sanity(last_end: Option<NaiveDateTime>, activity: &Activity, line_number: Option<usize>) -> bool {
fn check_sanity(
last_end: Option<NaiveDateTime>,
activity: &Activity,
line_number: Option<usize>,
) -> bool {
let mut sane = true;
if activity.get_duration().num_milliseconds() < 0 {
println!("Activity has negative duration");
@ -96,7 +98,7 @@ fn check_sanity(last_end: Option<NaiveDateTime>, activity: &Activity, line_numbe
if let Some(e) = last_end {
if e > activity.start {
println!("Activity startet before another activity ended");
println!("Activity started before another activity ended");
sane = false;
}
}
@ -109,17 +111,19 @@ fn check_sanity(last_end: Option<NaiveDateTime>, activity: &Activity, line_numbe
}
fn print_activity_with_line(activity: &Activity, line_number: usize) {
println!("{} (Started: {}, Ended: {}, Line: {})\n",
activity.description,
activity.start.format(conf::FORMAT_DATETIME),
activity.end
.map(|end| end.format(conf::FORMAT_DATETIME).to_string())
.unwrap_or_else(|| String::from("--")),
line_number
println!(
"{} (Started: {}, Ended: {}, Line: {})\n",
activity.description,
activity.start.format(conf::FORMAT_DATETIME),
activity
.end
.map(|end| end.format(conf::FORMAT_DATETIME).to_string())
.unwrap_or_else(|| String::from("--")),
line_number
)
}
// prints all errors that occured when reading the bartib file
// prints all errors that occurred when reading the bartib file
pub fn check(file_name: &str) -> Result<()> {
let file_content = bartib_file::get_file_content(file_name)?;

View file

@ -1,3 +1,3 @@
pub mod list;
pub mod manipulation;
pub mod report;
pub mod report;

View file

@ -13,10 +13,13 @@ pub fn show_report(file_name: &str, filter: getter::ActivityFilter) -> Result<()
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()));
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(())
}
}

View file

@ -19,7 +19,7 @@ pub enum ActivityError {
#[error("could not parse date or time of activity")]
DateTimeParseError,
#[error("could not parse activity")]
GeneralParseError
GeneralParseError,
}
impl Activity {
@ -83,7 +83,7 @@ impl FromStr for Activity {
type Err = ActivityError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<String> = split_with_escaped_delimeter(s).collect();
let parts: Vec<String> = split_with_escaped_delimiter(s).collect();
if parts.len() < 2 {
return Err(ActivityError::GeneralParseError);
@ -160,7 +160,7 @@ impl Iterator for StringSplitter<'_> {
}
}
fn split_with_escaped_delimeter(s: &str) -> StringSplitter {
fn split_with_escaped_delimiter(s: &str) -> StringSplitter {
StringSplitter { chars: s.chars() }
}

View file

@ -16,7 +16,7 @@ pub enum LineStatus {
#[derive(Debug)]
pub struct Line {
// the plaintext of the line as it has been read from the file
// we save this to be able write untouched lines back to file without chaning them
// we save this to be able write untouched lines back to file without changing them
pub plaintext: Option<String>,
// the line number
pub line_number: Option<usize>,

View file

@ -141,7 +141,7 @@ mod tests {
}
#[test]
fn get_descriptions_and_projects_test_restarted_activitiy() {
fn get_descriptions_and_projects_test_restarted_activity() {
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);

View file

@ -1,3 +1,3 @@
pub mod activity;
pub mod bartib_file;
pub mod getter;
pub mod getter;

View file

@ -2,4 +2,4 @@ pub mod conf;
pub mod controller;
pub mod data;
mod view;
mod view;

View file

@ -2,9 +2,9 @@ use anyhow::{bail, Context, Result};
use chrono::{Datelike, Duration, Local, NaiveDate, NaiveTime};
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use bartib::data::getter::ActivityFilter;
#[cfg(windows)]
use nu_ansi_term::enable_ansi_support;
use bartib::data::getter::ActivityFilter;
fn main() -> Result<()> {
#[cfg(windows)]
@ -65,7 +65,14 @@ fn main() -> Result<()> {
.long("last_week")
.help("show activities of the last week")
.required(false)
.conflicts_with_all(&["from_date", "to_date", "date", "today", "yesterday", "current_week"])
.conflicts_with_all(&[
"from_date",
"to_date",
"date",
"today",
"yesterday",
"current_week",
])
.takes_value(false);
let arg_description = Arg::with_name("description")
@ -99,16 +106,8 @@ fn main() -> Result<()> {
.subcommand(
SubCommand::with_name("start")
.about("starts a new activity")
.arg(
arg_project
.clone()
.required(true)
)
.arg(
arg_description
.clone()
.required(true)
)
.arg(arg_project.clone().required(true))
.arg(arg_description.clone().required(true))
.arg(&arg_time),
)
.subcommand(
@ -122,7 +121,7 @@ fn main() -> Result<()> {
.help("the number of the activity to continue (see subcommand `last`)")
.required(false)
.takes_value(true)
.default_value("0")
.default_value("0"),
)
.arg(&arg_time),
)
@ -131,7 +130,7 @@ fn main() -> Result<()> {
.about("changes the current activity")
.arg(&arg_description)
.arg(&arg_project)
.arg(&arg_time)
.arg(&arg_time),
)
.subcommand(
SubCommand::with_name("stop")
@ -139,12 +138,10 @@ fn main() -> Result<()> {
.arg(&arg_time),
)
.subcommand(
SubCommand::with_name("cancel")
.about("cancels all currently running activities")
SubCommand::with_name("cancel").about("cancels all currently running activities"),
)
.subcommand(
SubCommand::with_name("current")
.about("lists all currently running activities")
SubCommand::with_name("current").about("lists all currently running activities"),
)
.subcommand(
SubCommand::with_name("list")
@ -163,7 +160,7 @@ fn main() -> Result<()> {
.value_name("PROJECT")
.help("do list activities for this project only")
.takes_value(true)
.required(false)
.required(false),
)
.arg(
Arg::with_name("no_grouping")
@ -197,9 +194,9 @@ fn main() -> Result<()> {
.value_name("PROJECT")
.help("do report activities for this project only")
.takes_value(true)
.required(false)
)
)
.required(false),
),
)
.subcommand(
SubCommand::with_name("last")
.about("displays the descriptions and projects of recent activities")
@ -211,8 +208,8 @@ fn main() -> Result<()> {
.help("maximum number of lines to display")
.required(false)
.takes_value(true)
.default_value("10")
)
.default_value("10"),
),
)
.subcommand(
SubCommand::with_name("projects")
@ -223,8 +220,8 @@ fn main() -> Result<()> {
.long("current")
.help("prints currently running projects only")
.takes_value(false)
.required(false)
)
.required(false),
),
)
.subcommand(
SubCommand::with_name("edit")
@ -233,7 +230,7 @@ fn main() -> Result<()> {
Arg::with_name("editor")
.short("e")
.value_name("editor")
.help("the command to start your prefered text editor")
.help("the command to start your preferred text editor")
.env("EDITOR")
.takes_value(true),
),
@ -256,7 +253,12 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> {
let time = get_time_argument_or_ignore(sub_m.value_of("time"), "-t/--time")
.map(|t| Local::now().date_naive().and_time(t));
bartib::controller::manipulation::start(file_name, project_name, activity_description, time)
bartib::controller::manipulation::start(
file_name,
project_name,
activity_description,
time,
)
}
("change", Some(sub_m)) => {
let project_name = sub_m.value_of("project");
@ -264,19 +266,28 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> {
let time = get_time_argument_or_ignore(sub_m.value_of("time"), "-t/--time")
.map(|t| Local::now().date_naive().and_time(t));
bartib::controller::manipulation::change(file_name, project_name, activity_description, time)
bartib::controller::manipulation::change(
file_name,
project_name,
activity_description,
time,
)
}
("continue", Some(sub_m)) => {
let project_name = sub_m.value_of("project");
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::now().date_naive().and_time(t));
let number = get_number_argument_or_ignore(
sub_m.value_of("number"),
"-n/--number",
).unwrap_or(0);
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, number)
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")
@ -295,12 +306,12 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> {
let filter = create_filter_for_arguments(sub_m);
bartib::controller::report::show_report(file_name, filter)
}
("projects", Some(sub_m)) => bartib::controller::list::list_projects(file_name, sub_m.is_present("current")),
("projects", Some(sub_m)) => {
bartib::controller::list::list_projects(file_name, sub_m.is_present("current"))
}
("last", Some(sub_m)) => {
let number = get_number_argument_or_ignore(
sub_m.value_of("number"),
"-n/--number",
).unwrap_or(10);
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)) => {
@ -322,7 +333,7 @@ fn create_filter_for_arguments<'a>(sub_m: &'a ArgMatches) -> ActivityFilter<'a>
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"),
project: sub_m.value_of("project")
project: sub_m.value_of("project"),
};
let today = Local::now().naive_local().date();
@ -335,13 +346,26 @@ fn create_filter_for_arguments<'a>(sub_m: &'a ArgMatches) -> ActivityFilter<'a>
}
if sub_m.is_present("current_week") {
filter.from_date = Some(today - Duration::days(today.weekday().num_days_from_monday() as i64));
filter.to_date = Some(today - Duration::days(today.weekday().num_days_from_monday() as i64) + Duration::days(6));
filter.from_date =
Some(today - Duration::days(today.weekday().num_days_from_monday() as i64));
filter.to_date = Some(
today - Duration::days(today.weekday().num_days_from_monday() as i64)
+ Duration::days(6),
);
}
if sub_m.is_present("last_week") {
filter.from_date = Some(today - Duration::days(today.weekday().num_days_from_monday() as i64) - Duration::weeks(1));
filter.to_date = Some(today - Duration::days(today.weekday().num_days_from_monday() as i64) - Duration::weeks(1) + Duration::days(6) )
filter.from_date = Some(
today
- Duration::days(today.weekday().num_days_from_monday() as i64)
- Duration::weeks(1),
);
filter.to_date = Some(
today
- Duration::days(today.weekday().num_days_from_monday() as i64)
- Duration::weeks(1)
+ Duration::days(6),
)
}
filter

View file

@ -2,8 +2,8 @@ use chrono::NaiveDate;
use nu_ansi_term::Color;
use std::collections::BTreeMap;
use crate::data::activity;
use crate::conf;
use crate::data::activity;
use crate::view::format_util;
use crate::view::table;
@ -36,7 +36,7 @@ pub fn list_activities_grouped_by_date(activities: &[&activity::Activity]) {
group_activities_by_date(activities)
.iter()
.map(|(date, activity_list)| {
create_activites_group(&format!("{}", date), activity_list.as_slice())
create_activities_group(&format!("{}", date), activity_list.as_slice())
})
.for_each(|g| activity_table.add_group(g));
@ -45,15 +45,30 @@ pub fn list_activities_grouped_by_date(activities: &[&activity::Activity]) {
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 },
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 {
fn create_activities_group(title: &str, activities: &[&activity::Activity]) -> table::Group {
let rows = activities
.iter()
.map(|a| get_activity_table_row(a, false))
@ -67,10 +82,22 @@ pub fn list_running_activities(activities: &[&activity::Activity]) {
println!("No Activity is currently running");
} else {
let mut activity_table = table::Table::new(vec![
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 },
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,
},
]);
activities
@ -90,24 +117,35 @@ pub fn list_running_activities(activities: &[&activity::Activity]) {
}
// display a list of projects and descriptions with index number
pub fn list_descriptions_and_projects(descriptions_and_projects : &[(&String, &String)]) {
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![
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 },
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();
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()])
);
descriptions_and_projects_table.add_row(table::Row::new(vec![
format!("[{}]", i),
description.to_string(),
project.to_string(),
]));
}
println!("\n{}", descriptions_and_projects_table);
@ -118,7 +156,9 @@ pub fn list_descriptions_and_projects(descriptions_and_projects : &[(&String, &S
//
// the date of the end is shown when it is not the same date as the start
fn get_activity_table_row(activity: &activity::Activity, with_start_dates: bool) -> table::Row {
let more_then_one_day = activity.end.map_or(false, |end| activity.start.date() != end.date());
let more_then_one_day = activity
.end
.map_or(false, |end| activity.start.date() != end.date());
let display_end = activity.end.map_or_else(
|| "-".to_string(),

View file

@ -1,4 +1,4 @@
pub mod format_util;
pub mod list;
pub mod report;
pub mod table;
pub mod table;

View file

@ -15,14 +15,14 @@ type ProjectMap<'a> = BTreeMap<&'a str, (Vec<&'a activity::Activity>, Duration)>
struct Report<'a> {
project_map: ProjectMap<'a>,
total_duration: Duration
total_duration: Duration,
}
impl<'a> Report<'a> {
fn new(activities: &'a [&'a activity::Activity]) -> Report<'a> {
Report {
Report {
project_map: create_project_map(activities),
total_duration: sum_duration(activities)
total_duration: sum_duration(activities),
}
}
}
@ -32,7 +32,8 @@ impl<'a> fmt::Display for Report<'a> {
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 terminal_width = term_size::dimensions_stdout().map(|d| d.0)
let terminal_width = term_size::dimensions_stdout()
.map(|d| d.0)
.unwrap_or(conf::DEFAULT_WIDTH);
if terminal_width < longest_line + longest_duration_string + 1 {
@ -42,7 +43,12 @@ impl<'a> fmt::Display for Report<'a> {
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, longest_duration_string)?;
print_descriptions_with_durations(
f,
activities,
longest_line,
longest_duration_string,
)?;
writeln!(f)?;
}
@ -61,9 +67,11 @@ fn create_project_map<'a>(activities: &'a [&'a activity::Activity]) -> ProjectMa
let mut project_map: ProjectMap = BTreeMap::new();
activities.iter().for_each(|a| {
project_map.entry(&a.project)
project_map
.entry(&a.project)
.or_insert_with(|| (Vec::<&'a activity::Activity>::new(), Duration::seconds(0)))
.0.push(a);
.0
.push(a);
});
for (_project, (activities, duration)) in project_map.iter_mut() {
@ -83,7 +91,13 @@ fn sum_duration(activities: &[&activity::Activity]) -> Duration {
duration
}
fn print_project_heading(f: &mut Formatter, project: &&str, duration: &Duration, longest_line: usize, duration_width: usize) -> fmt::Result {
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));
@ -91,11 +105,13 @@ fn print_project_heading(f: &mut Formatter, project: &&str, duration: &Duration,
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
write!(
f,
"{line:.<width$} {duration:>duration_width$}",
line = line,
width = longest_line,
duration = format_util::format_duration(duration),
duration_width = duration_width
)?;
}
}
@ -103,7 +119,12 @@ fn print_project_heading(f: &mut Formatter, project: &&str, duration: &Duration,
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 {
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)
@ -118,11 +139,13 @@ fn print_descriptions_with_durations<'a>(f: &mut fmt::Formatter<'_>, activities:
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
writeln!(
f,
"{line:.<width$} {duration:>duration_width$}",
line = line,
width = line_width,
duration = format_util::format_duration(&description_duration),
duration_width = duration_width
)?;
}
}
@ -131,23 +154,32 @@ fn print_descriptions_with_durations<'a>(f: &mut fmt::Formatter<'_>, activities:
Ok(())
}
fn print_total_duration(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())
fn print_total_duration(
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_description<'a>(activities: &'a [&'a activity::Activity]) -> BTreeMap<&str, Vec<&'a activity::Activity>> {
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)
activity_map
.entry(&a.description)
.or_insert_with(Vec::<&'a activity::Activity>::new)
.push(a);
});
@ -157,23 +189,34 @@ fn group_activities_by_description<'a>(activities: &'a [&'a activity::Activity])
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().flat_map(|(a, _d)| a)
.map(|a| a.description.chars().count() + conf::REPORT_INDENTATION).max();
let longest_activity_line = project_map
.values()
.flat_map(|(a, _d)| a)
.map(|a| a.description.chars().count() + conf::REPORT_INDENTATION)
.max();
get_max_option(longest_project_line, longest_activity_line)
}
fn get_longest_duration_string(report: &Report) -> Option<usize> {
let longest_project_duration = report.project_map.values()
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().flat_map(|(a, _d)| a)
let longest_activity_duration = report
.project_map
.values()
.flat_map(|(a, _d)| a)
.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();
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))
}
@ -181,7 +224,11 @@ fn get_longest_duration_string(report: &Report) -> Option<usize> {
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 }
if s1 > s2 {
o1
} else {
o2
}
} else {
o1
}
@ -201,12 +248,36 @@ mod tests {
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
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);

View file

@ -1,7 +1,7 @@
use std::borrow::Cow;
use std::cmp;
use std::fmt;
use std::str;
use std::borrow::Cow;
use nu_ansi_term::Style;
use textwrap;
@ -10,12 +10,12 @@ use crate::conf;
pub enum Wrap {
Wrap,
NoWrap
NoWrap,
}
pub struct Column {
pub label: String,
pub wrap: Wrap
pub wrap: Wrap,
}
pub struct Row {
@ -80,15 +80,18 @@ impl Table {
/* 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.
*/
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();
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
@ -96,7 +99,9 @@ impl Table {
}
// the total width of the columns that we may not wrap
let unwrapable_width : usize = max_column_width.iter().zip(columns_wrap.iter())
let unwrapable_width: usize = max_column_width
.iter()
.zip(columns_wrap.iter())
.filter(|(_, wrap)| matches!(wrap, Wrap::NoWrap))
.map(|(width, _)| width)
.sum();
@ -107,21 +112,37 @@ impl Table {
}
// 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 } else { 0 })
let mut column_width: Vec<usize> = max_column_width
.iter()
.zip(columns_wrap.iter())
.map(|(width, wrap)| {
if matches!(wrap, Wrap::NoWrap) {
*width
} 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());
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 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
@ -144,7 +165,9 @@ impl Table {
}
fn get_max_column_width(&self) -> Vec<usize> {
let mut max_column_width: Vec<usize> = self.columns.iter()
let mut max_column_width: Vec<usize> = self
.columns
.iter()
.map(|c| c.label.chars().count())
.collect();
@ -167,20 +190,15 @@ impl Table {
impl fmt::Display for Table {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let terminal_width = term_size::dimensions_stdout().map(|d| d.0)
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();
let labels: Vec<&String> = self.columns.iter().map(|c| &c.label).collect();
write_cells(
f,
&labels,
&column_width,
Some(Style::new().underline()),
)?;
write_cells(f, &labels, &column_width, Some(Style::new().underline()))?;
writeln!(f)?;
for row in &self.rows {
@ -195,11 +213,7 @@ impl fmt::Display for Table {
}
}
fn write_group(
f: &mut fmt::Formatter<'_>,
group: &Group,
column_width: &[usize],
) -> fmt::Result {
fn write_group(f: &mut fmt::Formatter<'_>, group: &Group, column_width: &[usize]) -> fmt::Result {
let empty_string = "".to_string();
let title = group.title.as_ref().unwrap_or(&empty_string);
@ -225,8 +239,7 @@ fn write_cells<T: AsRef<str> + std::fmt::Display>(
column_width: &[usize],
style: Option<Style>,
) -> fmt::Result {
let wrapped_cells : Vec<Vec<Cow<str>>> = cells
let wrapped_cells: Vec<Vec<Cow<str>>> = cells
.iter()
.enumerate()
.map(|(i, c)| match column_width.get(i) {
@ -237,19 +250,20 @@ fn write_cells<T: AsRef<str> + std::fmt::Display>(
})
.collect();
let most_lines : usize = wrapped_cells.iter().map(|c| c.len()).max().unwrap_or(1);
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))?
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)?; }
if is_last_line {
writeln!(f)?;
}
}
Ok(())
@ -299,20 +313,38 @@ mod tests {
#[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 },
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
"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
"abcdefg".to_string(), // 7 -> muss nicht gewrapt werden
]);
t.add_row(row1);
@ -327,24 +359,41 @@ mod tests {
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 },
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
"abcdefg".to_string(), // 7
"abcdefghijkl".to_string(), // 12
"abcde".to_string(), // 5
"abc".to_string(), // 3
"abcdefghijklmno".to_string(), // 15
"abcdefg".to_string() // 7
"abcdefg".to_string(), // 7
]);
t.add_row(row1);
@ -362,20 +411,38 @@ mod tests {
#[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 },
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
"abcdefg".to_string(), // 7
"abcdefghijkl".to_string(), // 12
"abcde".to_string(), // 5
"abc".to_string(), // 3
"abcdefghijklmno".to_string(), // 15
"abcdefg".to_string() // 7
"abcdefg".to_string(), // 7
]);
t.add_row(row1);
@ -407,9 +474,18 @@ mod tests {
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 },
Column {
label: "a".to_string(),
wrap: Wrap::NoWrap,
},
Column {
label: "b".to_string(),
wrap: Wrap::NoWrap,
},
Column {
label: "c".to_string(),
wrap: Wrap::NoWrap,
},
]
}
}