added time parameter to start and stop commands

This commit is contained in:
Nikolas Schmidt-Voigt 2021-09-02 09:12:23 +02:00
parent 22aab0f31d
commit c757d92b38
6 changed files with 169 additions and 79 deletions

View file

@ -23,17 +23,17 @@ pub enum ActivityError {
}
impl Activity {
pub fn start(project: String, description: String) -> Activity {
pub fn start(project: String, description: String, time: Option<NaiveDateTime>) -> Activity {
Activity {
start: Local::now().naive_local(),
start: time.unwrap_or_else(|| Local::now().naive_local()),
end: None,
project,
description,
}
}
pub fn stop(&mut self) {
self.end = Some(Local::now().naive_local());
pub fn stop(&mut self, time: Option<NaiveDateTime>) {
self.end = time.or_else(|| Some(Local::now().naive_local()));
}
pub fn is_stopped(&self) -> bool {
@ -174,12 +174,14 @@ fn split_with_escaped_delimeter(s: &str) -> StringSplitter {
mod tests {
use super::*;
use chrono::{Datelike, Timelike};
use std::option::Option::None;
#[test]
fn start() {
let t = Activity::start(
"test project".to_string(),
"test description".to_string(),
None,
);
assert_eq!(t.description, "test description".to_string());
assert_eq!(t.project, "test project".to_string());
@ -191,8 +193,9 @@ mod tests {
let mut t = Activity::start(
"test project".to_string(),
"test description".to_string(),
None,
);
t.stop();
t.stop(None);
assert_ne!(t.end, None);
}
@ -201,6 +204,7 @@ mod tests {
let mut t = Activity::start(
"test project| 1".to_string(),
"test\\description".to_string(),
None,
);
t.start = NaiveDateTime::parse_from_str("2021-02-16 16:14", conf::FORMAT_DATETIME).unwrap();
assert_eq!(
@ -282,8 +286,9 @@ mod tests {
let mut t = Activity::start(
"ex\\ample\\\\pro|ject".to_string(),
"e\\\\xam|||ple tas\t\t\nk".to_string(),
None,
);
t.stop();
t.stop(None);
let t2 = Activity::from_str(format!("{}", t).as_str()).unwrap();
assert_eq!(

View file

@ -1,8 +1,8 @@
use anyhow::{Context, Result};
use std::fs::{File, OpenOptions};
use std::io;
use std::io::{BufRead, BufReader, Write};
use std::str::FromStr;
use anyhow::{Context, Result};
use crate::activity;
@ -49,7 +49,8 @@ impl Line {
// reads the content of a file to a vector of lines
pub fn get_file_content(file_name: &str) -> Result<Vec<Line>> {
let file_handler = File::open(file_name).context(format!("Could not read from file: {}", file_name))?;
let file_handler =
File::open(file_name).context(format!("Could not read from file: {}", file_name))?;
let reader = BufReader::new(file_handler);
let lines = reader

View file

@ -1,10 +1,9 @@
use anyhow::{anyhow, bail, Context, Result, Error};
use chrono::{naive, NaiveDate};
use anyhow::{anyhow, bail, Context, Error, Result};
use chrono::{naive, NaiveDate, NaiveDateTime};
use crate::bartib_file::Line;
use crate::activity::Activity;
use crate::bartib_file::Line;
use std::process::Command;
mod activity;
pub mod bartib_file;
pub mod conf;
@ -20,24 +19,37 @@ pub struct ActivityFilter {
}
// starts a new activity
pub fn start(file_name: &str, project_name: &str, activity_description: &str) -> Result<()>{
pub fn start(
file_name: &str,
project_name: &str,
activity_description: &str,
time: Option<NaiveDateTime>,
) -> Result<()> {
let mut file_content: Vec<Line> = Vec::new();
if let Ok(mut previous_file_content) = bartib_file::get_file_content(file_name) {
// if we start a new activities programaticly, we stop all other activities first.
// However, we must not assume that there is always only one activity
// running as the user may have started activities manually
stop_all_running_activities(&mut previous_file_content);
stop_all_running_activities(&mut previous_file_content, time);
file_content.append(&mut previous_file_content);
}
let activity = activity::Activity::start(project_name.to_string(), activity_description.to_string());
let activity = activity::Activity::start(
project_name.to_string(),
activity_description.to_string(),
time,
);
save_new_activity(file_name, &mut file_content, activity)
}
fn save_new_activity(file_name: &str, file_content: &mut Vec<Line>, activity: Activity) -> Result<(), Error> {
fn save_new_activity(
file_name: &str,
file_content: &mut Vec<Line>,
activity: Activity,
) -> Result<(), Error> {
println!(
"Started activity: \"{}\" ({}) at {}",
activity.description,
@ -46,18 +58,20 @@ fn save_new_activity(file_name: &str, file_content: &mut Vec<Line>, activity: Ac
);
file_content.push(bartib_file::Line::for_activity(activity));
bartib_file::write_to_file(file_name, &file_content).context(format!("Could not write to file: {}", file_name))
bartib_file::write_to_file(file_name, &file_content)
.context(format!("Could not write to file: {}", file_name))
}
// stops all currently running activities
pub fn stop(file_name: &str) -> Result<()>{
pub fn stop(file_name: &str, time: Option<NaiveDateTime>) -> Result<()> {
let mut file_content = bartib_file::get_file_content(file_name)?;
stop_all_running_activities(&mut file_content);
bartib_file::write_to_file(file_name, &file_content).context(format!("Could not write to file: {}", file_name))
stop_all_running_activities(&mut file_content, time);
bartib_file::write_to_file(file_name, &file_content)
.context(format!("Could not write to file: {}", file_name))
}
// lists all currently runninng activities.
pub fn list_running(file_name: &str) -> Result<()>{
pub fn list_running(file_name: &str) -> Result<()> {
let file_content = bartib_file::get_file_content(file_name)?;
let running_activities = get_running_activities(&file_content);
output::list_running_activities(&running_activities);
@ -98,7 +112,7 @@ pub fn list(file_name: &str, filter: ActivityFilter, do_group_activities: bool)
pub fn list_projects(file_name: &str) -> Result<()> {
let file_content = bartib_file::get_file_content(file_name)?;
let mut all_projects : Vec<&String> = get_activities(&file_content)
let mut all_projects: Vec<&String> = get_activities(&file_content)
.map(|activity| &activity.project)
.collect();
@ -128,18 +142,26 @@ pub fn display_last_activity(file_name: &str) -> Result<()> {
}
// continue last activity
pub fn continue_last_activity(file_name: &str, project_name: Option<&str>, activity_description: Option<&str>) -> Result<()> {
pub fn continue_last_activity(
file_name: &str,
project_name: Option<&str>,
activity_description: Option<&str>,
time: Option<NaiveDateTime>,
) -> Result<()> {
let mut file_content = bartib_file::get_file_content(file_name)?;
let optional_last_activity = get_last_activity_by_start(&file_content)
.or(get_last_activity(&file_content));
let optional_last_activity =
get_last_activity_by_start(&file_content).or(get_last_activity(&file_content));
if let Some(last_activity) = optional_last_activity {
let new_activity = activity::Activity::start(
project_name.unwrap_or(&last_activity.project).to_string(),
activity_description.unwrap_or(&last_activity.description).to_string()
activity_description
.unwrap_or(&last_activity.description)
.to_string(),
time,
);
stop_all_running_activities(&mut file_content);
stop_all_running_activities(&mut file_content, time);
save_new_activity(file_name, &mut file_content, new_activity)
} else {
bail!("No activity has been started before.")
@ -155,9 +177,7 @@ pub fn start_editor(file_name: &str, optional_editor_command: Option<&str>) -> R
child.wait().context("editor did not execute")?;
Ok(())
}
Err(e) => {
Err(anyhow!(e))
}
Err(e) => Err(anyhow!(e)),
}
}
@ -179,11 +199,14 @@ fn get_index_of_first_element(length: usize, sub: Option<usize>) -> usize {
}
}
fn stop_all_running_activities(file_content: &mut [bartib_file::Line]) {
fn stop_all_running_activities(
file_content: &mut [bartib_file::Line],
time: Option<NaiveDateTime>,
) {
for line in file_content {
if let Ok(activity) = &mut line.activity {
if !activity.is_stopped() {
activity.stop();
activity.stop(time);
println!(
"Stopped activity: \"{}\" ({}) started at {} ({})",
activity.description,

View file

@ -1,15 +1,21 @@
use anyhow::{bail, Context, Result};
use chrono::{NaiveDate, Local, Duration};
use chrono::{Duration, Local, NaiveDate, NaiveTime};
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use nu_ansi_term::enable_ansi_support;
fn main() -> Result<()> {
#[cfg(windows)]
if let Err(e) = enable_ansi_support() {
println!("Could not enable ansi support! Errorcode: {}", e);
}
let arg_time = Arg::with_name("time")
.short("t")
.long("time")
.value_name("TIME")
.help("the time for changing the activity status (HH:MM)")
.takes_value(true);
let matches = App::new("bartib")
.version("0.1")
.author("Nikolas Schmidt-Voigt <nikolas.schmidt-voigt@posteo.de>")
@ -42,7 +48,8 @@ fn main() -> Result<()> {
.help("a description of the new activity")
.required(true)
.takes_value(true),
),
)
.arg(&arg_time),
)
.subcommand(
SubCommand::with_name("continue")
@ -60,9 +67,14 @@ fn main() -> Result<()> {
.value_name("DESCRIPTION")
.help("a description of the new activity")
.takes_value(true),
),
)
.arg(&arg_time),
)
.subcommand(
SubCommand::with_name("stop")
.about("stops all currently running activities")
.arg(&arg_time),
)
.subcommand(SubCommand::with_name("stop").about("stops all currently running activities"))
.subcommand(
SubCommand::with_name("current").about("lists all currently running activities"),
)
@ -110,7 +122,7 @@ fn main() -> Result<()> {
.help("show activities of the current day")
.required(false)
.conflicts_with_all(&["from_date", "to_date", "date", "yesterday"])
.takes_value(false)
.takes_value(false),
)
.arg(
Arg::with_name("yesterday")
@ -118,7 +130,7 @@ fn main() -> Result<()> {
.help("show yesterdays activities")
.required(false)
.conflicts_with_all(&["from_date", "to_date", "date", "today"])
.takes_value(false)
.takes_value(false),
)
.arg(
Arg::with_name("no_grouping")
@ -126,12 +138,8 @@ fn main() -> Result<()> {
.help("do not group activities by date in list"),
),
)
.subcommand(
SubCommand::with_name("last").about("displays last finished acitivity")
)
.subcommand(
SubCommand::with_name("projects").about("list all projects")
)
.subcommand(SubCommand::with_name("last").about("displays last finished acitivity"))
.subcommand(SubCommand::with_name("projects").about("list all projects"))
.subcommand(
SubCommand::with_name("edit")
.about("opens the activity log in an editor")
@ -142,7 +150,7 @@ fn main() -> Result<()> {
.help("the command to start your prefered text editor")
.env("EDITOR")
.takes_value(true),
)
),
)
.get_matches();
@ -157,16 +165,25 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> {
("start", Some(sub_m)) => {
let project_name = sub_m.value_of("project").unwrap();
let activity_description = sub_m.value_of("description").unwrap();
let time = get_time_argument_or_ignore(sub_m.value_of("time"), "-t/--time")
.map(|t| Local::today().naive_local().and_time(t));
bartib::start(file_name, project_name, activity_description)
},
bartib::start(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::today().naive_local().and_time(t));
bartib::continue_last_activity(file_name, project_name, activity_description)
},
("stop", Some(_)) => bartib::stop(file_name),
bartib::continue_last_activity(file_name, project_name, activity_description, time)
}
("stop", Some(sub_m)) => {
let time = get_time_argument_or_ignore(sub_m.value_of("time"), "-t/--time")
.map(|t| Local::today().naive_local().and_time(t));
bartib::stop(file_name, time)
}
("current", Some(_)) => bartib::list_running(file_name),
("list", Some(sub_m)) => {
let mut filter = bartib::ActivityFilter {
@ -189,13 +206,13 @@ fn run_subcommand(matches: &ArgMatches, file_name: &str) -> Result<()> {
let do_group_activities = !sub_m.is_present("no_grouping") && !filter.date.is_some();
bartib::list(file_name, filter, do_group_activities)
},
}
("projects", Some(_)) => bartib::list_projects(file_name),
("last", Some(_)) => bartib::display_last_activity(file_name),
("edit", Some(sub_m)) => {
let optional_editor_command = sub_m.value_of("editor");
bartib::start_editor(file_name, optional_editor_command)
},
}
_ => bail!("Unknown command"),
}
}
@ -242,3 +259,25 @@ fn get_date_argument_or_ignore(
None
}
}
fn get_time_argument_or_ignore(
time_argument: Option<&str>,
argument_name: &str,
) -> Option<NaiveTime> {
if let Some(time_string) = time_argument {
let parsing_result = NaiveTime::parse_from_str(time_string, bartib::conf::FORMAT_TIME);
match parsing_result {
Ok(date) => Some(date),
Err(parsing_error) => {
println!(
"Can not parse \"{}\" as time. Argument for {} is ignored ({})",
time_string, argument_name, parsing_error
);
None
}
}
} else {
None
}
}

View file

@ -1,5 +1,5 @@
use nu_ansi_term::Color;
use chrono::NaiveDate;
use nu_ansi_term::Color;
use std::collections::BTreeMap;
use crate::activity;
@ -45,15 +45,21 @@ pub fn list_activities_grouped_by_date(activities: &[&activity::Activity]) {
"Duration".to_string(),
]);
group_activities_by_date(activities).iter()
.map(|(date, activity_list)| create_activites_group(&format!("{}", date), activity_list.as_slice()))
group_activities_by_date(activities)
.iter()
.map(|(date, activity_list)| {
create_activites_group(&format!("{}", date), activity_list.as_slice())
})
.for_each(|g| activity_table.add_group(g));
println!("\n{}", activity_table);
}
fn create_activites_group(title: &str, activities: &[&activity::Activity]) -> table::Group {
let rows = activities.iter().map(|a| get_activity_table_row(&a, false)).collect();
fn create_activites_group(title: &str, activities: &[&activity::Activity]) -> table::Group {
let rows = activities
.iter()
.map(|a| get_activity_table_row(&a, false))
.collect();
table::Group::new(Some(title.to_string()), rows)
}
@ -62,17 +68,16 @@ pub fn list_running_activities(running_activities: &[&activity::Activity]) {
if running_activities.is_empty() {
println!("No Activity is currently running");
} else {
let mut activity_table =
table::Table::new(vec![
"Started At".to_string(),
"Description".to_string(),
"Project".to_string(),
"Duration".to_string()
]);
let mut activity_table = table::Table::new(vec![
"Started At".to_string(),
"Description".to_string(),
"Project".to_string(),
"Duration".to_string(),
]);
running_activities
.iter()
.map(|activity | {
.map(|activity| {
table::Row::new(vec![
activity.start.format(conf::FORMAT_DATETIME).to_string(),
activity.description.clone(),
@ -92,7 +97,10 @@ pub fn display_single_activity(activity: &activity::Activity) {
if let Some(end) = activity.end {
println!("End: {}", end.format(conf::FORMAT_DATETIME));
println!("Duration: {}", format_util::format_duration(&activity.get_duration()));
println!(
"Duration: {}",
format_util::format_duration(&activity.get_duration())
);
}
println!("Project: {}", activity.project);

View file

@ -57,7 +57,8 @@ impl Table {
}
fn get_all_rows(&self) -> Vec<&Row> {
self.groups.iter()
self.groups
.iter()
.flat_map(|g| g.rows.as_slice())
.chain(self.rows.as_slice())
.collect()
@ -67,7 +68,8 @@ impl Table {
let mut column_width: Vec<usize> = self.header.iter().map(|e| e.chars().count()).collect();
for row in self.get_all_rows() {
row.content.iter()
row.content
.iter()
.map(|cell| cell.chars().count())
.enumerate()
.for_each(|(i, char_count)| {
@ -87,7 +89,12 @@ impl fmt::Display for Table {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let column_width = self.get_column_width();
write_cells(f, &self.header, &column_width, Some(Style::new().underline()))?;
write_cells(
f,
&self.header,
&column_width,
Some(Style::new().underline()),
)?;
writeln!(f)?;
for row in &self.rows {
@ -102,7 +109,11 @@ impl fmt::Display for Table {
}
}
fn write_group(f: &mut fmt::Formatter<'_>, group: &Group, column_width: &Vec<usize>) -> fmt::Result {
fn write_group(
f: &mut fmt::Formatter<'_>,
group: &Group,
column_width: &Vec<usize>,
) -> fmt::Result {
let empty_string = "".to_string();
let title = group.title.as_ref().unwrap_or(&empty_string);
@ -113,10 +124,10 @@ fn write_group(f: &mut fmt::Formatter<'_>, group: &Group, column_width: &Vec<usi
write_row(f, row, &column_width)?;
}
Ok(())
Ok(())
}
fn write_row(f: &mut fmt::Formatter<'_>, row: &Row, column_width : &Vec<usize>) -> fmt::Result {
fn write_row(f: &mut fmt::Formatter<'_>, row: &Row, column_width: &Vec<usize>) -> fmt::Result {
write_cells(f, &row.content, &column_width, row.style)?;
writeln!(f)?;
Ok(())
@ -128,7 +139,8 @@ fn write_cells<T: AsRef<str> + std::fmt::Display>(
column_width: &[usize],
style: Option<Style>,
) -> fmt::Result {
let cells_with_width : Vec<(Option<&usize>, &str)> = cells.iter()
let cells_with_width: Vec<(Option<&usize>, &str)> = cells
.iter()
.map(|cell| cell.as_ref())
.enumerate()
.map(|(i, cell)| (column_width.get(i), cell))
@ -152,11 +164,13 @@ fn write_with_width_and_style(
let style_suffix = opt_style.map_or("".to_string(), |style| style.suffix().to_string());
let width = opt_width.unwrap_or(&content_length);
write!(f, "{prefix}{content:<width$}{suffix} ",
prefix = style_prefix,
content = content,
width = width,
suffix = style_suffix
write!(
f,
"{prefix}{content:<width$}{suffix} ",
prefix = style_prefix,
content = content,
width = width,
suffix = style_suffix
)
}