From e977fb294c0b14cf6e65e289cda367caef560b39 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Sun, 23 May 2021 18:16:11 -0400 Subject: [PATCH] Implement modifying tasks' "wait" value --- Cargo.lock | 2 ++ cli/Cargo.toml | 2 ++ cli/src/argparse/args.rs | 37 +++++++++++++++++++ cli/src/argparse/mod.rs | 7 ++++ cli/src/argparse/modification.rs | 62 +++++++++++++++++++++++++++++--- cli/src/invocation/modify.rs | 4 +++ docs/src/tasks.md | 1 + taskchampion/src/task.rs | 55 ++++++++++++++++++++++++++++ 8 files changed, 166 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 144f8e56f..6499dfbb7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2809,8 +2809,10 @@ dependencies = [ "anyhow", "assert_cmd", "atty", + "chrono", "dirs-next", "env_logger 0.8.3", + "lazy_static", "log", "mdbook", "nom", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 591d068a9..53c26dd2b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -22,6 +22,8 @@ termcolor = "^1.1.2" atty = "^0.2.14" toml = "^0.5.8" toml_edit = "^0.2.0" +chrono = "*" +lazy_static = "1" # only needed for usage-docs mdbook = { version = "0.4", optional = true } diff --git a/cli/src/argparse/args.rs b/cli/src/argparse/args.rs index a902fd690..fceea572a 100644 --- a/cli/src/argparse/args.rs +++ b/cli/src/argparse/args.rs @@ -1,5 +1,7 @@ //! Parsers for argument lists -- arrays of strings use super::ArgList; +use super::NOW; +use chrono::prelude::*; use nom::bytes::complete::tag as nomtag; use nom::{ branch::*, @@ -67,6 +69,30 @@ pub(super) fn status_colon(input: &str) -> IResult<&str, Status> { map_res(colon_prefixed("status"), to_status)(input) } +/// Recognizes timestamps +pub(super) fn timestamp(input: &str) -> IResult<&str, DateTime> { + // TODO: full relative date language supported by TW + fn nn_d_to_timestamp(input: &str) -> Result, ()> { + // TODO: don't unwrap + Ok(*NOW + chrono::Duration::days(input.parse().unwrap())) + } + map_res(terminated(digit1, char('d')), nn_d_to_timestamp)(input) +} + +/// Recognizes `wait:` to None and `wait:` to `Some(ts)` +pub(super) fn wait_colon(input: &str) -> IResult<&str, Option>> { + fn to_wait(input: DateTime) -> Result>, ()> { + Ok(Some(input)) + } + fn to_none(_: &str) -> Result>, ()> { + Ok(None) + } + preceded( + nomtag("wait:"), + alt((map_res(timestamp, to_wait), map_res(nomtag(""), to_none))), + )(input) +} + /// Recognizes a comma-separated list of TaskIds pub(super) fn id_list(input: &str) -> IResult<&str, Vec> { fn hex_n(n: usize) -> impl Fn(&str) -> IResult<&str, &str> { @@ -237,6 +263,17 @@ mod test { assert!(minus_tag("-1abc").is_err()); } + #[test] + fn test_wait() { + assert_eq!(wait_colon("wait:").unwrap(), ("", None)); + + let one_day = *NOW + chrono::Duration::days(1); + assert_eq!(wait_colon("wait:1d").unwrap(), ("", Some(one_day))); + + let one_day = *NOW + chrono::Duration::days(1); + assert_eq!(wait_colon("wait:1d2").unwrap(), ("2", Some(one_day))); + } + #[test] fn test_literal() { assert_eq!(literal("list")("list").unwrap().1, "list"); diff --git a/cli/src/argparse/mod.rs b/cli/src/argparse/mod.rs index 88de59046..f837b0ecf 100644 --- a/cli/src/argparse/mod.rs +++ b/cli/src/argparse/mod.rs @@ -31,9 +31,16 @@ pub(crate) use modification::{DescriptionMod, Modification}; pub(crate) use subcommand::Subcommand; use crate::usage::Usage; +use chrono::prelude::*; +use lazy_static::lazy_static; type ArgList<'a> = &'a [&'a str]; +lazy_static! { + // A static value of NOW to make tests easier + pub(super) static ref NOW: DateTime = Utc::now(); +} + pub(crate) fn get_usage(usage: &mut Usage) { Subcommand::get_usage(usage); Filter::get_usage(usage); diff --git a/cli/src/argparse/modification.rs b/cli/src/argparse/modification.rs index 9628f2352..5b3cac3df 100644 --- a/cli/src/argparse/modification.rs +++ b/cli/src/argparse/modification.rs @@ -1,6 +1,7 @@ -use super::args::{any, arg_matching, minus_tag, plus_tag}; +use super::args::{any, arg_matching, minus_tag, plus_tag, wait_colon}; use super::ArgList; use crate::usage; +use chrono::prelude::*; use nom::{branch::alt, combinator::*, multi::fold_many0, IResult}; use std::collections::HashSet; use taskchampion::Status; @@ -36,6 +37,9 @@ pub struct Modification { /// Set the status pub status: Option, + /// Set (or, with `Some(None)`, clear) the wait timestamp + pub wait: Option>>, + /// Set the "active" state, that is, start (true) or stop (false) the task. pub active: Option, @@ -51,6 +55,7 @@ enum ModArg<'a> { Description(&'a str), PlusTag(&'a str), MinusTag(&'a str), + Wait(Option>), } impl Modification { @@ -71,6 +76,9 @@ impl Modification { ModArg::MinusTag(tag) => { acc.remove_tags.insert(tag.to_owned()); } + ModArg::Wait(wait) => { + acc.wait = Some(wait); + } } acc } @@ -78,6 +86,7 @@ impl Modification { alt(( Self::plus_tag, Self::minus_tag, + Self::wait, // this must come last Self::description, )), @@ -109,6 +118,13 @@ impl Modification { map_res(arg_matching(minus_tag), to_modarg)(input) } + fn wait(input: ArgList) -> IResult { + fn to_modarg(input: Option>) -> Result, ()> { + Ok(ModArg::Wait(input)) + } + map_res(arg_matching(wait_colon), to_modarg)(input) + } + pub(super) fn get_usage(u: &mut usage::Usage) { u.modifications.push(usage::Modification { syntax: "DESCRIPTION", @@ -122,14 +138,25 @@ impl Modification { u.modifications.push(usage::Modification { syntax: "+TAG", summary: "Tag task", - description: " - Add the given tag to the task.", + description: "Add the given tag to the task.", }); u.modifications.push(usage::Modification { syntax: "-TAG", summary: "Un-tag task", + description: "Remove the given tag from the task.", + }); + u.modifications.push(usage::Modification { + syntax: "status:{pending,completed,deleted}", + summary: "Set the task's status", + description: "Set the status of the task explicitly.", + }); + u.modifications.push(usage::Modification { + syntax: "wait:", + summary: "Set or unset the task's wait time", description: " - Remove the given tag from the task.", + Set the time before which the task is not actionable and + should not be shown in reports. With `wait:`, the time + is un-set.", }); } } @@ -137,6 +164,7 @@ impl Modification { #[cfg(test)] mod test { use super::*; + use crate::argparse::NOW; #[test] fn test_empty() { @@ -176,6 +204,32 @@ mod test { ); } + #[test] + fn test_set_wait() { + let (input, modification) = Modification::parse(argv!["wait:2d"]).unwrap(); + assert_eq!(input.len(), 0); + assert_eq!( + modification, + Modification { + wait: Some(Some(*NOW + chrono::Duration::days(2))), + ..Default::default() + } + ); + } + + #[test] + fn test_unset_wait() { + let (input, modification) = Modification::parse(argv!["wait:"]).unwrap(); + assert_eq!(input.len(), 0); + assert_eq!( + modification, + Modification { + wait: Some(None), + ..Default::default() + } + ); + } + #[test] fn test_multi_arg_description() { let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap(); diff --git a/cli/src/invocation/modify.rs b/cli/src/invocation/modify.rs index cbbb65e1c..7ef6d758c 100644 --- a/cli/src/invocation/modify.rs +++ b/cli/src/invocation/modify.rs @@ -40,5 +40,9 @@ pub(super) fn apply_modification( task.remove_tag(&tag)?; } + if let Some(wait) = modification.wait { + task.set_wait(wait)?; + } + Ok(()) } diff --git a/docs/src/tasks.md b/docs/src/tasks.md index 8bdf8fa72..ae354c62c 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -32,6 +32,7 @@ The following keys, and key formats, are defined: * `modified` - the time of the last modification of this task * `start.` - either an empty string (representing work on the task to the task that has not been stopped) or a timestamp (representing the time that work stopped) * `tag.` - indicates this task has tag `` (value is an empty string) +* `wait` - indicates the time before which this task should be hidden, as it is not actionable The following are not yet implemented: diff --git a/taskchampion/src/task.rs b/taskchampion/src/task.rs index 07bb19909..2cebf11ae 100644 --- a/taskchampion/src/task.rs +++ b/taskchampion/src/task.rs @@ -211,6 +211,20 @@ impl Task { .unwrap_or("") } + /// Get the wait time. If this value is set, it will be returned, even + /// if it is in the past. + pub fn get_wait(&self) -> Option> { + self.get_timestamp("wait") + } + + /// Determine whether this task is waiting now. + pub fn is_waiting(&self) -> bool { + if let Some(ts) = self.get_wait() { + return ts > Utc::now(); + } + false + } + /// Determine whether this task is active -- that is, that it has been started /// and not stopped. pub fn is_active(&self) -> bool { @@ -275,6 +289,10 @@ impl<'r> TaskMut<'r> { self.set_string("description", Some(description)) } + pub fn set_wait(&mut self, wait: Option>) -> anyhow::Result<()> { + self.set_timestamp("wait", wait) + } + pub fn set_modified(&mut self, modified: DateTime) -> anyhow::Result<()> { self.set_timestamp("modified", Some(modified)) } @@ -452,6 +470,43 @@ mod test { assert!(!task.is_active()); } + #[test] + fn test_wait_not_set() { + let task = Task::new(Uuid::new_v4(), TaskMap::new()); + + assert!(!task.is_waiting()); + assert_eq!(task.get_wait(), None); + } + + #[test] + fn test_wait_in_past() { + let ts = Utc.ymd(1970, 1, 1).and_hms(0, 0, 0); + let task = Task::new( + Uuid::new_v4(), + vec![(String::from("wait"), format!("{}", ts.timestamp()))] + .drain(..) + .collect(), + ); + dbg!(&task); + + assert!(!task.is_waiting()); + assert_eq!(task.get_wait(), Some(ts)); + } + + #[test] + fn test_wait_in_future() { + let ts = Utc.ymd(3000, 1, 1).and_hms(0, 0, 0); + let task = Task::new( + Uuid::new_v4(), + vec![(String::from("wait"), format!("{}", ts.timestamp()))] + .drain(..) + .collect(), + ); + + assert!(task.is_waiting()); + assert_eq!(task.get_wait(), Some(ts)); + } + #[test] fn test_has_tag() { let task = Task::new(