mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Merge pull request #98 from djmitche/issue44
Switch to a command-line API closer to TaskWarrior
This commit is contained in:
commit
141752bd4b
46 changed files with 2109 additions and 1072 deletions
67
Cargo.lock
generated
67
Cargo.lock
generated
|
@ -430,6 +430,18 @@ version = "1.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "0.19.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium",
|
||||
"tap",
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2b_simd"
|
||||
version = "0.5.11"
|
||||
|
@ -557,7 +569,7 @@ dependencies = [
|
|||
"atty",
|
||||
"bitflags",
|
||||
"strsim",
|
||||
"textwrap",
|
||||
"textwrap 0.11.0",
|
||||
"unicode-width",
|
||||
"vec_map",
|
||||
]
|
||||
|
@ -587,7 +599,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"nom",
|
||||
"nom 5.1.2",
|
||||
"serde",
|
||||
"yaml-rust",
|
||||
]
|
||||
|
@ -890,6 +902,12 @@ version = "0.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ba62103ce691c2fd80fbae2213dfdda9ce60804973ac6b6e97de818ea7f52c8"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.8"
|
||||
|
@ -1373,6 +1391,18 @@ dependencies = [
|
|||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "6.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"lexical-core",
|
||||
"memchr",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "normalize-line-endings"
|
||||
version = "0.3.0"
|
||||
|
@ -1649,6 +1679,12 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.4.6"
|
||||
|
@ -2200,6 +2236,12 @@ dependencies = [
|
|||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e"
|
||||
|
||||
[[package]]
|
||||
name = "taskchampion"
|
||||
version = "0.2.0"
|
||||
|
@ -2222,15 +2264,19 @@ name = "taskchampion-cli"
|
|||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"assert_cmd",
|
||||
"clap",
|
||||
"atty",
|
||||
"config",
|
||||
"dirs 3.0.1",
|
||||
"env_logger",
|
||||
"failure",
|
||||
"log",
|
||||
"nom 6.0.1",
|
||||
"predicates",
|
||||
"prettytable-rs",
|
||||
"taskchampion",
|
||||
"tempdir",
|
||||
"termcolor",
|
||||
"textwrap 0.12.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2303,6 +2349,15 @@ dependencies = [
|
|||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.22"
|
||||
|
@ -2798,6 +2853,12 @@ dependencies = [
|
|||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
|
||||
|
||||
[[package]]
|
||||
name = "yaml-rust"
|
||||
version = "0.4.4"
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
[package]
|
||||
name = "taskchampion-cli"
|
||||
version = "0.2.0"
|
||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||
edition = "2018"
|
||||
name = "taskchampion-cli"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
clap = "^2.33.0"
|
||||
taskchampion = { path = "../taskchampion" }
|
||||
failure = "^0.1.8"
|
||||
prettytable-rs = "^0.8.0"
|
||||
config = { version="^0.10.1", default-features=false, features=["yaml"] }
|
||||
dirs = "^3.0.1"
|
||||
log = "^0.4.11"
|
||||
env_logger = "^0.8.2"
|
||||
failure = "^0.1.8"
|
||||
log = "^0.4.11"
|
||||
nom = "*"
|
||||
prettytable-rs = "^0.8.0"
|
||||
textwrap = "0.12.1"
|
||||
termcolor = "1.1.2"
|
||||
atty = "0.2.14"
|
||||
|
||||
[dependencies.config]
|
||||
default-features = false
|
||||
features = ["yaml"]
|
||||
version = "^0.10.1"
|
||||
|
||||
[dependencies.taskchampion]
|
||||
path = "../taskchampion"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd = "^1.0.1"
|
||||
predicates = "^1.0.5"
|
||||
tempdir = "^0.3.7"
|
||||
|
|
232
cli/src/argparse/args.rs
Normal file
232
cli/src/argparse/args.rs
Normal file
|
@ -0,0 +1,232 @@
|
|||
//! Parsers for argument lists -- arrays of strings
|
||||
use super::ArgList;
|
||||
use nom::bytes::complete::tag as nomtag;
|
||||
use nom::{
|
||||
branch::*,
|
||||
character::complete::*,
|
||||
combinator::*,
|
||||
error::{Error, ErrorKind},
|
||||
multi::*,
|
||||
sequence::*,
|
||||
Err, IResult,
|
||||
};
|
||||
|
||||
/// Recognizes any argument
|
||||
pub(super) fn any(input: &str) -> IResult<&str, &str> {
|
||||
rest(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 comma-separated list of ID's (integers or UUID prefixes)
|
||||
pub(super) fn id_list(input: &str) -> IResult<&str, Vec<&str>> {
|
||||
fn hex_n(n: usize) -> impl Fn(&str) -> IResult<&str, &str> {
|
||||
move |input: &str| recognize(many_m_n(n, n, one_of(&b"0123456789abcdefABCDEF"[..])))(input)
|
||||
}
|
||||
all_consuming(separated_list1(
|
||||
char(','),
|
||||
alt((
|
||||
recognize(tuple((
|
||||
hex_n(8),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
char('-'),
|
||||
hex_n(12),
|
||||
))),
|
||||
recognize(tuple((
|
||||
hex_n(8),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
char('-'),
|
||||
hex_n(4),
|
||||
))),
|
||||
recognize(tuple((hex_n(8), char('-'), hex_n(4), char('-'), hex_n(4)))),
|
||||
recognize(tuple((hex_n(8), char('-'), hex_n(4)))),
|
||||
hex_n(8),
|
||||
digit1,
|
||||
)),
|
||||
))(input)
|
||||
}
|
||||
|
||||
/// Recognizes a tag prefixed with `+` and returns the tag value
|
||||
#[allow(dead_code)] // tags not implemented yet
|
||||
pub(super) fn plus_tag(input: &str) -> IResult<&str, &str> {
|
||||
fn to_tag(input: (char, &str)) -> Result<&str, ()> {
|
||||
Ok(input.1)
|
||||
}
|
||||
map_res(
|
||||
all_consuming(tuple((char('+'), recognize(pair(alpha1, alphanumeric0))))),
|
||||
to_tag,
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// Recognizes a tag prefixed with `-` and returns the tag value
|
||||
#[allow(dead_code)] // tags not implemented yet
|
||||
pub(super) fn minus_tag(input: &str) -> IResult<&str, &str> {
|
||||
fn to_tag(input: (char, &str)) -> Result<&str, ()> {
|
||||
Ok(input.1)
|
||||
}
|
||||
map_res(
|
||||
all_consuming(tuple((char('-'), recognize(pair(alpha1, alphanumeric0))))),
|
||||
to_tag,
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// Recognizes a tag prefixed with either `-` or `+`, returning true for + and false for -
|
||||
#[allow(dead_code)] // tags not implemented yet
|
||||
pub(super) fn tag(input: &str) -> IResult<&str, (bool, &str)> {
|
||||
fn to_plus(input: &str) -> Result<(bool, &str), ()> {
|
||||
Ok((true, input))
|
||||
}
|
||||
fn to_minus(input: &str) -> Result<(bool, &str), ()> {
|
||||
Ok((false, input))
|
||||
}
|
||||
alt((map_res(plus_tag, to_plus), map_res(minus_tag, to_minus)))(input)
|
||||
}
|
||||
|
||||
/// Consume a single argument from an argument list that matches the given string parser (one
|
||||
/// of the other functions in this module). The given parser must consume the entire input.
|
||||
pub(super) fn arg_matching<'a, O, F>(f: F) -> impl Fn(ArgList<'a>) -> IResult<ArgList, O>
|
||||
where
|
||||
F: Fn(&'a str) -> IResult<&'a str, O>,
|
||||
{
|
||||
move |input: ArgList<'a>| {
|
||||
if let Some(arg) = input.get(0) {
|
||||
return match f(arg) {
|
||||
Ok(("", rv)) => Ok((&input[1..], rv)),
|
||||
// single-arg parsers must consume the entire arg
|
||||
Ok((unconsumed, _)) => panic!("unconsumed argument input {}", unconsumed),
|
||||
// single-arg parsers are all complete parsers
|
||||
Err(Err::Incomplete(_)) => unreachable!(),
|
||||
// for error and failure, rewrite to an error at this position in the arugment list
|
||||
Err(Err::Error(Error { input: _, code })) => Err(Err::Error(Error { input, code })),
|
||||
Err(Err::Failure(Error { input: _, code })) => {
|
||||
Err(Err::Failure(Error { input, code }))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Err(Err::Error(Error {
|
||||
input,
|
||||
// since we're using nom's built-in Error, our choices here are limited, but tihs
|
||||
// occurs when there's no argument where one is expected, so Eof seems appropriate
|
||||
code: ErrorKind::Eof,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_arg_matching() {
|
||||
assert_eq!(
|
||||
arg_matching(tag)(argv!["+foo", "bar"]).unwrap(),
|
||||
(argv!["bar"], (true, "foo"))
|
||||
);
|
||||
assert_eq!(
|
||||
arg_matching(tag)(argv!["-foo", "bar"]).unwrap(),
|
||||
(argv!["bar"], (false, "foo"))
|
||||
);
|
||||
assert!(arg_matching(tag)(argv!["foo", "bar"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plus_tag() {
|
||||
assert_eq!(plus_tag("+abc").unwrap().1, "abc");
|
||||
assert_eq!(plus_tag("+abc123").unwrap().1, "abc123");
|
||||
assert!(plus_tag("-abc123").is_err());
|
||||
assert!(plus_tag("+abc123 ").is_err());
|
||||
assert!(plus_tag(" +abc123").is_err());
|
||||
assert!(plus_tag("+1abc").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_minus_tag() {
|
||||
assert_eq!(minus_tag("-abc").unwrap().1, "abc");
|
||||
assert_eq!(minus_tag("-abc123").unwrap().1, "abc123");
|
||||
assert!(minus_tag("+abc123").is_err());
|
||||
assert!(minus_tag("-abc123 ").is_err());
|
||||
assert!(minus_tag(" -abc123").is_err());
|
||||
assert!(minus_tag("-1abc").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag() {
|
||||
assert_eq!(tag("-abc").unwrap().1, (false, "abc"));
|
||||
assert_eq!(tag("+abc123").unwrap().1, (true, "abc123"));
|
||||
assert!(tag("+abc123 --").is_err());
|
||||
assert!(tag("-abc123 ").is_err());
|
||||
assert!(tag(" -abc123").is_err());
|
||||
assert!(tag("-1abc").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_literal() {
|
||||
assert_eq!(literal("list")("list").unwrap().1, "list");
|
||||
assert!(literal("list")("listicle").is_err());
|
||||
assert!(literal("list")(" list ").is_err());
|
||||
assert!(literal("list")("LiSt").is_err());
|
||||
assert!(literal("list")("denylist").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_single() {
|
||||
assert_eq!(id_list("123").unwrap().1, vec!["123".to_owned()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_uuids() {
|
||||
assert_eq!(id_list("12341234").unwrap().1, vec!["12341234".to_owned()]);
|
||||
assert_eq!(id_list("1234abcd").unwrap().1, vec!["1234abcd".to_owned()]);
|
||||
assert_eq!(id_list("abcd1234").unwrap().1, vec!["abcd1234".to_owned()]);
|
||||
assert_eq!(
|
||||
id_list("abcd1234-1234").unwrap().1,
|
||||
vec!["abcd1234-1234".to_owned()]
|
||||
);
|
||||
assert_eq!(
|
||||
id_list("abcd1234-1234-2345").unwrap().1,
|
||||
vec!["abcd1234-1234-2345".to_owned()]
|
||||
);
|
||||
assert_eq!(
|
||||
id_list("abcd1234-1234-2345-3456").unwrap().1,
|
||||
vec!["abcd1234-1234-2345-3456".to_owned()]
|
||||
);
|
||||
assert_eq!(
|
||||
id_list("abcd1234-1234-2345-3456-0123456789ab").unwrap().1,
|
||||
vec!["abcd1234-1234-2345-3456-0123456789ab".to_owned()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_invalid_partial_uuids() {
|
||||
assert!(id_list("abcd123").is_err());
|
||||
assert!(id_list("abcd12345").is_err());
|
||||
assert!(id_list("abcd1234-").is_err());
|
||||
assert!(id_list("abcd1234-123").is_err());
|
||||
assert!(id_list("abcd1234-1234-").is_err());
|
||||
assert!(id_list("abcd1234-12345-").is_err());
|
||||
assert!(id_list("abcd1234-1234-2345-3456-0123456789ab-").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_uuids_mixed() {
|
||||
assert_eq!(id_list("abcd1234,abcd1234-1234,abcd1234-1234-2345,abcd1234-1234-2345-3456,abcd1234-1234-2345-3456-0123456789ab").unwrap().1,
|
||||
vec!["abcd1234".to_owned(),
|
||||
"abcd1234-1234".to_owned(),
|
||||
"abcd1234-1234-2345".to_owned(),
|
||||
"abcd1234-1234-2345-3456".to_owned(),
|
||||
"abcd1234-1234-2345-3456-0123456789ab".to_owned(),
|
||||
]);
|
||||
}
|
||||
}
|
62
cli/src/argparse/command.rs
Normal file
62
cli/src/argparse/command.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use super::args::*;
|
||||
use super::{ArgList, Subcommand};
|
||||
use failure::{format_err, Fallible};
|
||||
use nom::{combinator::*, sequence::*, Err, IResult};
|
||||
|
||||
/// A command is the overall command that the CLI should execute.
|
||||
///
|
||||
/// It consists of some information common to all commands and a `Subcommand` identifying the
|
||||
/// particular kind of behavior desired.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) struct Command {
|
||||
pub(crate) command_name: String,
|
||||
pub(crate) subcommand: Subcommand,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Command> {
|
||||
fn to_command(input: (&str, Subcommand)) -> Result<Command, ()> {
|
||||
let command = Command {
|
||||
command_name: input.0.to_owned(),
|
||||
subcommand: input.1,
|
||||
};
|
||||
Ok(command)
|
||||
}
|
||||
map_res(
|
||||
all_consuming(tuple((arg_matching(any), Subcommand::parse))),
|
||||
to_command,
|
||||
)(input)
|
||||
}
|
||||
|
||||
/// Parse a command from the given list of strings.
|
||||
pub fn from_argv(argv: &[&str]) -> Fallible<Command> {
|
||||
match Command::parse(argv) {
|
||||
Ok((&[], cmd)) => Ok(cmd),
|
||||
Ok((trailing, _)) => Err(format_err!(
|
||||
"command line has trailing arguments: {:?}",
|
||||
trailing
|
||||
)),
|
||||
Err(Err::Incomplete(_)) => unreachable!(),
|
||||
Err(Err::Error(e)) => Err(format_err!("command line not recognized: {:?}", e)),
|
||||
Err(Err::Failure(e)) => Err(format_err!("command line not recognized: {:?}", e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
// NOTE: most testing of specific subcommands is handled in `subcommand.rs`.
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
assert_eq!(
|
||||
Command::from_argv(argv!["task", "version"]).unwrap(),
|
||||
Command {
|
||||
subcommand: Subcommand::Version,
|
||||
command_name: "task".to_owned(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
105
cli/src/argparse/filter.rs
Normal file
105
cli/src/argparse/filter.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
use super::args::{arg_matching, id_list};
|
||||
use super::ArgList;
|
||||
use nom::{combinator::*, multi::fold_many0, IResult};
|
||||
|
||||
/// A filter represents a selection of a particular set of tasks.
|
||||
#[derive(Debug, PartialEq, Default, Clone)]
|
||||
pub(crate) struct Filter {
|
||||
/// A list of numeric IDs or prefixes of UUIDs
|
||||
pub(crate) id_list: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
enum FilterArg {
|
||||
IdList(Vec<String>),
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Filter> {
|
||||
fn fold(mut acc: Filter, mod_arg: FilterArg) -> Filter {
|
||||
match mod_arg {
|
||||
FilterArg::IdList(mut id_list) => {
|
||||
if let Some(ref mut existing) = acc.id_list {
|
||||
// given multiple ID lists, concatenate them to represent
|
||||
// an "OR" between them.
|
||||
existing.append(&mut id_list);
|
||||
} else {
|
||||
acc.id_list = Some(id_list);
|
||||
}
|
||||
}
|
||||
}
|
||||
acc
|
||||
}
|
||||
fold_many0(
|
||||
Self::id_list,
|
||||
Filter {
|
||||
..Default::default()
|
||||
},
|
||||
fold,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn id_list(input: ArgList) -> IResult<ArgList, FilterArg> {
|
||||
fn to_filterarg(mut input: Vec<&str>) -> Result<FilterArg, ()> {
|
||||
Ok(FilterArg::IdList(
|
||||
input.drain(..).map(str::to_owned).collect(),
|
||||
))
|
||||
}
|
||||
map_res(arg_matching(id_list), to_filterarg)(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let (input, filter) = Filter::parse(argv![]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_single() {
|
||||
let (input, filter) = Filter::parse(argv!["1"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
id_list: Some(vec!["1".to_owned()]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_commas() {
|
||||
let (input, filter) = Filter::parse(argv!["1,2,3"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
id_list: Some(vec!["1".to_owned(), "2".to_owned(), "3".to_owned()]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_id_list_uuids() {
|
||||
let (input, filter) = Filter::parse(argv!["1,abcd1234"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
filter,
|
||||
Filter {
|
||||
id_list: Some(vec!["1".to_owned(), "abcd1234".to_owned()]),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
33
cli/src/argparse/mod.rs
Normal file
33
cli/src/argparse/mod.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*!
|
||||
|
||||
This module is responsible for parsing command lines (`Arglist`, an alias for `&[&str]`) into `Command` instances.
|
||||
It removes some redundancy from the command line, for example combining the multiple ways to modify a task into a single `Modification` struct.
|
||||
|
||||
The module is organized as a nom parser over ArgList, and each struct has a `parse` method to parse such a list.
|
||||
|
||||
The exception to this rule is the `args` sub-module, which contains string parsers that are applied to indivdual command-line elements.
|
||||
|
||||
All of the structs produced by this module are fully-owned, data-only structs.
|
||||
That is, they contain no references, and have no methods to aid in their execution -- that is the `invocation` module's job.
|
||||
|
||||
*/
|
||||
mod args;
|
||||
mod command;
|
||||
mod filter;
|
||||
mod modification;
|
||||
mod report;
|
||||
mod subcommand;
|
||||
|
||||
pub(crate) use command::Command;
|
||||
pub(crate) use filter::Filter;
|
||||
pub(crate) use modification::{DescriptionMod, Modification};
|
||||
pub(crate) use report::Report;
|
||||
pub(crate) use subcommand::Subcommand;
|
||||
|
||||
use crate::usage::Usage;
|
||||
|
||||
type ArgList<'a> = &'a [&'a str];
|
||||
|
||||
pub(crate) fn get_usage(usage: &mut Usage) {
|
||||
Subcommand::get_usage(usage);
|
||||
}
|
119
cli/src/argparse/modification.rs
Normal file
119
cli/src/argparse/modification.rs
Normal file
|
@ -0,0 +1,119 @@
|
|||
use super::args::{any, arg_matching};
|
||||
use super::ArgList;
|
||||
use nom::{combinator::*, multi::fold_many0, IResult};
|
||||
use taskchampion::Status;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum DescriptionMod {
|
||||
/// do not change the description
|
||||
None,
|
||||
|
||||
/// Prepend the given value to the description, with a space separator
|
||||
Prepend(String),
|
||||
|
||||
/// Append the given value to the description, with a space separator
|
||||
Append(String),
|
||||
|
||||
/// Set the description
|
||||
Set(String),
|
||||
}
|
||||
|
||||
impl Default for DescriptionMod {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
/// A modification represents a change to a task: adding or removing tags, setting the
|
||||
/// description, and so on.
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
pub struct Modification {
|
||||
/// Change the description
|
||||
pub description: DescriptionMod,
|
||||
|
||||
/// Set the status
|
||||
pub status: Option<Status>,
|
||||
|
||||
/// Set the "active" status, that is, start (true) or stop (false) the task.
|
||||
pub active: Option<bool>,
|
||||
}
|
||||
|
||||
/// A single argument that is part of a modification, used internally to this module
|
||||
enum ModArg<'a> {
|
||||
Description(&'a str),
|
||||
}
|
||||
|
||||
impl Modification {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Modification> {
|
||||
fn fold(mut acc: Modification, mod_arg: ModArg) -> Modification {
|
||||
match mod_arg {
|
||||
ModArg::Description(description) => {
|
||||
if let DescriptionMod::Set(existing) = acc.description {
|
||||
acc.description =
|
||||
DescriptionMod::Set(format!("{} {}", existing, description));
|
||||
} else {
|
||||
acc.description = DescriptionMod::Set(description.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
acc
|
||||
}
|
||||
fold_many0(
|
||||
Self::description,
|
||||
Modification {
|
||||
..Default::default()
|
||||
},
|
||||
fold,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn description(input: ArgList) -> IResult<ArgList, ModArg> {
|
||||
fn to_modarg(input: &str) -> Result<ModArg, ()> {
|
||||
Ok(ModArg::Description(input))
|
||||
}
|
||||
map_res(arg_matching(any), to_modarg)(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let (input, modification) = Modification::parse(argv![]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
modification,
|
||||
Modification {
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_arg_description() {
|
||||
let (input, modification) = Modification::parse(argv!["newdesc"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
modification,
|
||||
Modification {
|
||||
description: DescriptionMod::Set("newdesc".to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_arg_description() {
|
||||
let (input, modification) = Modification::parse(argv!["new", "desc", "fun"]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
modification,
|
||||
Modification {
|
||||
description: DescriptionMod::Set("new desc fun".to_owned()),
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
33
cli/src/argparse/report.rs
Normal file
33
cli/src/argparse/report.rs
Normal file
|
@ -0,0 +1,33 @@
|
|||
use super::{ArgList, Filter};
|
||||
use nom::IResult;
|
||||
|
||||
/// A report specifies a filter as well as a sort order and information about which
|
||||
/// task attributes to display
|
||||
#[derive(Debug, PartialEq, Default)]
|
||||
pub(crate) struct Report {
|
||||
pub filter: Filter,
|
||||
}
|
||||
|
||||
impl Report {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Report> {
|
||||
let (input, filter) = Filter::parse(input)?;
|
||||
Ok((input, Report { filter }))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_empty() {
|
||||
let (input, report) = Report::parse(argv![]).unwrap();
|
||||
assert_eq!(input.len(), 0);
|
||||
assert_eq!(
|
||||
report,
|
||||
Report {
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
699
cli/src/argparse/subcommand.rs
Normal file
699
cli/src/argparse/subcommand.rs
Normal file
|
@ -0,0 +1,699 @@
|
|||
use super::args::*;
|
||||
use super::{ArgList, DescriptionMod, Filter, Modification, Report};
|
||||
use crate::usage;
|
||||
use nom::{branch::alt, combinator::*, sequence::*, IResult};
|
||||
use taskchampion::Status;
|
||||
use textwrap::dedent;
|
||||
|
||||
// IMPLEMENTATION NOTE:
|
||||
//
|
||||
// For each variant of Subcommand, there is a private, empty type of the same name with a `parse`
|
||||
// method and a `get_usage` method. The parse methods may handle several subcommands, but always
|
||||
// produce the variant of the same name as the type.
|
||||
//
|
||||
// This organization helps to gather the parsing and usage information into
|
||||
// comprehensible chunks of code, to ensure that everything is documented.
|
||||
|
||||
/// A subcommand is the specific operation that the CLI should execute.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub(crate) enum Subcommand {
|
||||
/// Display the tool version
|
||||
Version,
|
||||
|
||||
/// Display the help output
|
||||
Help {
|
||||
/// Give the summary help (fitting on a few lines)
|
||||
summary: bool,
|
||||
},
|
||||
|
||||
/// Add a new task
|
||||
Add {
|
||||
modification: Modification,
|
||||
},
|
||||
|
||||
/// Modify existing tasks
|
||||
Modify {
|
||||
filter: Filter,
|
||||
modification: Modification,
|
||||
},
|
||||
|
||||
/// Lists (reports)
|
||||
List {
|
||||
report: Report,
|
||||
},
|
||||
|
||||
/// Per-task information (typically one task)
|
||||
Info {
|
||||
filter: Filter,
|
||||
debug: bool,
|
||||
},
|
||||
|
||||
/// Basic operations without args
|
||||
Gc,
|
||||
Sync,
|
||||
}
|
||||
|
||||
impl Subcommand {
|
||||
pub(super) fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
alt((
|
||||
Version::parse,
|
||||
Help::parse,
|
||||
Add::parse,
|
||||
Modify::parse,
|
||||
List::parse,
|
||||
Info::parse,
|
||||
Gc::parse,
|
||||
Sync::parse,
|
||||
))(input)
|
||||
}
|
||||
|
||||
pub(super) fn get_usage(u: &mut usage::Usage) {
|
||||
Version::get_usage(u);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
struct Version;
|
||||
|
||||
impl Version {
|
||||
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(_: &str) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Version)
|
||||
}
|
||||
map_res(
|
||||
alt((
|
||||
arg_matching(literal("version")),
|
||||
arg_matching(literal("--version")),
|
||||
)),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn get_usage(u: &mut usage::Usage) {
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "version".to_owned(),
|
||||
syntax: "version".to_owned(),
|
||||
summary: "Show the TaskChampion version".to_owned(),
|
||||
description: "Show the version of the TaskChampion binary".to_owned(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct Help;
|
||||
|
||||
impl Help {
|
||||
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: &str) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Help {
|
||||
summary: input == "-h",
|
||||
})
|
||||
}
|
||||
map_res(
|
||||
alt((
|
||||
arg_matching(literal("help")),
|
||||
arg_matching(literal("--help")),
|
||||
arg_matching(literal("-h")),
|
||||
)),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn get_usage(_u: &mut usage::Usage) {}
|
||||
}
|
||||
|
||||
struct Add;
|
||||
|
||||
impl Add {
|
||||
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: (&str, Modification)) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Add {
|
||||
modification: input.1,
|
||||
})
|
||||
}
|
||||
map_res(
|
||||
pair(arg_matching(literal("add")), Modification::parse),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn get_usage(u: &mut usage::Usage) {
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "add".to_owned(),
|
||||
syntax: "add [modification]".to_owned(),
|
||||
summary: "Add a new task".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Add a new, pending task to the list of tasks. The modification must include a
|
||||
description.",
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct Modify;
|
||||
|
||||
impl Modify {
|
||||
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: (Filter, &str, Modification)) -> Result<Subcommand, ()> {
|
||||
let filter = input.0;
|
||||
let mut modification = input.2;
|
||||
|
||||
match input.1 {
|
||||
"prepend" => {
|
||||
if let DescriptionMod::Set(s) = modification.description {
|
||||
modification.description = DescriptionMod::Prepend(s)
|
||||
}
|
||||
}
|
||||
"append" => {
|
||||
if let DescriptionMod::Set(s) = modification.description {
|
||||
modification.description = DescriptionMod::Append(s)
|
||||
}
|
||||
}
|
||||
"start" => modification.active = Some(true),
|
||||
"stop" => modification.active = Some(false),
|
||||
"done" => modification.status = Some(Status::Completed),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(Subcommand::Modify {
|
||||
filter,
|
||||
modification,
|
||||
})
|
||||
}
|
||||
map_res(
|
||||
tuple((
|
||||
Filter::parse,
|
||||
alt((
|
||||
arg_matching(literal("modify")),
|
||||
arg_matching(literal("prepend")),
|
||||
arg_matching(literal("append")),
|
||||
arg_matching(literal("start")),
|
||||
arg_matching(literal("stop")),
|
||||
arg_matching(literal("done")),
|
||||
)),
|
||||
Modification::parse,
|
||||
)),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn get_usage(u: &mut usage::Usage) {
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "modify".to_owned(),
|
||||
syntax: "[filter] modify [modification]".to_owned(),
|
||||
summary: "Modify tasks".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Modify all tasks matching the filter.",
|
||||
),
|
||||
});
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "prepend".to_owned(),
|
||||
syntax: "[filter] prepend [modification]".to_owned(),
|
||||
summary: "Prepend task description".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Modify all tasks matching the filter by inserting the given description before each
|
||||
task's description.",
|
||||
),
|
||||
});
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "append".to_owned(),
|
||||
syntax: "[filter] append [modification]".to_owned(),
|
||||
summary: "Append task description".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Modify all tasks matching the filter by adding the given description to the end
|
||||
of each task's description.",
|
||||
),
|
||||
});
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "start".to_owned(),
|
||||
syntax: "[filter] start [modification]".to_owned(),
|
||||
summary: "Start tasks".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Start all tasks matching the filter, additionally applying any given modifications."),
|
||||
});
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "stop".to_owned(),
|
||||
syntax: "[filter] start [modification]".to_owned(),
|
||||
summary: "Stop tasks".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Stop all tasks matching the filter, additionally applying any given modifications.",
|
||||
),
|
||||
});
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "done".to_owned(),
|
||||
syntax: "[filter] start [modification]".to_owned(),
|
||||
summary: "Mark tasks as completed".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Mark all tasks matching the filter as completed, additionally applying any given
|
||||
modifications.",
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct List;
|
||||
|
||||
impl List {
|
||||
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: (Report, &str)) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::List { report: input.0 })
|
||||
}
|
||||
map_res(
|
||||
pair(Report::parse, arg_matching(literal("list"))),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn get_usage(u: &mut usage::Usage) {
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "list".to_owned(),
|
||||
syntax: "[filter] list".to_owned(),
|
||||
summary: "List tasks".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Show a list of the tasks matching the filter",
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct Info;
|
||||
|
||||
impl Info {
|
||||
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(input: (Filter, &str)) -> Result<Subcommand, ()> {
|
||||
let debug = input.1 == "debug";
|
||||
Ok(Subcommand::Info {
|
||||
filter: input.0,
|
||||
debug,
|
||||
})
|
||||
}
|
||||
map_res(
|
||||
pair(
|
||||
Filter::parse,
|
||||
alt((
|
||||
arg_matching(literal("info")),
|
||||
arg_matching(literal("debug")),
|
||||
)),
|
||||
),
|
||||
to_subcommand,
|
||||
)(input)
|
||||
}
|
||||
|
||||
fn get_usage(u: &mut usage::Usage) {
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "info".to_owned(),
|
||||
syntax: "[filter] info".to_owned(),
|
||||
summary: "Show tasks".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Show information about all tasks matching the fiter.",
|
||||
),
|
||||
});
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "debug".to_owned(),
|
||||
syntax: "[filter] debug".to_owned(),
|
||||
summary: "Show task debug details".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Show all key/value properties of the tasks matching the fiter.",
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct Gc;
|
||||
|
||||
impl Gc {
|
||||
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(_: &str) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Gc)
|
||||
}
|
||||
map_res(arg_matching(literal("gc")), to_subcommand)(input)
|
||||
}
|
||||
|
||||
fn get_usage(u: &mut usage::Usage) {
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "gc".to_owned(),
|
||||
syntax: "gc".to_owned(),
|
||||
summary: "Perform 'garbage collection'".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Perform 'garbage collection'. This refreshes the list of pending tasks
|
||||
and their short id's.",
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct Sync;
|
||||
|
||||
impl Sync {
|
||||
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||
fn to_subcommand(_: &str) -> Result<Subcommand, ()> {
|
||||
Ok(Subcommand::Sync)
|
||||
}
|
||||
map_res(arg_matching(literal("sync")), to_subcommand)(input)
|
||||
}
|
||||
|
||||
fn get_usage(u: &mut usage::Usage) {
|
||||
u.subcommands.push(usage::Subcommand {
|
||||
name: "sync".to_owned(),
|
||||
syntax: "sync".to_owned(),
|
||||
summary: "Synchronize this replica".to_owned(),
|
||||
description: dedent(
|
||||
"
|
||||
Synchronize this replica locally or against a remote server, as configured.
|
||||
|
||||
Synchronization is a critical part of maintaining the task database, and should
|
||||
be done regularly, even if only locally. It is typically run in a crontask.",
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
const EMPTY: Vec<&str> = vec![];
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["version"]).unwrap(),
|
||||
(&EMPTY[..], Subcommand::Version)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dd_version() {
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["--version"]).unwrap(),
|
||||
(&EMPTY[..], Subcommand::Version)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_d_h() {
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["-h"]).unwrap(),
|
||||
(&EMPTY[..], Subcommand::Help { summary: true })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_help() {
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["help"]).unwrap(),
|
||||
(&EMPTY[..], Subcommand::Help { summary: false })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dd_help() {
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["--help"]).unwrap(),
|
||||
(&EMPTY[..], Subcommand::Help { summary: false })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_description() {
|
||||
let subcommand = Subcommand::Add {
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set("foo".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["add", "foo"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_description_multi() {
|
||||
let subcommand = Subcommand::Add {
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set("foo bar".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["add", "foo", "bar"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modify_description_multi() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set("foo bar".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "modify", "foo", "bar"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Append("foo bar".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "append", "foo", "bar"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prepend() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Prepend("foo bar".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "prepend", "foo", "bar"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_done() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
status: Some(Status::Completed),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "done"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_done_modify() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set("now-finished".to_owned()),
|
||||
status: Some(Status::Completed),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "done", "now-finished"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
active: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "start"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_start_modify() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
active: Some(true),
|
||||
description: DescriptionMod::Set("mod".to_owned()),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "start", "mod"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
active: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "stop"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stop_modify() {
|
||||
let subcommand = Subcommand::Modify {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["123".to_owned()]),
|
||||
},
|
||||
modification: Modification {
|
||||
description: DescriptionMod::Set("mod".to_owned()),
|
||||
active: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["123", "stop", "mod"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let subcommand = Subcommand::List {
|
||||
report: Report {
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["list"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_filter() {
|
||||
let subcommand = Subcommand::List {
|
||||
report: Report {
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["12".to_owned(), "13".to_owned()]),
|
||||
},
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["12,13", "list"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_info_filter() {
|
||||
let subcommand = Subcommand::Info {
|
||||
debug: false,
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["12".to_owned(), "13".to_owned()]),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["12,13", "info"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_debug_filter() {
|
||||
let subcommand = Subcommand::Info {
|
||||
debug: true,
|
||||
filter: Filter {
|
||||
id_list: Some(vec!["12".to_owned()]),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["12", "debug"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gc() {
|
||||
let subcommand = Subcommand::Gc;
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["gc"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gc_extra_args() {
|
||||
let subcommand = Subcommand::Gc;
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["gc", "foo"]).unwrap(),
|
||||
(&vec!["foo"][..], subcommand)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sync() {
|
||||
let subcommand = Subcommand::Sync;
|
||||
assert_eq!(
|
||||
Subcommand::parse(argv!["sync"]).unwrap(),
|
||||
(&EMPTY[..], subcommand)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,44 +1,8 @@
|
|||
use clap::{Error as ClapError, ErrorKind};
|
||||
use std::process::exit;
|
||||
use taskchampion_cli::parse_command_line;
|
||||
|
||||
enum Output {
|
||||
Stdout,
|
||||
Stderr,
|
||||
}
|
||||
use Output::*;
|
||||
|
||||
fn bail<E: std::fmt::Display>(err: E, output: Output, code: i32) -> ! {
|
||||
match output {
|
||||
Stdout => println!("{}", err),
|
||||
Stderr => eprintln!("{}", err),
|
||||
}
|
||||
exit(code)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let command = match parse_command_line(std::env::args_os()) {
|
||||
Ok(command) => command,
|
||||
Err(err) => {
|
||||
match err.downcast::<ClapError>() {
|
||||
Ok(err) => {
|
||||
if err.kind == ErrorKind::HelpDisplayed
|
||||
|| err.kind == ErrorKind::VersionDisplayed
|
||||
{
|
||||
// --help and --version go to stdout and succeed
|
||||
bail(err, Stdout, 0)
|
||||
} else {
|
||||
// other clap errors exit with failure
|
||||
bail(err, Stderr, 1)
|
||||
}
|
||||
}
|
||||
Err(err) => bail(err, Stderr, 1),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = command.run() {
|
||||
bail(err, Stderr, 1)
|
||||
pub fn main() {
|
||||
if let Err(err) = taskchampion_cli::main() {
|
||||
eprintln!("{}", err);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
use clap::{App, Arg, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::{format_err, Fallible};
|
||||
use taskchampion::Status;
|
||||
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
description: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("add").about("adds a task").arg(
|
||||
Arg::with_name("description")
|
||||
.help("task description")
|
||||
.required(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("add", Some(matches)) => {
|
||||
// TODO: .unwrap() would be safe here as description is required above
|
||||
let description: String = match matches.value_of("description") {
|
||||
Some(v) => v.into(),
|
||||
None => return ArgMatchResult::Err(format_err!("no description provided")),
|
||||
};
|
||||
ArgMatchResult::Ok(Box::new(Invocation { description }))
|
||||
}
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let t = command
|
||||
.get_replica()?
|
||||
.new_task(Status::Pending, self.description.clone())
|
||||
.unwrap();
|
||||
println!("added task {}", t.get_uuid());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "add", "foo bar"], |inv: &Invocation| {
|
||||
assert_eq!(inv.description, "foo bar".to_string());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
use crate::cmd::shared;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
task: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("append").about("appends to a task description")
|
||||
.arg(shared::task_arg())
|
||||
.arg(
|
||||
Arg::with_name("description")
|
||||
.help("extra task description")
|
||||
.required(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("append", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
|
||||
task: matches.value_of("task").unwrap().into(),
|
||||
description: matches.value_of("description").unwrap().into(),
|
||||
})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let task = shared::get_task(&mut replica, &self.task)?;
|
||||
|
||||
let mut task = task.into_mut(&mut replica);
|
||||
|
||||
let new_description = format!("{} {}", task.get_description(), self.description.clone());
|
||||
task.set_description(new_description)?;
|
||||
println!("appended to task {}", task.get_uuid());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(
|
||||
vec!["task", "append", "1", "foo bar"],
|
||||
|inv: &Invocation| {
|
||||
assert_eq!(inv.description, "foo bar".to_string());
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
use crate::table;
|
||||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
use prettytable::{cell, row, Table};
|
||||
|
||||
use crate::cmd::{shared, ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
task: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("debug")
|
||||
.about("debug info for the given task")
|
||||
.arg(shared::task_arg()))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("debug", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
|
||||
task: matches.value_of("task").unwrap().into(),
|
||||
})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let task = shared::get_task(&mut replica, &self.task)?;
|
||||
|
||||
let mut t = Table::new();
|
||||
t.set_format(table::format());
|
||||
t.set_titles(row![b->"key", b->"value"]);
|
||||
for (k, v) in task.get_taskmap().iter() {
|
||||
t.add_row(row![k, v]);
|
||||
}
|
||||
t.printstd();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "debug", "1"], |inv: &Invocation| {
|
||||
assert_eq!(inv.task, "1".to_string());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
use taskchampion::Status;
|
||||
|
||||
use crate::cmd::{shared, ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
task: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("delete")
|
||||
.about("mark the given task as deleted")
|
||||
.arg(shared::task_arg()))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("delete", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
|
||||
task: matches.value_of("task").unwrap().into(),
|
||||
})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let task = shared::get_task(&mut replica, &self.task)?;
|
||||
let mut task = task.into_mut(&mut replica);
|
||||
task.stop()?;
|
||||
task.set_status(Status::Deleted)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "delete", "1"], |inv: &Invocation| {
|
||||
assert_eq!(inv.task, "1".to_string());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
use taskchampion::Status;
|
||||
|
||||
use crate::cmd::{shared, ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
task: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("done")
|
||||
.about("finish the given task (status Completed)")
|
||||
.arg(shared::task_arg()))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("done", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
|
||||
task: matches.value_of("task").unwrap().into(),
|
||||
})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let task = shared::get_task(&mut replica, &self.task)?;
|
||||
let mut task = task.into_mut(&mut replica);
|
||||
task.stop()?;
|
||||
task.set_status(Status::Completed)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "done", "1"], |inv: &Invocation| {
|
||||
assert_eq!(inv.task, "1".to_string());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(ClapSubCommand::with_name("gc").about("run garbage collection"))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("gc", _) => ArgMatchResult::Ok(Box::new(Invocation {})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
command.get_replica()?.gc()?;
|
||||
println!("garbage collected.");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "gc"], |_inv| {});
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
use crate::table;
|
||||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
use prettytable::{cell, row, Table};
|
||||
|
||||
use crate::cmd::{shared, ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
task: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("info")
|
||||
.about("info on the given task")
|
||||
.arg(shared::task_arg()))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("info", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
|
||||
task: matches.value_of("task").unwrap().into(),
|
||||
})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let task = shared::get_task(&mut replica, &self.task)?;
|
||||
let uuid = task.get_uuid();
|
||||
|
||||
let mut t = Table::new();
|
||||
t.set_format(table::format());
|
||||
t.add_row(row![b->"Uuid", uuid]);
|
||||
if let Some(i) = replica.get_working_set_index(uuid)? {
|
||||
t.add_row(row![b->"Id", i]);
|
||||
}
|
||||
t.add_row(row![b->"Description", task.get_description()]);
|
||||
t.add_row(row![b->"Status", task.get_status()]);
|
||||
t.add_row(row![b->"Active", task.is_active()]);
|
||||
t.printstd();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "info", "1"], |inv: &Invocation| {
|
||||
assert_eq!(inv.task, "1".to_string());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
use crate::table;
|
||||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
use prettytable::{cell, row, Table};
|
||||
use taskchampion::Status;
|
||||
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(ClapSubCommand::with_name("list").about("lists tasks"))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("list", _) => ArgMatchResult::Ok(Box::new(Invocation {})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let mut t = Table::new();
|
||||
t.set_format(table::format());
|
||||
t.set_titles(row![b->"id", b->"act", b->"description"]);
|
||||
for (uuid, task) in replica.all_tasks().unwrap() {
|
||||
if task.get_status() != Status::Pending {
|
||||
continue;
|
||||
}
|
||||
let mut id = uuid.to_string();
|
||||
if let Some(i) = replica.get_working_set_index(&uuid)? {
|
||||
id = i.to_string();
|
||||
}
|
||||
let active = match task.is_active() {
|
||||
true => "*",
|
||||
false => "",
|
||||
};
|
||||
t.add_row(row![id, active, task.get_description()]);
|
||||
}
|
||||
t.printstd();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "list"], |_inv| {});
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/// Define a Command type implementing SubCommand with the enclosed methods (`decorate_app` and
|
||||
/// `arg_match`), along with a module-level `cmd` function as the parent module expects.
|
||||
macro_rules! define_subcommand {
|
||||
($($f:item) +) => {
|
||||
struct Command;
|
||||
|
||||
pub(super) fn cmd() -> Box<dyn crate::cmd::SubCommand> {
|
||||
Box::new(Command)
|
||||
}
|
||||
|
||||
impl crate::cmd::SubCommand for Command {
|
||||
$($f)+
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Define an Invocation type implementing SubCommandInvocation with the enclosed methods.
|
||||
macro_rules! subcommand_invocation {
|
||||
($($f:item) +) => {
|
||||
impl crate::cmd::SubCommandInvocation for Invocation {
|
||||
$($f)+
|
||||
|
||||
#[cfg(test)]
|
||||
fn as_any(&self) -> &dyn std::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Parse the first argument as a command line and convert the result to an Invocation (which must
|
||||
/// be in scope). If the conversion works, calls the second argument with it.
|
||||
#[cfg(test)]
|
||||
macro_rules! with_subcommand_invocation {
|
||||
($args:expr, $check:expr) => {
|
||||
let parsed = crate::parse_command_line($args).unwrap();
|
||||
let si = parsed
|
||||
.subcommand
|
||||
.as_any()
|
||||
.downcast_ref::<Invocation>()
|
||||
.expect("SubComand is not of the expected type");
|
||||
($check)(si);
|
||||
};
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
use clap::{App, ArgMatches};
|
||||
use failure::{Error, Fallible};
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod shared;
|
||||
|
||||
mod add;
|
||||
mod append;
|
||||
mod debug;
|
||||
mod delete;
|
||||
mod done;
|
||||
mod gc;
|
||||
mod info;
|
||||
mod list;
|
||||
mod modify;
|
||||
mod pending;
|
||||
mod prepend;
|
||||
mod start;
|
||||
mod stop;
|
||||
mod sync;
|
||||
|
||||
/// Get a list of all subcommands in this crate
|
||||
pub(crate) fn subcommands() -> Vec<Box<dyn SubCommand>> {
|
||||
vec![
|
||||
add::cmd(),
|
||||
append::cmd(),
|
||||
debug::cmd(),
|
||||
delete::cmd(),
|
||||
done::cmd(),
|
||||
gc::cmd(),
|
||||
info::cmd(),
|
||||
list::cmd(),
|
||||
modify::cmd(),
|
||||
pending::cmd(),
|
||||
prepend::cmd(),
|
||||
start::cmd(),
|
||||
stop::cmd(),
|
||||
sync::cmd(),
|
||||
]
|
||||
}
|
||||
|
||||
/// The result of a [`crate::cmd::SubCommand::arg_match`] call
|
||||
pub(crate) enum ArgMatchResult {
|
||||
/// No match
|
||||
None,
|
||||
|
||||
/// A good match
|
||||
Ok(Box<dyn SubCommandInvocation>),
|
||||
|
||||
/// A match, but an issue with the command line
|
||||
Err(Error),
|
||||
}
|
||||
|
||||
/// A subcommand represents a defined subcommand, and is typically a singleton.
|
||||
pub(crate) trait SubCommand {
|
||||
/// Decorate the given [`clap::App`] appropriately for this subcommand
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a>;
|
||||
|
||||
/// If this ArgMatches is for this command, return an appropriate invocation.
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult;
|
||||
}
|
||||
|
||||
/// A subcommand invocation is specialized to a subcommand
|
||||
pub(crate) trait SubCommandInvocation: std::fmt::Debug {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()>;
|
||||
|
||||
// tests use downcasting, which requires a function to cast to Any
|
||||
#[cfg(test)]
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
}
|
||||
|
||||
pub use shared::CommandInvocation;
|
|
@ -1,63 +0,0 @@
|
|||
use crate::cmd::shared;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
task: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("modify").about("modifies a task")
|
||||
.arg(shared::task_arg())
|
||||
.arg(
|
||||
Arg::with_name("description")
|
||||
.help("task description")
|
||||
.required(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("modify", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
|
||||
task: matches.value_of("task").unwrap().into(),
|
||||
description: matches.value_of("description").unwrap().into(),
|
||||
})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let task = shared::get_task(&mut replica, &self.task)?;
|
||||
|
||||
let mut task = task.into_mut(&mut replica);
|
||||
task.set_description(self.description.clone())?;
|
||||
println!("modified task {}", task.get_uuid());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(
|
||||
vec!["task", "modify", "2", "foo bar"],
|
||||
|inv: &Invocation| {
|
||||
assert_eq!(inv.task, "2".to_string());
|
||||
assert_eq!(inv.description, "foo bar".to_string());
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
use crate::table;
|
||||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
use prettytable::{cell, row, Table};
|
||||
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(ClapSubCommand::with_name("pending").about("lists pending tasks"))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("pending", _) => ArgMatchResult::Ok(Box::new(Invocation {})),
|
||||
// default to this command when no subcommand is given
|
||||
("", _) => ArgMatchResult::Ok(Box::new(Invocation {})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let working_set = command.get_replica()?.working_set().unwrap();
|
||||
let mut t = Table::new();
|
||||
t.set_format(table::format());
|
||||
t.set_titles(row![b->"id", b->"act", b->"description"]);
|
||||
for (i, item) in working_set.iter().enumerate() {
|
||||
if let Some(ref task) = item {
|
||||
let active = match task.is_active() {
|
||||
true => "*",
|
||||
false => "",
|
||||
};
|
||||
t.add_row(row![i, active, task.get_description()]);
|
||||
}
|
||||
}
|
||||
t.printstd();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "pending"], |_inv| {});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_command_default() {
|
||||
with_subcommand_invocation!(vec!["task"], |_inv| {});
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
use crate::cmd::shared;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
task: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("prepend").about("add to the start of a task description")
|
||||
.arg(shared::task_arg())
|
||||
.arg(
|
||||
Arg::with_name("description")
|
||||
.help("extra task description")
|
||||
.required(true),
|
||||
),
|
||||
)
|
||||
}
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("prepend", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
|
||||
task: matches.value_of("task").unwrap().into(),
|
||||
description: matches.value_of("description").unwrap().into(),
|
||||
})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let task = shared::get_task(&mut replica, &self.task)?;
|
||||
|
||||
let mut task = task.into_mut(&mut replica);
|
||||
|
||||
let new_description = format!("{} {}", self.description.clone(), task.get_description());
|
||||
task.set_description(new_description)?;
|
||||
println!("prepended to task {}", task.get_uuid());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(
|
||||
vec!["task", "prepend", "1", "foo bar"],
|
||||
|inv: &Invocation| {
|
||||
assert_eq!(inv.description, "foo bar".to_string());
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
use crate::settings;
|
||||
use clap::Arg;
|
||||
use config::{Config, ConfigError};
|
||||
use failure::{format_err, Fallible};
|
||||
use std::cell::{Ref, RefCell};
|
||||
use taskchampion::{server, Replica, ReplicaConfig, ServerConfig, Task, Uuid};
|
||||
|
||||
pub(super) fn task_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name("task")
|
||||
.help("task id or uuid")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
pub(super) fn get_task<S: AsRef<str>>(replica: &mut Replica, task_arg: S) -> Fallible<Task> {
|
||||
let task_arg = task_arg.as_ref();
|
||||
|
||||
// first try treating task as a working-set reference
|
||||
if let Ok(i) = task_arg.parse::<usize>() {
|
||||
if let Some(task) = replica.get_working_set_task(i)? {
|
||||
return Ok(task);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(uuid) = Uuid::parse_str(task_arg) {
|
||||
if let Some(task) = replica.get_task(&uuid)? {
|
||||
return Ok(task);
|
||||
}
|
||||
}
|
||||
|
||||
Err(format_err!("Cannot interpret {:?} as a task", task_arg))
|
||||
}
|
||||
|
||||
/// A command invocation contains all of the necessary regarding a single invocation of the CLI.
|
||||
#[derive(Debug)]
|
||||
pub struct CommandInvocation {
|
||||
pub(crate) subcommand: Box<dyn super::SubCommandInvocation>,
|
||||
settings: RefCell<Config>,
|
||||
}
|
||||
|
||||
impl CommandInvocation {
|
||||
pub(crate) fn new(subcommand: Box<dyn super::SubCommandInvocation>) -> Self {
|
||||
Self {
|
||||
subcommand,
|
||||
settings: RefCell::new(Config::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(self) -> Fallible<()> {
|
||||
self.subcommand.run(&self)
|
||||
}
|
||||
|
||||
// -- utilities for command invocations
|
||||
|
||||
pub(super) fn get_settings(&self) -> Fallible<Ref<Config>> {
|
||||
{
|
||||
// use the special `_loaded" config value to detect whether we have
|
||||
// loaded the configuration yet
|
||||
let mut settings = self.settings.borrow_mut();
|
||||
if let Err(ConfigError::NotFound(_)) = settings.get_bool("_loaded") {
|
||||
settings.merge(settings::read_settings()?)?;
|
||||
settings.set("_loaded", true)?;
|
||||
}
|
||||
}
|
||||
Ok(self.settings.borrow())
|
||||
}
|
||||
|
||||
pub(super) fn get_replica(&self) -> Fallible<Replica> {
|
||||
let settings = self.get_settings()?;
|
||||
let taskdb_dir = settings.get_str("data_dir")?.into();
|
||||
log::debug!("Replica data_dir: {:?}", taskdb_dir);
|
||||
let replica_config = ReplicaConfig { taskdb_dir };
|
||||
Ok(Replica::from_config(replica_config)?)
|
||||
}
|
||||
|
||||
pub(super) fn get_server(&self) -> Fallible<Box<dyn server::Server>> {
|
||||
let settings = self.get_settings()?;
|
||||
let client_id = settings.get_str("server_client_id")?;
|
||||
let client_id = Uuid::parse_str(&client_id)?;
|
||||
let origin = settings.get_str("server_origin")?;
|
||||
log::debug!("Using sync-server with origin {}", origin);
|
||||
log::debug!("Sync client ID: {}", client_id);
|
||||
Ok(server::from_config(ServerConfig::Remote {
|
||||
origin,
|
||||
client_id,
|
||||
})?)
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
|
||||
use crate::cmd::{shared, ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
task: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("start")
|
||||
.about("start the given task")
|
||||
.arg(shared::task_arg()))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("start", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
|
||||
task: matches.value_of("task").unwrap().into(),
|
||||
})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let task = shared::get_task(&mut replica, &self.task)?;
|
||||
task.into_mut(&mut replica).start()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "start", "1"], |inv: &Invocation| {
|
||||
assert_eq!(inv.task, "1".to_string());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
|
||||
use crate::cmd::{shared, ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {
|
||||
task: String,
|
||||
}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(
|
||||
ClapSubCommand::with_name("stop")
|
||||
.about("stop the given task")
|
||||
.arg(shared::task_arg()))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("stop", Some(matches)) => ArgMatchResult::Ok(Box::new(Invocation {
|
||||
task: matches.value_of("task").unwrap().into(),
|
||||
})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let task = shared::get_task(&mut replica, &self.task)?;
|
||||
task.into_mut(&mut replica).stop()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "stop", "1"], |inv: &Invocation| {
|
||||
assert_eq!(inv.task, "1".to_string());
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
use clap::{App, ArgMatches, SubCommand as ClapSubCommand};
|
||||
use failure::Fallible;
|
||||
|
||||
use crate::cmd::{ArgMatchResult, CommandInvocation};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Invocation {}
|
||||
|
||||
define_subcommand! {
|
||||
fn decorate_app<'a>(&self, app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app.subcommand(ClapSubCommand::with_name("sync").about("sync with the server"))
|
||||
}
|
||||
|
||||
fn arg_match<'a>(&self, matches: &ArgMatches<'a>) -> ArgMatchResult {
|
||||
match matches.subcommand() {
|
||||
("sync", _) => ArgMatchResult::Ok(Box::new(Invocation {})),
|
||||
_ => ArgMatchResult::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subcommand_invocation! {
|
||||
fn run(&self, command: &CommandInvocation) -> Fallible<()> {
|
||||
let mut replica = command.get_replica()?;
|
||||
let mut server = command.get_server()?;
|
||||
replica.sync(&mut server)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_command() {
|
||||
with_subcommand_invocation!(vec!["task", "sync"], |_inv| {});
|
||||
}
|
||||
}
|
42
cli/src/invocation/cmd/add.rs
Normal file
42
cli/src/invocation/cmd/add.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use crate::argparse::{DescriptionMod, Modification};
|
||||
use failure::Fallible;
|
||||
use taskchampion::{Replica, Status};
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
modification: Modification,
|
||||
) -> Fallible<()> {
|
||||
let description = match modification.description {
|
||||
DescriptionMod::Set(ref s) => s.clone(),
|
||||
_ => "(no description)".to_owned(),
|
||||
};
|
||||
let t = replica.new_task(Status::Pending, description).unwrap();
|
||||
write!(w, "added task {}\n", t.get_uuid())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::cmd::test::*;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
let modification = Modification {
|
||||
description: DescriptionMod::Set("my description".to_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
execute(&mut w, &mut replica, modification).unwrap();
|
||||
|
||||
// check that the task appeared..
|
||||
let task = replica.get_working_set_task(1).unwrap().unwrap();
|
||||
assert_eq!(task.get_description(), "my description");
|
||||
assert_eq!(task.get_status(), Status::Pending);
|
||||
|
||||
assert_eq!(w.into_string(), format!("added task {}\n", task.get_uuid()));
|
||||
}
|
||||
}
|
23
cli/src/invocation/cmd/gc.rs
Normal file
23
cli/src/invocation/cmd/gc.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use failure::Fallible;
|
||||
use taskchampion::Replica;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(w: &mut W, replica: &mut Replica) -> Fallible<()> {
|
||||
replica.gc()?;
|
||||
write!(w, "garbage collected.\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::cmd::test::*;
|
||||
|
||||
#[test]
|
||||
fn test_gc() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
execute(&mut w, &mut replica).unwrap();
|
||||
assert_eq!(&w.into_string(), "garbage collected.\n")
|
||||
}
|
||||
}
|
31
cli/src/invocation/cmd/help.rs
Normal file
31
cli/src/invocation/cmd/help.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use crate::usage::Usage;
|
||||
use failure::Fallible;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
command_name: String,
|
||||
summary: bool,
|
||||
) -> Fallible<()> {
|
||||
let usage = Usage::new();
|
||||
usage.write_help(w, command_name, summary)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::cmd::test::*;
|
||||
|
||||
#[test]
|
||||
fn test_summary() {
|
||||
let mut w = test_writer();
|
||||
execute(&mut w, "task".to_owned(), true).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_long() {
|
||||
let mut w = test_writer();
|
||||
execute(&mut w, "task".to_owned(), false).unwrap();
|
||||
}
|
||||
}
|
61
cli/src/invocation/cmd/info.rs
Normal file
61
cli/src/invocation/cmd/info.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
use crate::argparse::Filter;
|
||||
use crate::invocation::filtered_tasks;
|
||||
use crate::table;
|
||||
use failure::Fallible;
|
||||
use prettytable::{cell, row, Table};
|
||||
use taskchampion::Replica;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
filter: Filter,
|
||||
debug: bool,
|
||||
) -> Fallible<()> {
|
||||
for task in filtered_tasks(replica, &filter)? {
|
||||
let uuid = task.get_uuid();
|
||||
|
||||
let mut t = Table::new();
|
||||
t.set_format(table::format());
|
||||
if debug {
|
||||
t.set_titles(row![b->"key", b->"value"]);
|
||||
for (k, v) in task.get_taskmap().iter() {
|
||||
t.add_row(row![k, v]);
|
||||
}
|
||||
} else {
|
||||
t.add_row(row![b->"Uuid", uuid]);
|
||||
if let Some(i) = replica.get_working_set_index(uuid)? {
|
||||
t.add_row(row![b->"Id", i]);
|
||||
}
|
||||
t.add_row(row![b->"Description", task.get_description()]);
|
||||
t.add_row(row![b->"Status", task.get_status()]);
|
||||
t.add_row(row![b->"Active", task.is_active()]);
|
||||
}
|
||||
t.print(w)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::cmd::test::*;
|
||||
use taskchampion::Status;
|
||||
|
||||
#[test]
|
||||
fn test_info() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
replica
|
||||
.new_task(Status::Pending, "my task".to_owned())
|
||||
.unwrap();
|
||||
|
||||
let filter = Filter {
|
||||
..Default::default()
|
||||
};
|
||||
let debug = false;
|
||||
execute(&mut w, &mut replica, filter, debug).unwrap();
|
||||
assert!(w.into_string().contains("my task"));
|
||||
}
|
||||
}
|
56
cli/src/invocation/cmd/list.rs
Normal file
56
cli/src/invocation/cmd/list.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use crate::argparse::Report;
|
||||
use crate::invocation::filtered_tasks;
|
||||
use crate::table;
|
||||
use failure::Fallible;
|
||||
use prettytable::{cell, row, Table};
|
||||
use taskchampion::Replica;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
report: Report,
|
||||
) -> Fallible<()> {
|
||||
let mut t = Table::new();
|
||||
t.set_format(table::format());
|
||||
t.set_titles(row![b->"id", b->"act", b->"description"]);
|
||||
for task in filtered_tasks(replica, &report.filter)? {
|
||||
let uuid = task.get_uuid();
|
||||
let mut id = uuid.to_string();
|
||||
if let Some(i) = replica.get_working_set_index(&uuid)? {
|
||||
id = i.to_string();
|
||||
}
|
||||
let active = match task.is_active() {
|
||||
true => "*",
|
||||
false => "",
|
||||
};
|
||||
t.add_row(row![id, active, task.get_description()]);
|
||||
}
|
||||
t.print(w)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::Filter;
|
||||
use crate::invocation::cmd::test::*;
|
||||
use taskchampion::Status;
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
replica
|
||||
.new_task(Status::Pending, "my task".to_owned())
|
||||
.unwrap();
|
||||
|
||||
let report = Report {
|
||||
filter: Filter {
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
execute(&mut w, &mut replica, report).unwrap();
|
||||
assert!(w.into_string().contains("my task"));
|
||||
}
|
||||
}
|
13
cli/src/invocation/cmd/mod.rs
Normal file
13
cli/src/invocation/cmd/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
//! Responsible for executing commands as parsed by [`crate::argparse`].
|
||||
|
||||
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 sync;
|
||||
pub(crate) mod version;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
58
cli/src/invocation/cmd/modify.rs
Normal file
58
cli/src/invocation/cmd/modify.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use crate::argparse::{Filter, Modification};
|
||||
use crate::invocation::{apply_modification, filtered_tasks};
|
||||
use failure::Fallible;
|
||||
use taskchampion::Replica;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
filter: Filter,
|
||||
modification: Modification,
|
||||
) -> Fallible<()> {
|
||||
for task in filtered_tasks(replica, &filter)? {
|
||||
let mut task = task.into_mut(replica);
|
||||
|
||||
apply_modification(w, &mut task, &modification)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::DescriptionMod;
|
||||
use crate::invocation::cmd::test::test_replica;
|
||||
use crate::invocation::cmd::test::*;
|
||||
use taskchampion::Status;
|
||||
|
||||
#[test]
|
||||
fn test_modify() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
|
||||
let task = replica
|
||||
.new_task(Status::Pending, "old description".to_owned())
|
||||
.unwrap();
|
||||
|
||||
let filter = Filter {
|
||||
..Default::default()
|
||||
};
|
||||
let modification = Modification {
|
||||
description: DescriptionMod::Set("new description".to_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
execute(&mut w, &mut replica, filter, modification).unwrap();
|
||||
|
||||
// check that the task appeared..
|
||||
let task = replica.get_task(task.get_uuid()).unwrap().unwrap();
|
||||
assert_eq!(task.get_description(), "new description");
|
||||
assert_eq!(task.get_status(), Status::Pending);
|
||||
|
||||
assert_eq!(
|
||||
w.into_string(),
|
||||
format!("modified task {}\n", task.get_uuid())
|
||||
);
|
||||
}
|
||||
}
|
32
cli/src/invocation/cmd/sync.rs
Normal file
32
cli/src/invocation/cmd/sync.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use failure::Fallible;
|
||||
use taskchampion::{server::Server, Replica};
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
server: &mut Box<dyn Server>,
|
||||
) -> Fallible<()> {
|
||||
replica.sync(server)?;
|
||||
write!(w, "sync complete.\n")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::cmd::test::*;
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
let server_dir = TempDir::new("test").unwrap();
|
||||
let mut server = test_server(&server_dir);
|
||||
|
||||
// Note that the details of the actual sync are tested thoroughly in the taskchampion crate
|
||||
execute(&mut w, &mut replica, &mut server).unwrap();
|
||||
assert_eq!(&w.into_string(), "sync complete.\n")
|
||||
}
|
||||
}
|
50
cli/src/invocation/cmd/test.rs
Normal file
50
cli/src/invocation/cmd/test.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use std::io;
|
||||
use taskchampion::{server, taskstorage, Replica, ServerConfig};
|
||||
use tempdir::TempDir;
|
||||
|
||||
pub(super) fn test_replica() -> Replica {
|
||||
let storage = taskstorage::InMemoryStorage::new();
|
||||
Replica::new(Box::new(storage))
|
||||
}
|
||||
|
||||
pub(super) fn test_server(dir: &TempDir) -> Box<dyn server::Server> {
|
||||
server::from_config(ServerConfig::Local {
|
||||
server_dir: dir.path().to_path_buf(),
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(super) struct TestWriter {
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TestWriter {
|
||||
pub(super) fn into_string(self) -> String {
|
||||
String::from_utf8(self.data).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Write for TestWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.data.write(buf)
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.data.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl termcolor::WriteColor for TestWriter {
|
||||
fn supports_color(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn set_color(&mut self, _spec: &termcolor::ColorSpec) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn reset(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn test_writer() -> TestWriter {
|
||||
TestWriter { data: vec![] }
|
||||
}
|
23
cli/src/invocation/cmd/version.rs
Normal file
23
cli/src/invocation/cmd/version.rs
Normal file
|
@ -0,0 +1,23 @@
|
|||
use failure::Fallible;
|
||||
use termcolor::{ColorSpec, WriteColor};
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(w: &mut W) -> Fallible<()> {
|
||||
write!(w, "TaskChampion ")?;
|
||||
w.set_color(ColorSpec::new().set_bold(true))?;
|
||||
write!(w, "{}\n", env!("CARGO_PKG_VERSION"))?;
|
||||
w.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::cmd::test::*;
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
let mut w = test_writer();
|
||||
execute(&mut w).unwrap();
|
||||
assert!(w.into_string().starts_with("TaskChampion "));
|
||||
}
|
||||
}
|
38
cli/src/invocation/filter.rs
Normal file
38
cli/src/invocation/filter.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use crate::argparse::Filter;
|
||||
use failure::Fallible;
|
||||
use taskchampion::{Replica, Task};
|
||||
|
||||
/// Return the tasks matching the given filter.
|
||||
pub(super) fn filtered_tasks(
|
||||
replica: &mut Replica,
|
||||
filter: &Filter,
|
||||
) -> Fallible<impl Iterator<Item = Task>> {
|
||||
// For the moment, this gets the entire set of tasks and then iterates
|
||||
// over the result. A few optimizations are possible:
|
||||
//
|
||||
// - id_list could be better parsed (id, uuid-fragment, uuid) in argparse
|
||||
// - depending on the nature of the filter, we could just scan the working set
|
||||
// - we could produce the tasks on-demand (but at the cost of holding a ref
|
||||
// to the replica, preventing modifying tasks..)
|
||||
let mut res = vec![];
|
||||
'task: for (uuid, task) in replica.all_tasks()?.drain() {
|
||||
if let Some(ref ids) = filter.id_list {
|
||||
for id in ids {
|
||||
if let Ok(index) = id.parse::<usize>() {
|
||||
if replica.get_working_set_index(&uuid)? == Some(index) {
|
||||
res.push(task);
|
||||
continue 'task;
|
||||
}
|
||||
} else if uuid.to_string().starts_with(id) {
|
||||
res.push(task);
|
||||
continue 'task;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// default to returning all tasks
|
||||
res.push(task);
|
||||
continue 'task;
|
||||
}
|
||||
}
|
||||
Ok(res.into_iter())
|
||||
}
|
121
cli/src/invocation/mod.rs
Normal file
121
cli/src/invocation/mod.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
//! The invocation module handles invoking the commands parsed by the argparse module.
|
||||
|
||||
use crate::argparse::{Command, Subcommand};
|
||||
use config::Config;
|
||||
use failure::Fallible;
|
||||
use taskchampion::{server, Replica, ReplicaConfig, ServerConfig, Uuid};
|
||||
use termcolor::{ColorChoice, StandardStream};
|
||||
|
||||
mod cmd;
|
||||
mod filter;
|
||||
mod modify;
|
||||
|
||||
use filter::filtered_tasks;
|
||||
use modify::apply_modification;
|
||||
|
||||
/// Invoke the given Command in the context of the given settings
|
||||
pub(crate) fn invoke(command: Command, settings: Config) -> Fallible<()> {
|
||||
log::debug!("command: {:?}", command);
|
||||
log::debug!("settings: {:?}", settings);
|
||||
|
||||
let mut w = get_writer()?;
|
||||
|
||||
// This function examines the command and breaks out the necessary bits to call one of the
|
||||
// `execute` functions in a submodule of `cmd`.
|
||||
|
||||
// match the subcommands that do not require a replica first, before
|
||||
// getting the replica
|
||||
match command {
|
||||
Command {
|
||||
subcommand: Subcommand::Help { summary },
|
||||
command_name,
|
||||
} => return cmd::help::execute(&mut w, command_name, summary),
|
||||
Command {
|
||||
subcommand: Subcommand::Version,
|
||||
..
|
||||
} => return cmd::version::execute(&mut w),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut replica = get_replica(&settings)?;
|
||||
match command {
|
||||
Command {
|
||||
subcommand: Subcommand::Add { modification },
|
||||
..
|
||||
} => return cmd::add::execute(&mut w, &mut replica, modification),
|
||||
|
||||
Command {
|
||||
subcommand:
|
||||
Subcommand::Modify {
|
||||
filter,
|
||||
modification,
|
||||
},
|
||||
..
|
||||
} => return cmd::modify::execute(&mut w, &mut replica, filter, modification),
|
||||
|
||||
Command {
|
||||
subcommand: Subcommand::List { report },
|
||||
..
|
||||
} => return cmd::list::execute(&mut w, &mut replica, report),
|
||||
|
||||
Command {
|
||||
subcommand: Subcommand::Info { filter, debug },
|
||||
..
|
||||
} => return cmd::info::execute(&mut w, &mut replica, filter, debug),
|
||||
|
||||
Command {
|
||||
subcommand: Subcommand::Gc,
|
||||
..
|
||||
} => return cmd::gc::execute(&mut w, &mut replica),
|
||||
|
||||
Command {
|
||||
subcommand: Subcommand::Sync,
|
||||
..
|
||||
} => {
|
||||
let mut server = get_server(&settings)?;
|
||||
return cmd::sync::execute(&mut w, &mut replica, &mut server);
|
||||
}
|
||||
|
||||
// handled in the first match, but here to ensure this match is exhaustive
|
||||
Command {
|
||||
subcommand: Subcommand::Help { .. },
|
||||
..
|
||||
} => unreachable!(),
|
||||
Command {
|
||||
subcommand: Subcommand::Version,
|
||||
..
|
||||
} => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
// utilities for invoke
|
||||
|
||||
/// Get the replica for this invocation
|
||||
fn get_replica(settings: &Config) -> Fallible<Replica> {
|
||||
let taskdb_dir = settings.get_str("data_dir")?.into();
|
||||
log::debug!("Replica data_dir: {:?}", taskdb_dir);
|
||||
let replica_config = ReplicaConfig { taskdb_dir };
|
||||
Ok(Replica::from_config(replica_config)?)
|
||||
}
|
||||
|
||||
/// Get the server for this invocation
|
||||
fn get_server(settings: &Config) -> Fallible<Box<dyn server::Server>> {
|
||||
let client_id = settings.get_str("server_client_id")?;
|
||||
let client_id = Uuid::parse_str(&client_id)?;
|
||||
let origin = settings.get_str("server_origin")?;
|
||||
log::debug!("Using sync-server with origin {}", origin);
|
||||
log::debug!("Sync client ID: {}", client_id);
|
||||
Ok(server::from_config(ServerConfig::Remote {
|
||||
origin,
|
||||
client_id,
|
||||
})?)
|
||||
}
|
||||
|
||||
/// Get a WriteColor implementation based on whether the output is a tty.
|
||||
fn get_writer() -> Fallible<StandardStream> {
|
||||
Ok(StandardStream::stdout(if atty::is(atty::Stream::Stdout) {
|
||||
ColorChoice::Auto
|
||||
} else {
|
||||
ColorChoice::Never
|
||||
}))
|
||||
}
|
38
cli/src/invocation/modify.rs
Normal file
38
cli/src/invocation/modify.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use crate::argparse::{DescriptionMod, Modification};
|
||||
use failure::Fallible;
|
||||
use taskchampion::TaskMut;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
/// Apply the given modification
|
||||
pub(super) fn apply_modification<W: WriteColor>(
|
||||
w: &mut W,
|
||||
task: &mut TaskMut,
|
||||
modification: &Modification,
|
||||
) -> Fallible<()> {
|
||||
match modification.description {
|
||||
DescriptionMod::Set(ref description) => task.set_description(description.clone())?,
|
||||
DescriptionMod::Prepend(ref description) => {
|
||||
task.set_description(format!("{} {}", description, task.get_description()))?
|
||||
}
|
||||
DescriptionMod::Append(ref description) => {
|
||||
task.set_description(format!("{} {}", task.get_description(), description))?
|
||||
}
|
||||
DescriptionMod::None => {}
|
||||
}
|
||||
|
||||
if let Some(ref status) = modification.status {
|
||||
task.set_status(status.clone())?;
|
||||
}
|
||||
|
||||
if let Some(true) = modification.active {
|
||||
task.start()?;
|
||||
}
|
||||
|
||||
if let Some(false) = modification.active {
|
||||
task.stop()?;
|
||||
}
|
||||
|
||||
write!(w, "modified task {}\n", task.get_uuid())?;
|
||||
|
||||
Ok(())
|
||||
}
|
106
cli/src/lib.rs
106
cli/src/lib.rs
|
@ -1,60 +1,64 @@
|
|||
use clap::{App, AppSettings};
|
||||
/*!
|
||||
This crate implements the command-line interface to TaskChampion.
|
||||
|
||||
## Design
|
||||
|
||||
The crate is split into two parts: argument parsing (`argparse`) and command invocation (`invocation`).
|
||||
Both are fairly complex operations, and the split serves both to isolate that complexity and to facilitate testing.
|
||||
|
||||
### Argparse
|
||||
|
||||
The TaskChampion command line API is modeled on TaskWarrior's API, which is far from that of a typical UNIX command.
|
||||
Tools like `clap` and `structopt` are not flexible enough to handle this syntax.
|
||||
|
||||
Instead, the `argparse` module uses [nom](https://crates.io/crates/nom) to parse command lines as a sequence of words.
|
||||
These parsers act on a list of strings, `&[&str]`, and at the top level return a `crate::argparse::Command`.
|
||||
This is a wholly-owned repesentation of the command line's meaning, but with some interpretation.
|
||||
For example, `task start`, `task stop`, and `task append` all map to a `crate::argparse::Subcommand::Modify` variant.
|
||||
|
||||
### Invocation
|
||||
|
||||
The `invocation` module executes a `Command`, given some settings and other ancillary data.
|
||||
Most of its functionality is in common functions to handle filtering tasks, modifying tasks, and so on.
|
||||
|
||||
## Rust API
|
||||
|
||||
Note that this crate does not expose a Rust API for use from other crates.
|
||||
For the public TaskChampion Rust API, see the `taskchampion` crate.
|
||||
|
||||
*/
|
||||
|
||||
use failure::Fallible;
|
||||
use std::ffi::OsString;
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
mod cmd;
|
||||
pub(crate) mod settings;
|
||||
// NOTE: it's important that this 'mod' comes first so that the macros can be used in other modules
|
||||
mod macros;
|
||||
|
||||
mod argparse;
|
||||
mod invocation;
|
||||
mod settings;
|
||||
mod table;
|
||||
mod usage;
|
||||
|
||||
use cmd::ArgMatchResult;
|
||||
pub(crate) use cmd::CommandInvocation;
|
||||
/// The main entry point for the command-line interface. This builds an Invocation
|
||||
/// from the particulars of the operating-system interface, and then executes it.
|
||||
pub fn main() -> Fallible<()> {
|
||||
env_logger::init();
|
||||
|
||||
/// Parse the given command line and return an as-yet un-executed CommandInvocation.
|
||||
pub fn parse_command_line<I, T>(iter: I) -> Fallible<CommandInvocation>
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
T: Into<OsString> + Clone,
|
||||
{
|
||||
let subcommands = cmd::subcommands();
|
||||
// parse the command line into a vector of &str, failing if
|
||||
// there are invalid utf-8 sequences.
|
||||
let argv: Vec<String> = std::env::args_os()
|
||||
.map(|oss| String::from_utf8(oss.into_vec()))
|
||||
.collect::<Result<_, FromUtf8Error>>()?;
|
||||
let argv: Vec<&str> = argv.iter().map(|s| s.as_ref()).collect();
|
||||
|
||||
let mut app = App::new("TaskChampion")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("Personal task-tracking")
|
||||
.setting(AppSettings::ColoredHelp);
|
||||
// parse the command line
|
||||
let command = argparse::Command::from_argv(&argv[..])?;
|
||||
|
||||
for subcommand in subcommands.iter() {
|
||||
app = subcommand.decorate_app(app);
|
||||
}
|
||||
// load the application settings
|
||||
let settings = settings::read_settings()?;
|
||||
|
||||
let matches = app.get_matches_from_safe(iter)?;
|
||||
|
||||
for subcommand in subcommands.iter() {
|
||||
match subcommand.arg_match(&matches) {
|
||||
ArgMatchResult::Ok(invocation) => return Ok(CommandInvocation::new(invocation)),
|
||||
ArgMatchResult::Err(err) => return Err(err),
|
||||
ArgMatchResult::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
// one of the subcommands also matches the lack of subcommands, so this never
|
||||
// occurrs.
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_command_line_success() -> Fallible<()> {
|
||||
// This just verifies that one of the subcommands works; the subcommands themselves
|
||||
// are tested in their own unit tests.
|
||||
parse_command_line(vec!["task", "pending"].iter())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_command_line_failure() {
|
||||
assert!(parse_command_line(vec!["task", "--no-such-arg"].iter()).is_err());
|
||||
}
|
||||
invocation::invoke(command, settings)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
12
cli/src/macros.rs
Normal file
12
cli/src/macros.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
#![macro_use]
|
||||
|
||||
/// create a &[&str] from vec notation
|
||||
#[cfg(test)]
|
||||
macro_rules! argv {
|
||||
() => (
|
||||
&[][..]
|
||||
);
|
||||
($($x:expr),* $(,)?) => (
|
||||
&[$($x),*][..]
|
||||
);
|
||||
}
|
83
cli/src/usage.rs
Normal file
83
cli/src/usage.rs
Normal file
|
@ -0,0 +1,83 @@
|
|||
//! This module handles creation of CLI usage documents (--help, manpages, etc.) in
|
||||
//! a way that puts the source of that documentation near its implementation.
|
||||
|
||||
use crate::argparse;
|
||||
use std::io::{Result, Write};
|
||||
use textwrap::indent;
|
||||
|
||||
/// A top-level structure containing usage/help information for the entire CLI.
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Usage {
|
||||
pub(crate) subcommands: Vec<Subcommand>,
|
||||
}
|
||||
|
||||
impl Usage {
|
||||
/// Get a new, completely-filled-out usage object
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut rv = Self {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
argparse::get_usage(&mut rv);
|
||||
|
||||
// TODO: sort subcommands
|
||||
|
||||
rv
|
||||
}
|
||||
|
||||
/// Write this usage to the given output as a help string, writing a short version if `summary`
|
||||
/// is true.
|
||||
pub(crate) fn write_help<W: Write>(
|
||||
&self,
|
||||
mut w: W,
|
||||
command_name: String,
|
||||
summary: bool,
|
||||
) -> Result<()> {
|
||||
write!(
|
||||
w,
|
||||
"TaskChampion {}: Personal task-tracking\n\n",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
)?;
|
||||
write!(w, "USAGE:\n {} [args]\n\n", command_name)?;
|
||||
write!(w, "TaskChampion subcommands:\n")?;
|
||||
for subcommand in self.subcommands.iter() {
|
||||
subcommand.write_help(&mut w, summary)?;
|
||||
}
|
||||
if !summary {
|
||||
write!(w, "\nSee `task help` for more detail\n")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Subcommand {
|
||||
/// Name of the subcommand
|
||||
pub(crate) name: String,
|
||||
|
||||
/// Syntax summary, without command_name
|
||||
pub(crate) syntax: String,
|
||||
|
||||
/// One-line description of the subcommand. Use an initial capital and no trailing period.
|
||||
pub(crate) summary: String,
|
||||
|
||||
/// Multi-line description of the subcommand. It's OK for this to duplicate summary, as the
|
||||
/// two are not displayed together.
|
||||
pub(crate) description: String,
|
||||
}
|
||||
|
||||
impl Subcommand {
|
||||
fn write_help<W: Write>(&self, mut w: W, summary: bool) -> Result<()> {
|
||||
if summary {
|
||||
write!(w, " task {} - {}\n", self.name, self.summary)?;
|
||||
} else {
|
||||
write!(
|
||||
w,
|
||||
" task {}\n{}\n",
|
||||
self.syntax,
|
||||
indent(self.description.trim(), " ")
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@ use assert_cmd::prelude::*;
|
|||
use predicates::prelude::*;
|
||||
use std::process::Command;
|
||||
|
||||
// This tests that the task binary is running and parsing arguments. The details of subcommands
|
||||
// are handled with unit tests.
|
||||
// NOTE: This tests that the task binary is running and parsing arguments. The details of
|
||||
// subcommands are handled with unit tests.
|
||||
|
||||
#[test]
|
||||
fn help() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
@ -36,7 +36,7 @@ fn invalid_option() -> Result<(), Box<dyn std::error::Error>> {
|
|||
cmd.arg("--no-such-option");
|
||||
cmd.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains("USAGE"));
|
||||
.stderr(predicate::str::contains("command line not recognized"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ impl Priority {
|
|||
}
|
||||
|
||||
/// The status of a task. The default status in "Pending".
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Status {
|
||||
Pending,
|
||||
Completed,
|
||||
|
|
|
@ -2,13 +2,11 @@ use failure::Fallible;
|
|||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(test)]
|
||||
mod inmemory;
|
||||
mod kv;
|
||||
mod operation;
|
||||
|
||||
pub use self::kv::KVStorage;
|
||||
#[cfg(test)]
|
||||
pub use inmemory::InMemoryStorage;
|
||||
|
||||
pub use operation::Operation;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue