@@ -10,7 +10,7 @@ use nom::{
|
||||
sequence::*,
|
||||
Err, IResult,
|
||||
};
|
||||
use taskchampion::Uuid;
|
||||
use taskchampion::{Status, Uuid};
|
||||
|
||||
/// A task identifier, as given in a filter command-line expression
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
@@ -30,11 +30,42 @@ pub(super) fn any(input: &str) -> IResult<&str, &str> {
|
||||
rest(input)
|
||||
}
|
||||
|
||||
/// Recognizes a report name
|
||||
pub(super) fn report_name(input: &str) -> IResult<&str, &str> {
|
||||
all_consuming(recognize(pair(alpha1, alphanumeric0)))(input)
|
||||
}
|
||||
|
||||
/// Recognizes a literal string
|
||||
pub(super) fn literal(literal: &'static str) -> impl Fn(&str) -> IResult<&str, &str> {
|
||||
move |input: &str| all_consuming(nomtag(literal))(input)
|
||||
}
|
||||
|
||||
/// Recognizes a colon-prefixed pair
|
||||
pub(super) fn colon_prefixed(prefix: &'static str) -> impl Fn(&str) -> IResult<&str, &str> {
|
||||
fn to_suffix<'a>(input: (&'a str, char, &'a str)) -> Result<&'a str, ()> {
|
||||
Ok(input.2)
|
||||
}
|
||||
move |input: &str| {
|
||||
map_res(
|
||||
all_consuming(tuple((nomtag(prefix), char(':'), any))),
|
||||
to_suffix,
|
||||
)(input)
|
||||
}
|
||||
}
|
||||
|
||||
/// Recognizes `status:{pending,completed,deleted}`
|
||||
pub(super) fn status_colon(input: &str) -> IResult<&str, Status> {
|
||||
fn to_status(input: &str) -> Result<Status, ()> {
|
||||
match input {
|
||||
"pending" => Ok(Status::Pending),
|
||||
"completed" => Ok(Status::Completed),
|
||||
"deleted" => Ok(Status::Deleted),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
map_res(colon_prefixed("status"), to_status)(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> {
|
||||
@@ -159,6 +190,26 @@ mod test {
|
||||
assert!(arg_matching(plus_tag)(argv!["foo", "bar"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_colon_prefixed() {
|
||||
assert_eq!(colon_prefixed("foo")("foo:abc").unwrap().1, "abc");
|
||||
assert_eq!(colon_prefixed("foo")("foo:").unwrap().1, "");
|
||||
assert!(colon_prefixed("foo")("foo").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status_colon() {
|
||||
assert_eq!(status_colon("status:pending").unwrap().1, Status::Pending);
|
||||
assert_eq!(
|
||||
status_colon("status:completed").unwrap().1,
|
||||
Status::Completed
|
||||
);
|
||||
assert_eq!(status_colon("status:deleted").unwrap().1, Status::Deleted);
|
||||
assert!(status_colon("status:foo").is_err());
|
||||
assert!(status_colon("status:complete").is_err());
|
||||
assert!(status_colon("status").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plus_tag() {
|
||||
assert_eq!(plus_tag("+abc").unwrap().1, "abc");
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use super::args::{arg_matching, id_list, minus_tag, plus_tag, TaskId};
|
||||
use super::args::{arg_matching, id_list, minus_tag, plus_tag, status_colon, TaskId};
|
||||
use super::ArgList;
|
||||
use crate::usage;
|
||||
use nom::{branch::alt, combinator::*, multi::fold_many0, IResult};
|
||||
use taskchampion::Status;
|
||||
|
||||
/// A filter represents a selection of a particular set of tasks.
|
||||
///
|
||||
@@ -10,40 +11,11 @@ use nom::{branch::alt, combinator::*, multi::fold_many0, IResult};
|
||||
/// pending tasks, or all tasks.
|
||||
#[derive(Debug, PartialEq, Default, Clone)]
|
||||
pub(crate) struct Filter {
|
||||
/// The universe of tasks from which this filter can select
|
||||
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.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub(crate) enum Universe {
|
||||
/// Only the identified tasks. Note that this may contain duplicates.
|
||||
IdList(Vec<TaskId>),
|
||||
/// All tasks in the task database
|
||||
AllTasks,
|
||||
/// Only pending tasks (or as an approximation, the working set)
|
||||
#[allow(dead_code)] // currently only used in tests
|
||||
PendingTasks,
|
||||
}
|
||||
|
||||
impl Universe {
|
||||
/// Testing shorthand to construct a simple universe
|
||||
#[cfg(test)]
|
||||
pub(super) fn for_ids(mut ids: Vec<usize>) -> Self {
|
||||
Universe::IdList(ids.drain(..).map(|id| TaskId::WorkingSetId(id)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Universe {
|
||||
fn default() -> Self {
|
||||
Self::AllTasks
|
||||
}
|
||||
}
|
||||
|
||||
/// A condition which tasks must match to be accepted by the filter.
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub(crate) enum Condition {
|
||||
@@ -52,66 +24,94 @@ pub(crate) enum Condition {
|
||||
|
||||
/// Task does not have the given tag
|
||||
NoTag(String),
|
||||
}
|
||||
|
||||
/// Internal struct representing a parsed filter argument
|
||||
enum FilterArg {
|
||||
/// Task has the given status
|
||||
Status(Status),
|
||||
|
||||
/// Task has one of the given IDs
|
||||
IdList(Vec<TaskId>),
|
||||
Condition(Condition),
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Filter> {
|
||||
fold_many0(
|
||||
alt((Self::id_list, Self::plus_tag, Self::minus_tag)),
|
||||
alt((
|
||||
Self::parse_id_list,
|
||||
Self::parse_plus_tag,
|
||||
Self::parse_minus_tag,
|
||||
Self::parse_status,
|
||||
)),
|
||||
Filter {
|
||||
..Default::default()
|
||||
},
|
||||
Self::fold_args,
|
||||
|acc, arg| acc.with_arg(arg),
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// fold multiple filter args into a single Filter instance
|
||||
fn fold_args(mut acc: Filter, mod_arg: FilterArg) -> Filter {
|
||||
match mod_arg {
|
||||
FilterArg::IdList(mut id_list) => {
|
||||
// If any IDs are specified, then the filter's universe
|
||||
// is those IDs. If there are already IDs, append to the
|
||||
// list.
|
||||
if let Universe::IdList(ref mut existing) = acc.universe {
|
||||
existing.append(&mut id_list);
|
||||
} else {
|
||||
acc.universe = Universe::IdList(id_list);
|
||||
}
|
||||
}
|
||||
FilterArg::Condition(cond) => {
|
||||
acc.conditions.push(cond);
|
||||
fn with_arg(mut self, cond: Condition) -> Filter {
|
||||
if let Condition::IdList(mut id_list) = cond {
|
||||
// If there is already an IdList condition, concatenate this one
|
||||
// to it. Thus multiple IdList command-line args represent an OR
|
||||
// operation. This assumes that the filter is still being built
|
||||
// from command-line arguments and thus has at most one IdList
|
||||
// condition.
|
||||
if let Some(Condition::IdList(existing)) = self
|
||||
.conditions
|
||||
.iter_mut()
|
||||
.find(|c| matches!(c, Condition::IdList(_)))
|
||||
{
|
||||
existing.append(&mut id_list);
|
||||
} else {
|
||||
self.conditions.push(Condition::IdList(id_list));
|
||||
}
|
||||
} else {
|
||||
// all other command-line conditions are AND'd together
|
||||
self.conditions.push(cond);
|
||||
}
|
||||
acc
|
||||
self
|
||||
}
|
||||
|
||||
fn id_list(input: ArgList) -> IResult<ArgList, FilterArg> {
|
||||
fn to_filterarg(input: Vec<TaskId>) -> Result<FilterArg, ()> {
|
||||
Ok(FilterArg::IdList(input))
|
||||
}
|
||||
map_res(arg_matching(id_list), to_filterarg)(input)
|
||||
/// combine this filter with another filter in an AND operation
|
||||
pub(crate) fn intersect(mut self, mut other: Filter) -> Filter {
|
||||
// simply concatenate the conditions
|
||||
self.conditions.append(&mut other.conditions);
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
fn plus_tag(input: ArgList) -> IResult<ArgList, FilterArg> {
|
||||
fn to_filterarg(input: &str) -> Result<FilterArg, ()> {
|
||||
Ok(FilterArg::Condition(Condition::HasTag(input.to_owned())))
|
||||
// parsers
|
||||
|
||||
fn parse_id_list(input: ArgList) -> IResult<ArgList, Condition> {
|
||||
fn to_condition(input: Vec<TaskId>) -> Result<Condition, ()> {
|
||||
Ok(Condition::IdList(input))
|
||||
}
|
||||
map_res(arg_matching(plus_tag), to_filterarg)(input)
|
||||
map_res(arg_matching(id_list), to_condition)(input)
|
||||
}
|
||||
|
||||
fn minus_tag(input: ArgList) -> IResult<ArgList, FilterArg> {
|
||||
fn to_filterarg(input: &str) -> Result<FilterArg, ()> {
|
||||
Ok(FilterArg::Condition(Condition::NoTag(input.to_owned())))
|
||||
fn parse_plus_tag(input: ArgList) -> IResult<ArgList, Condition> {
|
||||
fn to_condition(input: &str) -> Result<Condition, ()> {
|
||||
Ok(Condition::HasTag(input.to_owned()))
|
||||
}
|
||||
map_res(arg_matching(minus_tag), to_filterarg)(input)
|
||||
map_res(arg_matching(plus_tag), to_condition)(input)
|
||||
}
|
||||
|
||||
fn parse_minus_tag(input: ArgList) -> IResult<ArgList, Condition> {
|
||||
fn to_condition(input: &str) -> Result<Condition, ()> {
|
||||
Ok(Condition::NoTag(input.to_owned()))
|
||||
}
|
||||
map_res(arg_matching(minus_tag), to_condition)(input)
|
||||
}
|
||||
|
||||
fn parse_status(input: ArgList) -> IResult<ArgList, Condition> {
|
||||
fn to_condition(input: Status) -> Result<Condition, ()> {
|
||||
Ok(Condition::Status(input))
|
||||
}
|
||||
map_res(arg_matching(status_colon), to_condition)(input)
|
||||
}
|
||||
|
||||
// usage
|
||||
|
||||
pub(super) fn get_usage(u: &mut usage::Usage) {
|
||||
u.filters.push(usage::Filter {
|
||||
syntax: "TASKID[,TASKID,..]",
|
||||
@@ -134,6 +134,12 @@ impl Filter {
|
||||
description: "
|
||||
Select tasks that do not have the given tag.",
|
||||
});
|
||||
u.filters.push(usage::Filter {
|
||||
syntax: "status:pending, status:completed, status:deleted",
|
||||
summary: "Task status",
|
||||
description: "
|
||||
Select tasks with the given status.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,8 +166,7 @@ mod test {
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
universe: Universe::IdList(vec![TaskId::WorkingSetId(1)]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(1)])],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -173,12 +178,11 @@ mod test {
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
universe: Universe::IdList(vec![
|
||||
conditions: vec![Condition::IdList(vec![
|
||||
TaskId::WorkingSetId(1),
|
||||
TaskId::WorkingSetId(2),
|
||||
TaskId::WorkingSetId(3),
|
||||
]),
|
||||
..Default::default()
|
||||
])],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -190,13 +194,12 @@ mod test {
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
universe: Universe::IdList(vec![
|
||||
conditions: vec![Condition::IdList(vec![
|
||||
TaskId::WorkingSetId(1),
|
||||
TaskId::WorkingSetId(2),
|
||||
TaskId::WorkingSetId(3),
|
||||
TaskId::WorkingSetId(4),
|
||||
]),
|
||||
..Default::default()
|
||||
])],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -208,11 +211,10 @@ mod test {
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
universe: Universe::IdList(vec![
|
||||
conditions: vec![Condition::IdList(vec![
|
||||
TaskId::WorkingSetId(1),
|
||||
TaskId::PartialUuid(s!("abcd1234")),
|
||||
]),
|
||||
..Default::default()
|
||||
])],
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -224,12 +226,81 @@ mod test {
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
universe: Universe::IdList(vec![TaskId::WorkingSetId(1),]),
|
||||
conditions: vec![
|
||||
Condition::IdList(vec![TaskId::WorkingSetId(1),]),
|
||||
Condition::HasTag("yes".into()),
|
||||
Condition::NoTag("no".into()),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status() {
|
||||
let (input, filter) = Filter::parse(argv!["status:completed", "status:pending"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
conditions: vec![
|
||||
Condition::Status(Status::Completed),
|
||||
Condition::Status(Status::Pending),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_idlist_idlist() {
|
||||
let left = Filter::parse(argv!["1,2", "+yes"]).unwrap().1;
|
||||
let right = Filter::parse(argv!["2,3", "+no"]).unwrap().1;
|
||||
let both = left.intersect(right);
|
||||
assert_eq!(
|
||||
both,
|
||||
Filter {
|
||||
conditions: vec![
|
||||
// from first filter
|
||||
Condition::IdList(vec![TaskId::WorkingSetId(1), TaskId::WorkingSetId(2),]),
|
||||
Condition::HasTag("yes".into()),
|
||||
// from second filter
|
||||
Condition::IdList(vec![TaskId::WorkingSetId(2), TaskId::WorkingSetId(3)]),
|
||||
Condition::HasTag("no".into()),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_idlist_alltasks() {
|
||||
let left = Filter::parse(argv!["1,2", "+yes"]).unwrap().1;
|
||||
let right = Filter::parse(argv!["+no"]).unwrap().1;
|
||||
let both = left.intersect(right);
|
||||
assert_eq!(
|
||||
both,
|
||||
Filter {
|
||||
conditions: vec![
|
||||
// from first filter
|
||||
Condition::IdList(vec![TaskId::WorkingSetId(1), TaskId::WorkingSetId(2),]),
|
||||
Condition::HasTag("yes".into()),
|
||||
// from second filter
|
||||
Condition::HasTag("no".into()),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_alltasks_alltasks() {
|
||||
let left = Filter::parse(argv!["+yes"]).unwrap().1;
|
||||
let right = Filter::parse(argv!["+no"]).unwrap().1;
|
||||
let both = left.intersect(right);
|
||||
assert_eq!(
|
||||
both,
|
||||
Filter {
|
||||
conditions: vec![
|
||||
Condition::HasTag("yes".into()),
|
||||
Condition::HasTag("no".into()),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,14 +15,12 @@ mod args;
|
||||
mod command;
|
||||
mod filter;
|
||||
mod modification;
|
||||
mod report;
|
||||
mod subcommand;
|
||||
|
||||
pub(crate) use args::TaskId;
|
||||
pub(crate) use command::Command;
|
||||
pub(crate) use filter::{Condition, Filter, Universe};
|
||||
pub(crate) use filter::{Condition, Filter};
|
||||
pub(crate) use modification::{DescriptionMod, Modification};
|
||||
pub(crate) use report::{Column, Property, Report, Sort, SortBy};
|
||||
pub(crate) use subcommand::Subcommand;
|
||||
|
||||
use crate::usage::Usage;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use super::args::*;
|
||||
use super::{
|
||||
ArgList, Column, DescriptionMod, Filter, Modification, Property, Report, Sort, SortBy,
|
||||
};
|
||||
use super::{ArgList, DescriptionMod, Filter, Modification};
|
||||
use crate::usage;
|
||||
use nom::{branch::alt, combinator::*, sequence::*, IResult};
|
||||
use taskchampion::Status;
|
||||
@@ -39,8 +37,12 @@ pub(crate) enum Subcommand {
|
||||
},
|
||||
|
||||
/// Lists (reports)
|
||||
List {
|
||||
report: Report,
|
||||
Report {
|
||||
/// The name of the report to show
|
||||
report_name: String,
|
||||
|
||||
/// Additional filter terms beyond those in the report
|
||||
filter: Filter,
|
||||
},
|
||||
|
||||
/// Per-task information (typically one task)
|
||||
@@ -56,16 +58,17 @@ pub(crate) enum Subcommand {
|
||||
|
||||
impl Subcommand {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
alt((
|
||||
all_consuming(alt((
|
||||
Version::parse,
|
||||
Help::parse,
|
||||
Add::parse,
|
||||
Modify::parse,
|
||||
List::parse,
|
||||
Info::parse,
|
||||
Gc::parse,
|
||||
Sync::parse,
|
||||
))(input)
|
||||
// This must come last since it accepts arbitrary report names
|
||||
Report::parse,
|
||||
)))(input)
|
||||
}
|
||||
|
||||
pub(super) fn get_usage(u: &mut usage::Usage) {
|
||||
@@ -73,10 +76,10 @@ impl Subcommand {
|
||||
Help::get_usage(u);
|
||||
Add::get_usage(u);
|
||||
Modify::get_usage(u);
|
||||
List::get_usage(u);
|
||||
Info::get_usage(u);
|
||||
Gc::get_usage(u);
|
||||
Sync::get_usage(u);
|
||||
Report::get_usage(u);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,59 +254,43 @@ impl Modify {
|
||||
}
|
||||
}
|
||||
|
||||
struct List;
|
||||
|
||||
impl List {
|
||||
// temporary
|
||||
fn default_report() -> Report {
|
||||
Report {
|
||||
columns: vec![
|
||||
Column {
|
||||
label: "Id".to_owned(),
|
||||
property: Property::Id,
|
||||
},
|
||||
Column {
|
||||
label: "Description".to_owned(),
|
||||
property: Property::Description,
|
||||
},
|
||||
Column {
|
||||
label: "Active".to_owned(),
|
||||
property: Property::Active,
|
||||
},
|
||||
Column {
|
||||
label: "Tags".to_owned(),
|
||||
property: Property::Tags,
|
||||
},
|
||||
],
|
||||
sort: vec![Sort {
|
||||
ascending: false,
|
||||
sort_by: SortBy::Uuid,
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
struct Report;
|
||||
|
||||
impl Report {
|
||||
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: (Filter, &str)) -> Result<Subcommand, ()> {
|
||||
let report = Report {
|
||||
filter: input.0,
|
||||
..List::default_report()
|
||||
};
|
||||
Ok(Subcommand::List { report })
|
||||
fn to_subcommand(filter: Filter, report_name: &str) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Report {
|
||||
filter,
|
||||
report_name: report_name.to_owned(),
|
||||
})
|
||||
}
|
||||
map_res(
|
||||
pair(Filter::parse, arg_matching(literal("list"))),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
// allow the filter expression before or after the report name
|
||||
alt((
|
||||
map_res(pair(arg_matching(report_name), Filter::parse), |input| {
|
||||
to_subcommand(input.1, input.0)
|
||||
}),
|
||||
map_res(pair(Filter::parse, arg_matching(report_name)), |input| {
|
||||
to_subcommand(input.0, input.1)
|
||||
}),
|
||||
// default to a "next" report
|
||||
map_res(Filter::parse, |input| to_subcommand(input, "next")),
|
||||
))(input)
|
||||
}
|
||||
|
||||
fn get_usage(u: &mut usage::Usage) {
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "list",
|
||||
syntax: "[filter] list",
|
||||
summary: "List tasks",
|
||||
name: "report",
|
||||
syntax: "[filter] [report-name] *or* [report-name] [filter]",
|
||||
summary: "Show a report",
|
||||
description: "
|
||||
Show a list of the tasks matching the filter",
|
||||
Show the named report, including only tasks matching the filter",
|
||||
});
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "next",
|
||||
syntax: "[filter]",
|
||||
summary: "Show the 'next' report",
|
||||
description: "
|
||||
Show the report named 'next', including only tasks matching the filter",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -396,7 +383,7 @@ impl Sync {
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::Universe;
|
||||
use crate::argparse::Condition;
|
||||
|
||||
const EMPTY: Vec<&str> = vec![];
|
||||
|
||||
@@ -472,8 +459,7 @@ mod test {
|
||||
fn test_modify_description_multi() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![123]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set(s!("foo bar")),
|
||||
@@ -490,8 +476,7 @@ mod test {
|
||||
fn test_append() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![123]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Append(s!("foo bar")),
|
||||
@@ -508,8 +493,7 @@ mod test {
|
||||
fn test_prepend() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![123]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Prepend(s!("foo bar")),
|
||||
@@ -526,8 +510,7 @@ mod test {
|
||||
fn test_done() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![123]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
|
||||
},
|
||||
modification: Modification {
|
||||
status: Some(Status::Completed),
|
||||
@@ -544,8 +527,7 @@ mod test {
|
||||
fn test_done_modify() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![123]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set(s!("now-finished")),
|
||||
@@ -563,8 +545,7 @@ mod test {
|
||||
fn test_start() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![123]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
|
||||
},
|
||||
modification: Modification {
|
||||
active: Some(true),
|
||||
@@ -581,8 +562,7 @@ mod test {
|
||||
fn test_start_modify() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![123]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
|
||||
},
|
||||
modification: Modification {
|
||||
active: Some(true),
|
||||
@@ -600,8 +580,7 @@ mod test {
|
||||
fn test_stop() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![123]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
|
||||
},
|
||||
modification: Modification {
|
||||
active: Some(false),
|
||||
@@ -618,8 +597,7 @@ mod test {
|
||||
fn test_stop_modify() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![123]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(123)])],
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set(s!("mod")),
|
||||
@@ -634,31 +612,78 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let subcommand = Subcommand::List {
|
||||
report: Report {
|
||||
..List::default_report()
|
||||
},
|
||||
fn test_report() {
|
||||
let subcommand = Subcommand::Report {
|
||||
filter: Default::default(),
|
||||
report_name: "myreport".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["list"]).unwrap(),
|
||||
Subcommand::parse(argv!["myreport"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_filter() {
|
||||
let subcommand = Subcommand::List {
|
||||
report: Report {
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![12, 13]),
|
||||
..Default::default()
|
||||
},
|
||||
..List::default_report()
|
||||
fn test_report_filter_before() {
|
||||
let subcommand = Subcommand::Report {
|
||||
filter: Filter {
|
||||
conditions: vec![Condition::IdList(vec![
|
||||
TaskId::WorkingSetId(12),
|
||||
TaskId::WorkingSetId(13),
|
||||
])],
|
||||
},
|
||||
report_name: "foo".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["12,13", "list"]).unwrap(),
|
||||
Subcommand::parse(argv!["12,13", "foo"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_filter_after() {
|
||||
let subcommand = Subcommand::Report {
|
||||
filter: Filter {
|
||||
conditions: vec![Condition::IdList(vec![
|
||||
TaskId::WorkingSetId(12),
|
||||
TaskId::WorkingSetId(13),
|
||||
])],
|
||||
},
|
||||
report_name: "foo".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["foo", "12,13"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_filter_next() {
|
||||
let subcommand = Subcommand::Report {
|
||||
filter: Filter {
|
||||
conditions: vec![Condition::IdList(vec![
|
||||
TaskId::WorkingSetId(12),
|
||||
TaskId::WorkingSetId(13),
|
||||
])],
|
||||
},
|
||||
report_name: "next".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["12,13"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_report_next() {
|
||||
let subcommand = Subcommand::Report {
|
||||
filter: Filter {
|
||||
..Default::default()
|
||||
},
|
||||
report_name: "next".to_owned(),
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv![]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
@@ -668,8 +693,10 @@ mod test {
|
||||
let subcommand = Subcommand::Info {
|
||||
debug: false,
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![12, 13]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![
|
||||
TaskId::WorkingSetId(12),
|
||||
TaskId::WorkingSetId(13),
|
||||
])],
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -683,8 +710,7 @@ mod test {
|
||||
let subcommand = Subcommand::Info {
|
||||
debug: true,
|
||||
filter: Filter {
|
||||
universe: Universe::for_ids(vec![12]),
|
||||
..Default::default()
|
||||
conditions: vec![Condition::IdList(vec![TaskId::WorkingSetId(12)])],
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
@@ -704,11 +730,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_gc_extra_args() {
|
||||
let subcommand = Subcommand::Gc;
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["gc", "foo"]).unwrap(),
|
||||
(&vec!["foo"][..], subcommand)
|
||||
);
|
||||
assert!(Subcommand::parse(argv!["gc", "foo"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,7 +4,7 @@ pub(crate) mod add;
|
||||
pub(crate) mod gc;
|
||||
pub(crate) mod help;
|
||||
pub(crate) mod info;
|
||||
pub(crate) mod list;
|
||||
pub(crate) mod modify;
|
||||
pub(crate) mod report;
|
||||
pub(crate) mod sync;
|
||||
pub(crate) mod version;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::argparse::Report;
|
||||
use crate::argparse::Filter;
|
||||
use crate::invocation::display_report;
|
||||
use failure::Fallible;
|
||||
use taskchampion::Replica;
|
||||
@@ -7,35 +7,33 @@ use termcolor::WriteColor;
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
report: Report,
|
||||
report_name: String,
|
||||
filter: Filter,
|
||||
) -> Fallible<()> {
|
||||
display_report(w, replica, &report)
|
||||
display_report(w, replica, report_name, filter)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::{Column, Filter, Property};
|
||||
use crate::argparse::Filter;
|
||||
use crate::invocation::test::*;
|
||||
use taskchampion::Status;
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
fn test_report() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
replica.new_task(Status::Pending, s!("my task")).unwrap();
|
||||
|
||||
let report = Report {
|
||||
filter: Filter {
|
||||
..Default::default()
|
||||
},
|
||||
columns: vec![Column {
|
||||
label: "Description".to_owned(),
|
||||
property: Property::Description,
|
||||
}],
|
||||
// The function being tested is only one line long, so this is sort of an integration test
|
||||
// for display_report.
|
||||
|
||||
let report_name = "next".to_owned();
|
||||
let filter = Filter {
|
||||
..Default::default()
|
||||
};
|
||||
execute(&mut w, &mut replica, report).unwrap();
|
||||
execute(&mut w, &mut replica, report_name, filter).unwrap();
|
||||
assert!(w.into_string().contains("my task"));
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::argparse::{Condition, Filter, TaskId, Universe};
|
||||
use crate::argparse::{Condition, Filter, TaskId};
|
||||
use failure::Fallible;
|
||||
use std::collections::HashSet;
|
||||
use std::convert::TryInto;
|
||||
use taskchampion::{Replica, Tag, Task};
|
||||
use taskchampion::{Replica, Status, Tag, Task, Uuid};
|
||||
|
||||
fn match_task(filter: &Filter, task: &Task) -> bool {
|
||||
fn match_task(filter: &Filter, task: &Task, uuid: Uuid, working_set_id: Option<usize>) -> bool {
|
||||
for cond in &filter.conditions {
|
||||
match cond {
|
||||
Condition::HasTag(ref tag) => {
|
||||
@@ -21,11 +21,85 @@ fn match_task(filter: &Filter, task: &Task) -> bool {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Condition::Status(status) => {
|
||||
if task.get_status() != *status {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Condition::IdList(ids) => {
|
||||
let uuid_str = uuid.to_string();
|
||||
let mut found = false;
|
||||
for id in ids {
|
||||
if match id {
|
||||
TaskId::WorkingSetId(i) => Some(*i) == working_set_id,
|
||||
TaskId::PartialUuid(partial) => uuid_str.starts_with(partial),
|
||||
TaskId::Uuid(i) => *i == uuid,
|
||||
} {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// the universe of tasks we must consider
|
||||
enum Universe {
|
||||
/// Scan all the tasks
|
||||
AllTasks,
|
||||
/// Scan the working set (for pending tasks)
|
||||
WorkingSet,
|
||||
/// Scan an explicit set of tasks, "Absolute" meaning either full UUID or a working set
|
||||
/// index
|
||||
AbsoluteIdList(Vec<TaskId>),
|
||||
}
|
||||
|
||||
/// Determine the universe for the given filter; avoiding the need to scan all tasks in most cases.
|
||||
fn universe_for_filter(filter: &Filter) -> Universe {
|
||||
/// If there is a condition with Status::Pending, return true
|
||||
fn has_pending_condition(filter: &Filter) -> bool {
|
||||
filter
|
||||
.conditions
|
||||
.iter()
|
||||
.any(|cond| matches!(cond, Condition::Status(Status::Pending)))
|
||||
}
|
||||
|
||||
/// If there is a condition with an IdList containing no partial UUIDs,
|
||||
/// return that.
|
||||
fn absolute_id_list_condition(filter: &Filter) -> Option<Vec<TaskId>> {
|
||||
filter
|
||||
.conditions
|
||||
.iter()
|
||||
.find(|cond| {
|
||||
if let Condition::IdList(ids) = cond {
|
||||
!ids.iter().any(|id| matches!(id, TaskId::PartialUuid(_)))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.map(|cond| {
|
||||
if let Condition::IdList(ids) = cond {
|
||||
ids.to_vec()
|
||||
} else {
|
||||
unreachable!() // any condition found above must be an IdList(_)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if let Some(ids) = absolute_id_list_condition(filter) {
|
||||
Universe::AbsoluteIdList(ids)
|
||||
} else if has_pending_condition(filter) {
|
||||
Universe::WorkingSet
|
||||
} else {
|
||||
Universe::AllTasks
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the tasks matching the given filter. This will return each matching
|
||||
/// task once, even if the user specified the same task multiple times on the
|
||||
/// command line.
|
||||
@@ -35,38 +109,14 @@ pub(super) fn filtered_tasks(
|
||||
) -> Fallible<impl Iterator<Item = Task>> {
|
||||
let mut res = vec![];
|
||||
|
||||
fn is_partial_uuid(taskid: &TaskId) -> bool {
|
||||
matches!(taskid, TaskId::PartialUuid(_))
|
||||
}
|
||||
log::debug!("Applying filter {:?}", filter);
|
||||
|
||||
// We will enumerate the universe of tasks for this filter, checking
|
||||
// each resulting task with match_task
|
||||
match filter.universe {
|
||||
match universe_for_filter(filter) {
|
||||
// A list of IDs, but some are partial so we need to iterate over
|
||||
// all tasks and pattern-match their Uuids
|
||||
Universe::IdList(ref ids) if ids.iter().any(is_partial_uuid) => {
|
||||
log::debug!("Scanning entire task database due to partial UUIDs in the filter");
|
||||
'task: for (uuid, task) in replica.all_tasks()?.drain() {
|
||||
for id in ids {
|
||||
let in_universe = match id {
|
||||
TaskId::WorkingSetId(id) => {
|
||||
// NOTE: (#108) this results in many reads of the working set; it
|
||||
// may be better to cache this information here or in the Replica.
|
||||
replica.get_working_set_index(&uuid)? == Some(*id)
|
||||
}
|
||||
TaskId::PartialUuid(prefix) => uuid.to_string().starts_with(prefix),
|
||||
TaskId::Uuid(id) => id == &uuid,
|
||||
};
|
||||
if in_universe && match_task(filter, &task) {
|
||||
res.push(task);
|
||||
continue 'task;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A list of full IDs, which we can fetch directly
|
||||
Universe::IdList(ref ids) => {
|
||||
Universe::AbsoluteIdList(ref ids) => {
|
||||
log::debug!("Scanning only the tasks specified in the filter");
|
||||
// this is the only case where we might accidentally return the same task
|
||||
// several times, so we must track the seen tasks.
|
||||
@@ -74,7 +124,7 @@ pub(super) fn filtered_tasks(
|
||||
for id in ids {
|
||||
let task = match id {
|
||||
TaskId::WorkingSetId(id) => replica.get_working_set_task(*id)?,
|
||||
TaskId::PartialUuid(_) => unreachable!(), // handled above
|
||||
TaskId::PartialUuid(_) => unreachable!(), // not present in absolute id list
|
||||
TaskId::Uuid(id) => replica.get_task(id)?,
|
||||
};
|
||||
|
||||
@@ -86,7 +136,9 @@ pub(super) fn filtered_tasks(
|
||||
}
|
||||
seen.insert(uuid);
|
||||
|
||||
if match_task(filter, &task) {
|
||||
let working_set_id = replica.get_working_set_index(&uuid)?;
|
||||
|
||||
if match_task(filter, &task, uuid, working_set_id) {
|
||||
res.push(task);
|
||||
}
|
||||
}
|
||||
@@ -96,19 +148,20 @@ pub(super) fn filtered_tasks(
|
||||
// All tasks -- iterate over the full set
|
||||
Universe::AllTasks => {
|
||||
log::debug!("Scanning all tasks in the task database");
|
||||
for (_, task) in replica.all_tasks()?.drain() {
|
||||
if match_task(filter, &task) {
|
||||
for (uuid, task) in replica.all_tasks()?.drain() {
|
||||
// Yikes, slow! https://github.com/djmitche/taskchampion/issues/108
|
||||
let working_set_id = replica.get_working_set_index(&uuid)?;
|
||||
if match_task(filter, &task, uuid, working_set_id) {
|
||||
res.push(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pending tasks -- just scan the working set
|
||||
Universe::PendingTasks => {
|
||||
Universe::WorkingSet => {
|
||||
log::debug!("Scanning only the working set (pending tasks)");
|
||||
for task in replica.working_set()?.drain(..) {
|
||||
for (i, task) in replica.working_set()?.drain(..).enumerate() {
|
||||
if let Some(task) = task {
|
||||
if match_task(filter, &task) {
|
||||
let uuid = *task.get_uuid();
|
||||
if match_task(filter, &task, uuid, Some(i)) {
|
||||
res.push(task);
|
||||
}
|
||||
}
|
||||
@@ -136,12 +189,11 @@ mod test {
|
||||
let t1uuid = *t1.get_uuid();
|
||||
|
||||
let filter = Filter {
|
||||
universe: Universe::IdList(vec![
|
||||
conditions: vec![Condition::IdList(vec![
|
||||
TaskId::Uuid(t1uuid), // A
|
||||
TaskId::WorkingSetId(1), // A (again, dups filtered)
|
||||
TaskId::Uuid(*t2.get_uuid()), // B
|
||||
]),
|
||||
..Default::default()
|
||||
])],
|
||||
};
|
||||
let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)
|
||||
.unwrap()
|
||||
@@ -165,12 +217,11 @@ mod test {
|
||||
let t2partial = t2uuid[..13].to_owned();
|
||||
|
||||
let filter = Filter {
|
||||
universe: Universe::IdList(vec![
|
||||
conditions: vec![Condition::IdList(vec![
|
||||
TaskId::Uuid(t1uuid), // A
|
||||
TaskId::WorkingSetId(1), // A (again, dups filtered)
|
||||
TaskId::PartialUuid(t2partial), // B
|
||||
]),
|
||||
..Default::default()
|
||||
])],
|
||||
};
|
||||
let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)
|
||||
.unwrap()
|
||||
@@ -189,10 +240,7 @@ mod test {
|
||||
replica.new_task(Status::Deleted, s!("C")).unwrap();
|
||||
replica.gc().unwrap();
|
||||
|
||||
let filter = Filter {
|
||||
universe: Universe::AllTasks,
|
||||
..Default::default()
|
||||
};
|
||||
let filter = Filter { conditions: vec![] };
|
||||
let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)
|
||||
.unwrap()
|
||||
.map(|t| t.get_description().to_owned())
|
||||
@@ -224,9 +272,7 @@ mod test {
|
||||
|
||||
// look for just "yes" (A and B)
|
||||
let filter = Filter {
|
||||
universe: Universe::AllTasks,
|
||||
conditions: vec![Condition::HasTag(s!("yes"))],
|
||||
..Default::default()
|
||||
};
|
||||
let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)?
|
||||
.map(|t| t.get_description().to_owned())
|
||||
@@ -236,9 +282,7 @@ mod test {
|
||||
|
||||
// look for tags without "no" (A, D)
|
||||
let filter = Filter {
|
||||
universe: Universe::AllTasks,
|
||||
conditions: vec![Condition::NoTag(s!("no"))],
|
||||
..Default::default()
|
||||
};
|
||||
let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)?
|
||||
.map(|t| t.get_description().to_owned())
|
||||
@@ -248,9 +292,7 @@ mod test {
|
||||
|
||||
// look for tags with "yes" and "no" (B)
|
||||
let filter = Filter {
|
||||
universe: Universe::AllTasks,
|
||||
conditions: vec![Condition::HasTag(s!("yes")), Condition::HasTag(s!("no"))],
|
||||
..Default::default()
|
||||
};
|
||||
let filtered: Vec<_> = filtered_tasks(&mut replica, &filter)?
|
||||
.map(|t| t.get_description().to_owned())
|
||||
@@ -270,8 +312,7 @@ mod test {
|
||||
replica.gc().unwrap();
|
||||
|
||||
let filter = Filter {
|
||||
universe: Universe::PendingTasks,
|
||||
..Default::default()
|
||||
conditions: vec![Condition::Status(Status::Pending)],
|
||||
};
|
||||
let mut filtered: Vec<_> = filtered_tasks(&mut replica, &filter)
|
||||
.unwrap()
|
||||
|
||||
@@ -60,9 +60,13 @@ pub(crate) fn invoke(command: Command, settings: Config) -> Fallible<()> {
|
||||
} => return cmd::modify::execute(&mut w, &mut replica, filter, modification),
|
||||
|
||||
Command {
|
||||
subcommand: Subcommand::List { report },
|
||||
subcommand:
|
||||
Subcommand::Report {
|
||||
report_name,
|
||||
filter,
|
||||
},
|
||||
..
|
||||
} => return cmd::list::execute(&mut w, &mut replica, report),
|
||||
} => return cmd::report::execute(&mut w, &mut replica, report_name, filter),
|
||||
|
||||
Command {
|
||||
subcommand: Subcommand::Info { filter, debug },
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use crate::argparse::{Column, Property, Report, SortBy};
|
||||
use crate::argparse::{Condition, Filter};
|
||||
use crate::invocation::filtered_tasks;
|
||||
use crate::report::{Column, Property, Report, Sort, SortBy};
|
||||
use crate::table;
|
||||
use failure::Fallible;
|
||||
use failure::{bail, Fallible};
|
||||
use prettytable::{Row, Table};
|
||||
use std::cmp::Ordering;
|
||||
use taskchampion::{Replica, Task, Uuid};
|
||||
use taskchampion::{Replica, Status, Task, Uuid};
|
||||
use termcolor::WriteColor;
|
||||
|
||||
// pending #123, this is a non-fallible way of looking up a task's working set index
|
||||
@@ -101,20 +102,66 @@ fn task_column(task: &Task, column: &Column, working_set: &WorkingSet) -> String
|
||||
}
|
||||
}
|
||||
|
||||
fn get_report(report_name: String, filter: Filter) -> Fallible<Report> {
|
||||
let columns = vec![
|
||||
Column {
|
||||
label: "Id".to_owned(),
|
||||
property: Property::Id,
|
||||
},
|
||||
Column {
|
||||
label: "Description".to_owned(),
|
||||
property: Property::Description,
|
||||
},
|
||||
Column {
|
||||
label: "Active".to_owned(),
|
||||
property: Property::Active,
|
||||
},
|
||||
Column {
|
||||
label: "Tags".to_owned(),
|
||||
property: Property::Tags,
|
||||
},
|
||||
];
|
||||
let sort = vec![Sort {
|
||||
ascending: false,
|
||||
sort_by: SortBy::Uuid,
|
||||
}];
|
||||
let mut report = match report_name.as_ref() {
|
||||
"list" => Report {
|
||||
columns,
|
||||
sort,
|
||||
filter: Default::default(),
|
||||
},
|
||||
"next" => Report {
|
||||
columns,
|
||||
sort,
|
||||
filter: Filter {
|
||||
conditions: vec![Condition::Status(Status::Pending)],
|
||||
},
|
||||
},
|
||||
_ => bail!("Unknown report {:?}", report_name),
|
||||
};
|
||||
|
||||
// intersect the report's filter with the user-supplied filter
|
||||
report.filter = report.filter.intersect(filter);
|
||||
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
pub(super) fn display_report<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
report: &Report,
|
||||
report_name: String,
|
||||
filter: Filter,
|
||||
) -> Fallible<()> {
|
||||
let mut t = Table::new();
|
||||
|
||||
let report = get_report(report_name, filter)?;
|
||||
let working_set = WorkingSet::new(replica)?;
|
||||
|
||||
// Get the tasks from the filter
|
||||
let mut tasks: Vec<_> = filtered_tasks(replica, &report.filter)?.collect();
|
||||
|
||||
// ..sort them as desired
|
||||
sort_tasks(&mut tasks, report, &working_set);
|
||||
sort_tasks(&mut tasks, &report, &working_set);
|
||||
|
||||
// ..set up the column titles
|
||||
t.set_format(table::format());
|
||||
@@ -138,8 +185,8 @@ pub(super) fn display_report<W: WriteColor>(
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::{Column, Property, Report, Sort, SortBy};
|
||||
use crate::invocation::test::*;
|
||||
use crate::report::Sort;
|
||||
use std::convert::TryInto;
|
||||
use taskchampion::Status;
|
||||
|
||||
@@ -371,4 +418,3 @@ mod test {
|
||||
assert_eq!(task_column(&task, &column, &working_set), s!(""));
|
||||
}
|
||||
}
|
||||
// TODO: test task_column
|
||||
|
||||
@@ -38,6 +38,7 @@ mod macros;
|
||||
|
||||
mod argparse;
|
||||
mod invocation;
|
||||
mod report;
|
||||
mod settings;
|
||||
mod table;
|
||||
mod usage;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use super::Filter;
|
||||
//! This module contains the data structures used to define reports.
|
||||
|
||||
use crate::argparse::Filter;
|
||||
|
||||
/// A report specifies a filter as well as a sort order and information about which
|
||||
/// task attributes to display
|
||||
Reference in New Issue
Block a user