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

View file

@ -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 }

View file

@ -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<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
pub(super) fn id_list(input: &str) -> IResult<&str, Vec<TaskId>> {
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");

View file

@ -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> = Utc::now();
}
pub(crate) fn get_usage(usage: &mut Usage) {
Subcommand::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 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<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.
pub active: Option<bool>,
@ -51,6 +55,7 @@ enum ModArg<'a> {
Description(&'a str),
PlusTag(&'a str),
MinusTag(&'a str),
Wait(Option<DateTime<Utc>>),
}
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<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) {
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:<timestamp>",
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();

View file

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