mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-08-23 05:27:47 +02:00
Merge pull request #114 from djmitche/issue11
Support filtering by tags
This commit is contained in:
commit
c75d07df94
7 changed files with 281 additions and 15 deletions
|
@ -1,6 +1,8 @@
|
||||||
use super::args::{arg_matching, id_list, TaskId};
|
use super::args::{arg_matching, id_list, minus_tag, plus_tag, TaskId};
|
||||||
use super::ArgList;
|
use super::ArgList;
|
||||||
use nom::{combinator::*, multi::fold_many0, IResult};
|
use crate::usage;
|
||||||
|
use nom::{branch::alt, combinator::*, multi::fold_many0, IResult};
|
||||||
|
use textwrap::dedent;
|
||||||
|
|
||||||
/// A filter represents a selection of a particular set of tasks.
|
/// A filter represents a selection of a particular set of tasks.
|
||||||
///
|
///
|
||||||
|
@ -9,8 +11,12 @@ use nom::{combinator::*, multi::fold_many0, IResult};
|
||||||
/// pending tasks, or all tasks.
|
/// pending tasks, or all tasks.
|
||||||
#[derive(Debug, PartialEq, Default, Clone)]
|
#[derive(Debug, PartialEq, Default, Clone)]
|
||||||
pub(crate) struct Filter {
|
pub(crate) struct Filter {
|
||||||
/// A list of numeric IDs or prefixes of UUIDs
|
/// The universe of tasks from which this filter can select
|
||||||
pub(crate) universe: Universe,
|
pub(crate) universe: Universe,
|
||||||
|
|
||||||
|
/// A set of filter conditions, all of which must match a task in order for that task to be
|
||||||
|
/// selected.
|
||||||
|
pub(crate) conditions: Vec<Condition>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The universe of tasks over which a filter should be applied.
|
/// The universe of tasks over which a filter should be applied.
|
||||||
|
@ -39,15 +45,26 @@ impl Default for Universe {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A condition which tasks must match to be accepted by the filter.
|
||||||
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
pub(crate) enum Condition {
|
||||||
|
/// Task has the given tag
|
||||||
|
HasTag(String),
|
||||||
|
|
||||||
|
/// Task does not have the given tag
|
||||||
|
NoTag(String),
|
||||||
|
}
|
||||||
|
|
||||||
/// Internal struct representing a parsed filter argument
|
/// Internal struct representing a parsed filter argument
|
||||||
enum FilterArg {
|
enum FilterArg {
|
||||||
IdList(Vec<TaskId>),
|
IdList(Vec<TaskId>),
|
||||||
|
Condition(Condition),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Filter {
|
impl Filter {
|
||||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Filter> {
|
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Filter> {
|
||||||
fold_many0(
|
fold_many0(
|
||||||
Self::id_list,
|
alt((Self::id_list, Self::plus_tag, Self::minus_tag)),
|
||||||
Filter {
|
Filter {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
@ -68,6 +85,9 @@ impl Filter {
|
||||||
acc.universe = Universe::IdList(id_list);
|
acc.universe = Universe::IdList(id_list);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
FilterArg::Condition(cond) => {
|
||||||
|
acc.conditions.push(cond);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
acc
|
acc
|
||||||
}
|
}
|
||||||
|
@ -78,6 +98,50 @@ impl Filter {
|
||||||
}
|
}
|
||||||
map_res(arg_matching(id_list), to_filterarg)(input)
|
map_res(arg_matching(id_list), to_filterarg)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn plus_tag(input: ArgList) -> IResult<ArgList, FilterArg> {
|
||||||
|
fn to_filterarg(input: &str) -> Result<FilterArg, ()> {
|
||||||
|
Ok(FilterArg::Condition(Condition::HasTag(input.to_owned())))
|
||||||
|
}
|
||||||
|
map_res(arg_matching(plus_tag), to_filterarg)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minus_tag(input: ArgList) -> IResult<ArgList, FilterArg> {
|
||||||
|
fn to_filterarg(input: &str) -> Result<FilterArg, ()> {
|
||||||
|
Ok(FilterArg::Condition(Condition::NoTag(input.to_owned())))
|
||||||
|
}
|
||||||
|
map_res(arg_matching(minus_tag), to_filterarg)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_usage(u: &mut usage::Usage) {
|
||||||
|
u.filters.push(usage::Filter {
|
||||||
|
syntax: "TASKID[,TASKID,..]".to_owned(),
|
||||||
|
summary: "Specific tasks".to_owned(),
|
||||||
|
description: dedent(
|
||||||
|
"
|
||||||
|
Select only specific tasks. Multiple tasks can be specified either separated by
|
||||||
|
commas or as separate arguments. Each task may be specfied by its working-set
|
||||||
|
index (a small number) or by its UUID. Prefixes of UUIDs broken at hyphens are
|
||||||
|
also supported, such as `b5664ef8-423d` or `b5664ef8`.",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
u.filters.push(usage::Filter {
|
||||||
|
syntax: "+TAG".to_owned(),
|
||||||
|
summary: "Tagged tasks".to_owned(),
|
||||||
|
description: dedent(
|
||||||
|
"
|
||||||
|
Select tasks with the given tag.",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
u.filters.push(usage::Filter {
|
||||||
|
syntax: "-TAG".to_owned(),
|
||||||
|
summary: "Un-tagged tasks".to_owned(),
|
||||||
|
description: dedent(
|
||||||
|
"
|
||||||
|
Select tasks that do not have the given tag.",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -159,4 +223,21 @@ mod test {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tags() {
|
||||||
|
let (input, filter) = Filter::parse(argv!["1", "+yes", "-no"]).unwrap();
|
||||||
|
assert_eq!(input.len(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
filter,
|
||||||
|
Filter {
|
||||||
|
universe: Universe::IdList(vec![TaskId::WorkingSetId(1),]),
|
||||||
|
conditions: vec![
|
||||||
|
Condition::HasTag("yes".into()),
|
||||||
|
Condition::NoTag("no".into()),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ mod subcommand;
|
||||||
|
|
||||||
pub(crate) use args::TaskId;
|
pub(crate) use args::TaskId;
|
||||||
pub(crate) use command::Command;
|
pub(crate) use command::Command;
|
||||||
pub(crate) use filter::{Filter, Universe};
|
pub(crate) use filter::{Condition, Filter, Universe};
|
||||||
pub(crate) use modification::{DescriptionMod, Modification};
|
pub(crate) use modification::{DescriptionMod, Modification};
|
||||||
pub(crate) use report::Report;
|
pub(crate) use report::Report;
|
||||||
pub(crate) use subcommand::Subcommand;
|
pub(crate) use subcommand::Subcommand;
|
||||||
|
@ -31,4 +31,6 @@ type ArgList<'a> = &'a [&'a str];
|
||||||
|
|
||||||
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);
|
||||||
|
Modification::get_usage(usage);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use super::args::{any, arg_matching, minus_tag, plus_tag};
|
use super::args::{any, arg_matching, minus_tag, plus_tag};
|
||||||
use super::ArgList;
|
use super::ArgList;
|
||||||
|
use crate::usage;
|
||||||
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;
|
||||||
|
use textwrap::dedent;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub enum DescriptionMod {
|
pub enum DescriptionMod {
|
||||||
|
@ -107,6 +109,34 @@ impl Modification {
|
||||||
}
|
}
|
||||||
map_res(arg_matching(minus_tag), to_modarg)(input)
|
map_res(arg_matching(minus_tag), to_modarg)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn get_usage(u: &mut usage::Usage) {
|
||||||
|
u.modifications.push(usage::Modification {
|
||||||
|
syntax: "DESCRIPTION".to_owned(),
|
||||||
|
summary: "Set description".to_owned(),
|
||||||
|
description: dedent(
|
||||||
|
"
|
||||||
|
Set the task description. Multiple arguments are combined into a single
|
||||||
|
space-separated description.",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
u.modifications.push(usage::Modification {
|
||||||
|
syntax: "+TAG".to_owned(),
|
||||||
|
summary: "Tag task".to_owned(),
|
||||||
|
description: dedent(
|
||||||
|
"
|
||||||
|
Add the given tag to the task.",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
u.modifications.push(usage::Modification {
|
||||||
|
syntax: "-TAG".to_owned(),
|
||||||
|
summary: "Un-tag task".to_owned(),
|
||||||
|
description: dedent(
|
||||||
|
"
|
||||||
|
Remove the given tag from the task.",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -243,7 +243,7 @@ impl Modify {
|
||||||
});
|
});
|
||||||
u.subcommands.push(usage::Subcommand {
|
u.subcommands.push(usage::Subcommand {
|
||||||
name: "stop".to_owned(),
|
name: "stop".to_owned(),
|
||||||
syntax: "[filter] start [modification]".to_owned(),
|
syntax: "[filter] stop [modification]".to_owned(),
|
||||||
summary: "Stop tasks".to_owned(),
|
summary: "Stop tasks".to_owned(),
|
||||||
description: dedent(
|
description: dedent(
|
||||||
"
|
"
|
||||||
|
@ -252,7 +252,7 @@ impl Modify {
|
||||||
});
|
});
|
||||||
u.subcommands.push(usage::Subcommand {
|
u.subcommands.push(usage::Subcommand {
|
||||||
name: "done".to_owned(),
|
name: "done".to_owned(),
|
||||||
syntax: "[filter] start [modification]".to_owned(),
|
syntax: "[filter] done [modification]".to_owned(),
|
||||||
summary: "Mark tasks as completed".to_owned(),
|
summary: "Mark tasks as completed".to_owned(),
|
||||||
description: dedent(
|
description: dedent(
|
||||||
"
|
"
|
||||||
|
|
|
@ -1,10 +1,28 @@
|
||||||
use crate::argparse::{Filter, TaskId, Universe};
|
use crate::argparse::{Condition, Filter, TaskId, Universe};
|
||||||
use failure::Fallible;
|
use failure::Fallible;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use taskchampion::{Replica, Task};
|
use std::convert::TryInto;
|
||||||
|
use taskchampion::{Replica, Tag, Task};
|
||||||
|
|
||||||
fn match_task(_filter: &Filter, _task: &Task) -> bool {
|
fn match_task(filter: &Filter, task: &Task) -> bool {
|
||||||
// TODO: at the moment, only filtering by Universe is supported
|
for cond in &filter.conditions {
|
||||||
|
match cond {
|
||||||
|
Condition::HasTag(ref tag) => {
|
||||||
|
// see #111 for the unwrap
|
||||||
|
let tag: Tag = tag.try_into().unwrap();
|
||||||
|
if !task.has_tag(&tag) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Condition::NoTag(ref tag) => {
|
||||||
|
// see #111 for the unwrap
|
||||||
|
let tag: Tag = tag.try_into().unwrap();
|
||||||
|
if task.has_tag(&tag) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +204,68 @@ mod test {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tag_filtering() -> Fallible<()> {
|
||||||
|
let mut replica = test_replica();
|
||||||
|
let yes: Tag = "yes".try_into()?;
|
||||||
|
let no: Tag = "no".try_into()?;
|
||||||
|
|
||||||
|
let mut t1 = replica
|
||||||
|
.new_task(Status::Pending, "A".to_owned())?
|
||||||
|
.into_mut(&mut replica);
|
||||||
|
t1.add_tag(&yes)?;
|
||||||
|
let mut t2 = replica
|
||||||
|
.new_task(Status::Pending, "B".to_owned())?
|
||||||
|
.into_mut(&mut replica);
|
||||||
|
t2.add_tag(&yes)?;
|
||||||
|
t2.add_tag(&no)?;
|
||||||
|
let mut t3 = replica
|
||||||
|
.new_task(Status::Pending, "C".to_owned())?
|
||||||
|
.into_mut(&mut replica);
|
||||||
|
t3.add_tag(&no)?;
|
||||||
|
let _t4 = replica.new_task(Status::Pending, "D".to_owned())?;
|
||||||
|
|
||||||
|
// look for just "yes" (A and B)
|
||||||
|
let filter = Filter {
|
||||||
|
universe: Universe::AllTasks,
|
||||||
|
conditions: vec![Condition::HasTag("yes".to_owned())],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)?
|
||||||
|
.map(|t| t.get_description().to_owned())
|
||||||
|
.collect();
|
||||||
|
filtered.sort();
|
||||||
|
assert_eq!(vec!["A".to_owned(), "B".to_owned()], filtered);
|
||||||
|
|
||||||
|
// look for tags without "no" (A, D)
|
||||||
|
let filter = Filter {
|
||||||
|
universe: Universe::AllTasks,
|
||||||
|
conditions: vec![Condition::NoTag("no".to_owned())],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)?
|
||||||
|
.map(|t| t.get_description().to_owned())
|
||||||
|
.collect();
|
||||||
|
filtered.sort();
|
||||||
|
assert_eq!(vec!["A".to_owned(), "D".to_owned()], filtered);
|
||||||
|
|
||||||
|
// look for tags with "yes" and "no" (B)
|
||||||
|
let filter = Filter {
|
||||||
|
universe: Universe::AllTasks,
|
||||||
|
conditions: vec![
|
||||||
|
Condition::HasTag("yes".to_owned()),
|
||||||
|
Condition::HasTag("no".to_owned()),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let filtered: Vec<_> = filtered_tasks(&mut replica, &filter)?
|
||||||
|
.map(|t| t.get_description().to_owned())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(vec!["B".to_owned()], filtered);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pending_tasks() {
|
fn pending_tasks() {
|
||||||
let mut replica = test_replica();
|
let mut replica = test_replica();
|
||||||
|
|
|
@ -34,14 +34,12 @@ pub(super) fn apply_modification<W: WriteColor>(
|
||||||
}
|
}
|
||||||
|
|
||||||
for tag in modification.add_tags.iter() {
|
for tag in modification.add_tags.iter() {
|
||||||
// note that the parser should have already ensured that this tag was valid
|
let tag = tag.try_into()?; // see #111
|
||||||
let tag = tag.try_into()?;
|
|
||||||
task.add_tag(&tag)?;
|
task.add_tag(&tag)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for tag in modification.remove_tags.iter() {
|
for tag in modification.remove_tags.iter() {
|
||||||
// note that the parser should have already ensured that this tag was valid
|
let tag = tag.try_into()?; // see #111
|
||||||
let tag = tag.try_into()?;
|
|
||||||
task.remove_tag(&tag)?;
|
task.remove_tag(&tag)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@ use textwrap::indent;
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub(crate) struct Usage {
|
pub(crate) struct Usage {
|
||||||
pub(crate) subcommands: Vec<Subcommand>,
|
pub(crate) subcommands: Vec<Subcommand>,
|
||||||
|
pub(crate) filters: Vec<Filter>,
|
||||||
|
pub(crate) modifications: Vec<Modification>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Usage {
|
impl Usage {
|
||||||
|
@ -43,6 +45,18 @@ impl Usage {
|
||||||
for subcommand in self.subcommands.iter() {
|
for subcommand in self.subcommands.iter() {
|
||||||
subcommand.write_help(&mut w, summary)?;
|
subcommand.write_help(&mut w, summary)?;
|
||||||
}
|
}
|
||||||
|
write!(w, "Filter Expressions:\n\n")?;
|
||||||
|
write!(w, "Where [filter] appears above, zero or more of the following arguments can be used to limit\n")?;
|
||||||
|
write!(w, "the tasks concerned.\n\n")?;
|
||||||
|
for filter in self.filters.iter() {
|
||||||
|
filter.write_help(&mut w, summary)?;
|
||||||
|
}
|
||||||
|
write!(w, "Modifications:\n\n")?;
|
||||||
|
write!(w, "Where [modification] appears above, zero or more of the following arguments can be used\n")?;
|
||||||
|
write!(w, "to modify the selected tasks.\n\n")?;
|
||||||
|
for modification in self.modifications.iter() {
|
||||||
|
modification.write_help(&mut w, summary)?;
|
||||||
|
}
|
||||||
if !summary {
|
if !summary {
|
||||||
write!(w, "\nSee `task help` for more detail\n")?;
|
write!(w, "\nSee `task help` for more detail\n")?;
|
||||||
}
|
}
|
||||||
|
@ -50,6 +64,7 @@ impl Usage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Usage documentation for a subcommand
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub(crate) struct Subcommand {
|
pub(crate) struct Subcommand {
|
||||||
/// Name of the subcommand
|
/// Name of the subcommand
|
||||||
|
@ -81,3 +96,63 @@ impl Subcommand {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Usage documentation for a filter argument
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct Filter {
|
||||||
|
/// Syntax summary
|
||||||
|
pub(crate) syntax: String,
|
||||||
|
|
||||||
|
/// One-line description of the filter. Use all-caps words for placeholders.
|
||||||
|
pub(crate) summary: String,
|
||||||
|
|
||||||
|
/// Multi-line description of the filter. It's OK for this to duplicate summary, as the
|
||||||
|
/// two are not displayed together.
|
||||||
|
pub(crate) description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Filter {
|
||||||
|
fn write_help<W: Write>(&self, mut w: W, summary: bool) -> Result<()> {
|
||||||
|
if summary {
|
||||||
|
write!(w, " {} - {}\n", self.syntax, self.summary)?;
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
w,
|
||||||
|
" {}\n{}\n",
|
||||||
|
self.syntax,
|
||||||
|
indent(self.description.trim(), " ")
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Usage documentation for a modification argument
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub(crate) struct Modification {
|
||||||
|
/// Syntax summary
|
||||||
|
pub(crate) syntax: String,
|
||||||
|
|
||||||
|
/// One-line description of the modification. Use all-caps words for placeholders.
|
||||||
|
pub(crate) summary: String,
|
||||||
|
|
||||||
|
/// Multi-line description of the modification. It's OK for this to duplicate summary, as the
|
||||||
|
/// two are not displayed together.
|
||||||
|
pub(crate) description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Modification {
|
||||||
|
fn write_help<W: Write>(&self, mut w: W, summary: bool) -> Result<()> {
|
||||||
|
if summary {
|
||||||
|
write!(w, " {} - {}\n", self.syntax, self.summary)?;
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
w,
|
||||||
|
" {}\n{}\n",
|
||||||
|
self.syntax,
|
||||||
|
indent(self.description.trim(), " ")
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue