Switch to a command-line API closer to TaskWarrior
* Use a parser (rather than clap) to process the command line * Outline some generic support for filtering, reporting, modifying, etc. * Break argument parsing strictly from invocation, to allow independent testing
This commit is contained in:
34
cli/src/invocation/cmd/add.rs
Normal file
34
cli/src/invocation/cmd/add.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use crate::argparse::{DescriptionMod, Modification};
|
||||
use failure::Fallible;
|
||||
use taskchampion::{Replica, Status};
|
||||
|
||||
pub(crate) fn execute(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();
|
||||
println!("added task {}", t.get_uuid());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::cmd::test::test_replica;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let mut replica = test_replica();
|
||||
let modification = Modification {
|
||||
description: DescriptionMod::Set("my description".to_owned()),
|
||||
..Default::default()
|
||||
};
|
||||
execute(&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);
|
||||
}
|
||||
}
|
||||
21
cli/src/invocation/cmd/gc.rs
Normal file
21
cli/src/invocation/cmd/gc.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use failure::Fallible;
|
||||
use taskchampion::Replica;
|
||||
|
||||
pub(crate) fn execute(replica: &mut Replica) -> Fallible<()> {
|
||||
replica.gc()?;
|
||||
println!("garbage collected.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::cmd::test::test_replica;
|
||||
|
||||
#[test]
|
||||
fn test_gc() {
|
||||
let mut replica = test_replica();
|
||||
execute(&mut replica).unwrap();
|
||||
// this mostly just needs to not fail!
|
||||
}
|
||||
}
|
||||
28
cli/src/invocation/cmd/help.rs
Normal file
28
cli/src/invocation/cmd/help.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use failure::Fallible;
|
||||
|
||||
pub(crate) fn execute(command_name: String, summary: bool) -> Fallible<()> {
|
||||
println!(
|
||||
"TaskChampion {}: Personal task-tracking",
|
||||
env!("CARGO_PKG_VERSION")
|
||||
);
|
||||
if !summary {
|
||||
println!();
|
||||
println!("USAGE: {} [args]\n(help output TODO)", command_name); // TODO
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_summary() {
|
||||
execute("task".to_owned(), true).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_long() {
|
||||
execute("task".to_owned(), false).unwrap();
|
||||
}
|
||||
}
|
||||
49
cli/src/invocation/cmd/info.rs
Normal file
49
cli/src/invocation/cmd/info.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use crate::argparse::Filter;
|
||||
use crate::invocation::filtered_tasks;
|
||||
use crate::table;
|
||||
use failure::Fallible;
|
||||
use prettytable::{cell, row, Table};
|
||||
use taskchampion::Replica;
|
||||
|
||||
pub(crate) fn execute(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.printstd();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::cmd::test::test_replica;
|
||||
|
||||
#[test]
|
||||
fn test_info() {
|
||||
let mut replica = test_replica();
|
||||
let filter = Filter {
|
||||
..Default::default()
|
||||
};
|
||||
let debug = false;
|
||||
execute(&mut replica, filter, debug).unwrap();
|
||||
// output is to stdout, so this is as much as we can check
|
||||
}
|
||||
}
|
||||
45
cli/src/invocation/cmd/list.rs
Normal file
45
cli/src/invocation/cmd/list.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use crate::argparse::Report;
|
||||
use crate::invocation::filtered_tasks;
|
||||
use crate::table;
|
||||
use failure::Fallible;
|
||||
use prettytable::{cell, row, Table};
|
||||
use taskchampion::Replica;
|
||||
|
||||
pub(crate) fn execute(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.printstd();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::Filter;
|
||||
use crate::invocation::cmd::test::test_replica;
|
||||
|
||||
#[test]
|
||||
fn test_list() {
|
||||
let mut replica = test_replica();
|
||||
let report = Report {
|
||||
filter: Filter {
|
||||
..Default::default()
|
||||
},
|
||||
};
|
||||
execute(&mut replica, report).unwrap();
|
||||
// output is to stdout, so this is as much as we can check
|
||||
}
|
||||
}
|
||||
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;
|
||||
49
cli/src/invocation/cmd/modify.rs
Normal file
49
cli/src/invocation/cmd/modify.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use crate::argparse::{Filter, Modification};
|
||||
use crate::invocation::{apply_modification, filtered_tasks};
|
||||
use failure::Fallible;
|
||||
use taskchampion::Replica;
|
||||
|
||||
pub(crate) fn execute(
|
||||
replica: &mut Replica,
|
||||
filter: Filter,
|
||||
modification: Modification,
|
||||
) -> Fallible<()> {
|
||||
for task in filtered_tasks(replica, &filter)? {
|
||||
let mut task = task.into_mut(replica);
|
||||
|
||||
apply_modification(&mut task, &modification)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::DescriptionMod;
|
||||
use crate::invocation::cmd::test::test_replica;
|
||||
use taskchampion::Status;
|
||||
|
||||
#[test]
|
||||
fn test_modify() {
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
26
cli/src/invocation/cmd/sync.rs
Normal file
26
cli/src/invocation/cmd/sync.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use failure::Fallible;
|
||||
use taskchampion::{server::Server, Replica};
|
||||
|
||||
pub(crate) fn execute(replica: &mut Replica, server: &mut Box<dyn Server>) -> Fallible<()> {
|
||||
replica.sync(server)?;
|
||||
println!("sync complete.");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::cmd::test::{test_replica, test_server};
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let mut replica = test_replica();
|
||||
let server_dir = TempDir::new("test").unwrap();
|
||||
let mut server = test_server(&server_dir);
|
||||
|
||||
// this just has to not fail -- the details of the actual sync are
|
||||
// tested thoroughly in the taskchampion crate
|
||||
execute(&mut replica, &mut server).unwrap();
|
||||
}
|
||||
}
|
||||
14
cli/src/invocation/cmd/test.rs
Normal file
14
cli/src/invocation/cmd/test.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
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()
|
||||
}
|
||||
16
cli/src/invocation/cmd/version.rs
Normal file
16
cli/src/invocation/cmd/version.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use failure::Fallible;
|
||||
|
||||
pub(crate) fn execute() -> Fallible<()> {
|
||||
println!("TaskChampion {}", env!("CARGO_PKG_VERSION"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
execute().unwrap();
|
||||
}
|
||||
}
|
||||
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())
|
||||
}
|
||||
109
cli/src/invocation/mod.rs
Normal file
109
cli/src/invocation/mod.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
//! 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};
|
||||
|
||||
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);
|
||||
|
||||
// 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(command_name, summary),
|
||||
Command {
|
||||
subcommand: Subcommand::Version,
|
||||
..
|
||||
} => return cmd::version::execute(),
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let mut replica = get_replica(&settings)?;
|
||||
match command {
|
||||
Command {
|
||||
subcommand: Subcommand::Add { modification },
|
||||
..
|
||||
} => return cmd::add::execute(&mut replica, modification),
|
||||
|
||||
Command {
|
||||
subcommand:
|
||||
Subcommand::Modify {
|
||||
filter,
|
||||
modification,
|
||||
},
|
||||
..
|
||||
} => return cmd::modify::execute(&mut replica, filter, modification),
|
||||
|
||||
Command {
|
||||
subcommand: Subcommand::List { report },
|
||||
..
|
||||
} => return cmd::list::execute(&mut replica, report),
|
||||
|
||||
Command {
|
||||
subcommand: Subcommand::Info { filter, debug },
|
||||
..
|
||||
} => return cmd::info::execute(&mut replica, filter, debug),
|
||||
|
||||
Command {
|
||||
subcommand: Subcommand::Gc,
|
||||
..
|
||||
} => return cmd::gc::execute(&mut replica),
|
||||
|
||||
Command {
|
||||
subcommand: Subcommand::Sync,
|
||||
..
|
||||
} => {
|
||||
let mut server = get_server(&settings)?;
|
||||
return cmd::sync::execute(&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,
|
||||
})?)
|
||||
}
|
||||
33
cli/src/invocation/modify.rs
Normal file
33
cli/src/invocation/modify.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use crate::argparse::{DescriptionMod, Modification};
|
||||
use failure::Fallible;
|
||||
use taskchampion::TaskMut;
|
||||
|
||||
/// Apply the given modification
|
||||
pub(super) fn apply_modification(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()?;
|
||||
}
|
||||
|
||||
println!("modified task {}", task.get_uuid());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user