Implement modifying tasks' "wait" value

This commit is contained in:
Dustin J. Mitchell 2021-05-23 18:16:11 -04:00 committed by Dustin J. Mitchell
parent d7d703f135
commit e977fb294c
8 changed files with 166 additions and 4 deletions

2
Cargo.lock generated
View file

@ -2809,8 +2809,10 @@ dependencies = [
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
"atty", "atty",
"chrono",
"dirs-next", "dirs-next",
"env_logger 0.8.3", "env_logger 0.8.3",
"lazy_static",
"log", "log",
"mdbook", "mdbook",
"nom", "nom",

View file

@ -22,6 +22,8 @@ termcolor = "^1.1.2"
atty = "^0.2.14" atty = "^0.2.14"
toml = "^0.5.8" toml = "^0.5.8"
toml_edit = "^0.2.0" toml_edit = "^0.2.0"
chrono = "*"
lazy_static = "1"
# only needed for usage-docs # only needed for usage-docs
mdbook = { version = "0.4", optional = true } mdbook = { version = "0.4", optional = true }

View file

@ -1,5 +1,7 @@
//! Parsers for argument lists -- arrays of strings //! Parsers for argument lists -- arrays of strings
use super::ArgList; use super::ArgList;
use super::NOW;
use chrono::prelude::*;
use nom::bytes::complete::tag as nomtag; use nom::bytes::complete::tag as nomtag;
use nom::{ use nom::{
branch::*, branch::*,
@ -67,6 +69,30 @@ pub(super) fn status_colon(input: &str) -> IResult<&str, Status> {
map_res(colon_prefixed("status"), to_status)(input) map_res(colon_prefixed("status"), to_status)(input)
} }
/// Recognizes timestamps
pub(super) fn timestamp(input: &str) -> IResult<&str, DateTime<Utc>> {
// TODO: full relative date language supported by TW
fn nn_d_to_timestamp(input: &str) -> Result<DateTime<Utc>, ()> {
// 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:<ts>` to `Some(ts)`
pub(super) fn wait_colon(input: &str) -> IResult<&str, Option<DateTime<Utc>>> {
fn to_wait(input: DateTime<Utc>) -> Result<Option<DateTime<Utc>>, ()> {
Ok(Some(input))
}
fn to_none(_: &str) -> Result<Option<DateTime<Utc>>, ()> {
Ok(None)
}
preceded(
nomtag("wait:"),
alt((map_res(timestamp, to_wait), map_res(nomtag(""), to_none))),
)(input)
}
/// Recognizes a comma-separated list of TaskIds /// Recognizes a comma-separated list of TaskIds
pub(super) fn id_list(input: &str) -> IResult<&str, Vec<TaskId>> { pub(super) fn id_list(input: &str) -> IResult<&str, Vec<TaskId>> {
fn hex_n(n: usize) -> impl Fn(&str) -> IResult<&str, &str> { fn hex_n(n: usize) -> impl Fn(&str) -> IResult<&str, &str> {
@ -237,6 +263,17 @@ mod test {
assert!(minus_tag("-1abc").is_err()); 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] #[test]
fn test_literal() { fn test_literal() {
assert_eq!(literal("list")("list").unwrap().1, "list"); assert_eq!(literal("list")("list").unwrap().1, "list");

View file

@ -31,9 +31,16 @@ pub(crate) use modification::{DescriptionMod, Modification};
pub(crate) use subcommand::Subcommand; pub(crate) use subcommand::Subcommand;
use crate::usage::Usage; use crate::usage::Usage;
use chrono::prelude::*;
use lazy_static::lazy_static;
type ArgList<'a> = &'a [&'a str]; type ArgList<'a> = &'a [&'a str];
lazy_static! {
// A static value of NOW to make tests easier
pub(super) static ref NOW: DateTime<Utc> = Utc::now();
}
pub(crate) fn get_usage(usage: &mut Usage) { pub(crate) fn get_usage(usage: &mut Usage) {
Subcommand::get_usage(usage); Subcommand::get_usage(usage);
Filter::get_usage(usage); Filter::get_usage(usage);

View file

@ -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 super::ArgList;
use crate::usage; use crate::usage;
use chrono::prelude::*;
use nom::{branch::alt, combinator::*, multi::fold_many0, IResult}; use nom::{branch::alt, combinator::*, multi::fold_many0, IResult};
use std::collections::HashSet; use std::collections::HashSet;
use taskchampion::Status; use taskchampion::Status;
@ -36,6 +37,9 @@ pub struct Modification {
/// Set the status /// Set the status
pub status: Option<Status>, pub status: Option<Status>,
/// Set (or, with `Some(None)`, clear) the wait timestamp
pub wait: Option<Option<DateTime<Utc>>>,
/// Set the "active" state, that is, start (true) or stop (false) the task. /// Set the "active" state, that is, start (true) or stop (false) the task.
pub active: Option<bool>, pub active: Option<bool>,
@ -51,6 +55,7 @@ enum ModArg<'a> {
Description(&'a str), Description(&'a str),
PlusTag(&'a str), PlusTag(&'a str),
MinusTag(&'a str), MinusTag(&'a str),
Wait(Option<DateTime<Utc>>),
} }
impl Modification { impl Modification {
@ -71,6 +76,9 @@ impl Modification {
ModArg::MinusTag(tag) => { ModArg::MinusTag(tag) => {
acc.remove_tags.insert(tag.to_owned()); acc.remove_tags.insert(tag.to_owned());
} }
ModArg::Wait(wait) => {
acc.wait = Some(wait);
}
} }
acc acc
} }
@ -78,6 +86,7 @@ impl Modification {
alt(( alt((
Self::plus_tag, Self::plus_tag,
Self::minus_tag, Self::minus_tag,
Self::wait,
// this must come last // this must come last
Self::description, Self::description,
)), )),
@ -109,6 +118,13 @@ impl Modification {
map_res(arg_matching(minus_tag), to_modarg)(input) map_res(arg_matching(minus_tag), to_modarg)(input)
} }
fn wait(input: ArgList) -> IResult<ArgList, ModArg> {
fn to_modarg(input: Option<DateTime<Utc>>) -> Result<ModArg<'static>, ()> {
Ok(ModArg::Wait(input))
}
map_res(arg_matching(wait_colon), to_modarg)(input)
}
pub(super) fn get_usage(u: &mut usage::Usage) { pub(super) fn get_usage(u: &mut usage::Usage) {
u.modifications.push(usage::Modification { u.modifications.push(usage::Modification {
syntax: "DESCRIPTION", syntax: "DESCRIPTION",
@ -122,14 +138,25 @@ impl Modification {
u.modifications.push(usage::Modification { u.modifications.push(usage::Modification {
syntax: "+TAG", syntax: "+TAG",
summary: "Tag task", summary: "Tag task",
description: " description: "Add the given tag to the task.",
Add the given tag to the task.",
}); });
u.modifications.push(usage::Modification { u.modifications.push(usage::Modification {
syntax: "-TAG", syntax: "-TAG",
summary: "Un-tag task", 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:<timestamp>",
summary: "Set or unset the task's wait time",
description: " 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::argparse::NOW;
#[test] #[test]
fn test_empty() { 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] #[test]
fn test_multi_arg_description() { fn test_multi_arg_description() {
let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap(); let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap();

View file

@ -40,5 +40,9 @@ pub(super) fn apply_modification(
task.remove_tag(&tag)?; task.remove_tag(&tag)?;
} }
if let Some(wait) = modification.wait {
task.set_wait(wait)?;
}
Ok(()) Ok(())
} }

View file

@ -32,6 +32,7 @@ The following keys, and key formats, are defined:
* `modified` - the time of the last modification of this task * `modified` - the time of the last modification of this task
* `start.<timestamp>` - 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) * `start.<timestamp>` - 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.<tag>` - indicates this task has tag `<tag>` (value is an empty string) * `tag.<tag>` - indicates this task has tag `<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: The following are not yet implemented:

View file

@ -211,6 +211,20 @@ impl Task {
.unwrap_or("") .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<DateTime<Utc>> {
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 /// Determine whether this task is active -- that is, that it has been started
/// and not stopped. /// and not stopped.
pub fn is_active(&self) -> bool { pub fn is_active(&self) -> bool {
@ -275,6 +289,10 @@ impl<'r> TaskMut<'r> {
self.set_string("description", Some(description)) self.set_string("description", Some(description))
} }
pub fn set_wait(&mut self, wait: Option<DateTime<Utc>>) -> anyhow::Result<()> {
self.set_timestamp("wait", wait)
}
pub fn set_modified(&mut self, modified: DateTime<Utc>) -> anyhow::Result<()> { pub fn set_modified(&mut self, modified: DateTime<Utc>) -> anyhow::Result<()> {
self.set_timestamp("modified", Some(modified)) self.set_timestamp("modified", Some(modified))
} }
@ -452,6 +470,43 @@ mod test {
assert!(!task.is_active()); 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] #[test]
fn test_has_tag() { fn test_has_tag() {
let task = Task::new( let task = Task::new(