mirror of
https://github.com/nikolassv/bartib
synced 2024-11-10 14:14:12 +00:00
issue-18: fix some typos
This commit is contained in:
parent
7a4c258a08
commit
8bd6d97718
15 changed files with 464 additions and 235 deletions
51
README.md
51
README.md
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)?;
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
pub mod list;
|
||||
pub mod manipulation;
|
||||
pub mod report;
|
||||
pub mod report;
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
pub mod activity;
|
||||
pub mod bartib_file;
|
||||
pub mod getter;
|
||||
pub mod getter;
|
||||
|
|
|
@ -2,4 +2,4 @@ pub mod conf;
|
|||
pub mod controller;
|
||||
pub mod data;
|
||||
|
||||
mod view;
|
||||
mod view;
|
||||
|
|
112
src/main.rs
112
src/main.rs
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
pub mod format_util;
|
||||
pub mod list;
|
||||
pub mod report;
|
||||
pub mod table;
|
||||
pub mod table;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue