mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Merge pull request #71 from djmitche/issue7
Add start and stop commands
This commit is contained in:
commit
47cd051bd7
9 changed files with 360 additions and 9 deletions
57
cli/src/cmd/debug.rs
Normal file
57
cli/src/cmd/debug.rs
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,6 +42,7 @@ subcommand_invocation! {
|
||||||
}
|
}
|
||||||
t.add_row(row![b->"Description", task.get_description()]);
|
t.add_row(row![b->"Description", task.get_description()]);
|
||||||
t.add_row(row![b->"Status", task.get_status()]);
|
t.add_row(row![b->"Status", task.get_status()]);
|
||||||
|
t.add_row(row![b->"Active", task.is_active()]);
|
||||||
t.printstd();
|
t.printstd();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,13 +26,17 @@ subcommand_invocation! {
|
||||||
let mut replica = command.get_replica()?;
|
let mut replica = command.get_replica()?;
|
||||||
let mut t = Table::new();
|
let mut t = Table::new();
|
||||||
t.set_format(table::format());
|
t.set_format(table::format());
|
||||||
t.set_titles(row![b->"id", b->"description"]);
|
t.set_titles(row![b->"id", b->"act", b->"description"]);
|
||||||
for (uuid, task) in replica.all_tasks().unwrap() {
|
for (uuid, task) in replica.all_tasks().unwrap() {
|
||||||
let mut id = uuid.to_string();
|
let mut id = uuid.to_string();
|
||||||
if let Some(i) = replica.get_working_set_index(&uuid)? {
|
if let Some(i) = replica.get_working_set_index(&uuid)? {
|
||||||
id = i.to_string();
|
id = i.to_string();
|
||||||
}
|
}
|
||||||
t.add_row(row![id, task.get_description()]);
|
let active = match task.is_active() {
|
||||||
|
true => "*",
|
||||||
|
false => "",
|
||||||
|
};
|
||||||
|
t.add_row(row![id, active, task.get_description()]);
|
||||||
}
|
}
|
||||||
t.printstd();
|
t.printstd();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -6,22 +6,28 @@ mod macros;
|
||||||
mod shared;
|
mod shared;
|
||||||
|
|
||||||
mod add;
|
mod add;
|
||||||
|
mod debug;
|
||||||
mod gc;
|
mod gc;
|
||||||
mod info;
|
mod info;
|
||||||
mod list;
|
mod list;
|
||||||
mod modify;
|
mod modify;
|
||||||
mod pending;
|
mod pending;
|
||||||
|
mod start;
|
||||||
|
mod stop;
|
||||||
mod sync;
|
mod sync;
|
||||||
|
|
||||||
/// Get a list of all subcommands in this crate
|
/// Get a list of all subcommands in this crate
|
||||||
pub(crate) fn subcommands() -> Vec<Box<dyn SubCommand>> {
|
pub(crate) fn subcommands() -> Vec<Box<dyn SubCommand>> {
|
||||||
vec![
|
vec![
|
||||||
add::cmd(),
|
add::cmd(),
|
||||||
|
debug::cmd(),
|
||||||
gc::cmd(),
|
gc::cmd(),
|
||||||
info::cmd(),
|
info::cmd(),
|
||||||
list::cmd(),
|
list::cmd(),
|
||||||
modify::cmd(),
|
modify::cmd(),
|
||||||
pending::cmd(),
|
pending::cmd(),
|
||||||
|
start::cmd(),
|
||||||
|
stop::cmd(),
|
||||||
sync::cmd(),
|
sync::cmd(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,10 +28,14 @@ subcommand_invocation! {
|
||||||
let working_set = command.get_replica()?.working_set().unwrap();
|
let working_set = command.get_replica()?.working_set().unwrap();
|
||||||
let mut t = Table::new();
|
let mut t = Table::new();
|
||||||
t.set_format(table::format());
|
t.set_format(table::format());
|
||||||
t.set_titles(row![b->"id", b->"description"]);
|
t.set_titles(row![b->"id", b->"act", b->"description"]);
|
||||||
for (i, item) in working_set.iter().enumerate() {
|
for (i, item) in working_set.iter().enumerate() {
|
||||||
if let Some(ref task) = item {
|
if let Some(ref task) = item {
|
||||||
t.add_row(row![i, task.get_description()]);
|
let active = match task.is_active() {
|
||||||
|
true => "*",
|
||||||
|
false => "",
|
||||||
|
};
|
||||||
|
t.add_row(row![i, active, task.get_description()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
t.printstd();
|
t.printstd();
|
||||||
|
|
48
cli/src/cmd/start.rs
Normal file
48
cli/src/cmd/start.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
48
cli/src/cmd/stop.rs
Normal file
48
cli/src/cmd/stop.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ The following keys, and key formats, are defined:
|
||||||
* `status` - one of `P` for a pending task (the default), `C` for completed or `D` for deleted
|
* `status` - one of `P` for a pending task (the default), `C` for completed or `D` for deleted
|
||||||
* `description` - the one-line summary of the task
|
* `description` - the one-line summary of the task
|
||||||
* `modified` - the time of the last modification of this task
|
* `modified` - the time of the last modification of this task
|
||||||
|
* `start.<timestamp>` - either an empty string (representing work on the task to the task that has not been stopped) or a timestamp (representing the time that work stopped)
|
||||||
|
|
||||||
The following are not yet implemented:
|
The following are not yet implemented:
|
||||||
|
|
||||||
|
|
|
@ -118,6 +118,10 @@ impl Task {
|
||||||
&self.uuid
|
&self.uuid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_taskmap(&self) -> &TaskMap {
|
||||||
|
&self.taskmap
|
||||||
|
}
|
||||||
|
|
||||||
/// Prepare to mutate this task, requiring a mutable Replica
|
/// Prepare to mutate this task, requiring a mutable Replica
|
||||||
/// in order to update the data it contains.
|
/// in order to update the data it contains.
|
||||||
pub fn into_mut(self, replica: &mut Replica) -> TaskMut {
|
pub fn into_mut(self, replica: &mut Replica) -> TaskMut {
|
||||||
|
@ -142,6 +146,16 @@ impl Task {
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine whether this task is active -- that is, that it has been started
|
||||||
|
/// and not stopped.
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.taskmap
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, v)| k.starts_with("start.") && v.is_empty())
|
||||||
|
.next()
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_modified(&self) -> Option<DateTime<Utc>> {
|
pub fn get_modified(&self) -> Option<DateTime<Utc>> {
|
||||||
self.get_timestamp("modified")
|
self.get_timestamp("modified")
|
||||||
}
|
}
|
||||||
|
@ -184,6 +198,33 @@ impl<'r> TaskMut<'r> {
|
||||||
self.set_timestamp("modified", Some(modified))
|
self.set_timestamp("modified", Some(modified))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start the task by creating "start.<timestamp": "", if the task is not already
|
||||||
|
/// active.
|
||||||
|
pub fn start(&mut self) -> Fallible<()> {
|
||||||
|
if self.is_active() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let k = format!("start.{}", Utc::now().timestamp());
|
||||||
|
self.set_string(k.as_ref(), Some(String::from("")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the task by adding the current timestamp to all un-resolved "start.<timestamp>" keys.
|
||||||
|
pub fn stop(&mut self) -> Fallible<()> {
|
||||||
|
let keys = self
|
||||||
|
.taskmap
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, v)| k.starts_with("start.") && v.is_empty())
|
||||||
|
.map(|(k, _)| k)
|
||||||
|
.cloned()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let now = Utc::now();
|
||||||
|
for key in keys {
|
||||||
|
println!("{}", key);
|
||||||
|
self.set_timestamp(&key, Some(now))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// -- utility functions
|
// -- utility functions
|
||||||
|
|
||||||
fn lastmod(&mut self) -> Fallible<()> {
|
fn lastmod(&mut self) -> Fallible<()> {
|
||||||
|
@ -212,11 +253,26 @@ impl<'r> TaskMut<'r> {
|
||||||
|
|
||||||
fn set_timestamp(&mut self, property: &str, value: Option<DateTime<Utc>>) -> Fallible<()> {
|
fn set_timestamp(&mut self, property: &str, value: Option<DateTime<Utc>>) -> Fallible<()> {
|
||||||
self.lastmod()?;
|
self.lastmod()?;
|
||||||
self.replica.update_task(
|
if let Some(value) = value {
|
||||||
self.task.uuid,
|
let ts = format!("{}", value.timestamp());
|
||||||
property,
|
self.replica
|
||||||
value.map(|v| format!("{}", v.timestamp())),
|
.update_task(self.task.uuid, property, Some(ts.clone()))?;
|
||||||
)
|
self.task.taskmap.insert(property.to_string(), ts);
|
||||||
|
} else {
|
||||||
|
self.replica
|
||||||
|
.update_task::<_, &str>(self.task.uuid, property, None)?;
|
||||||
|
self.task.taskmap.remove(property);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used by tests to ensure that updates are properly written
|
||||||
|
#[cfg(test)]
|
||||||
|
fn reload(&mut self) -> Fallible<()> {
|
||||||
|
let uuid = self.uuid;
|
||||||
|
let task = self.replica.get_task(&uuid)?.unwrap();
|
||||||
|
self.task.taskmap = task.taskmap;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,6 +288,132 @@ impl<'r> std::ops::Deref for TaskMut<'r> {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
fn with_mut_task<F: FnOnce(TaskMut)>(f: F) {
|
||||||
|
let mut replica = Replica::new_inmemory();
|
||||||
|
let task = replica.new_task(Status::Pending, "test".into()).unwrap();
|
||||||
|
let task = task.into_mut(&mut replica);
|
||||||
|
f(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_active_never_started() {
|
||||||
|
let task = Task::new(Uuid::new_v4(), TaskMap::new());
|
||||||
|
assert!(!task.is_active());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_active() {
|
||||||
|
let task = Task::new(
|
||||||
|
Uuid::new_v4(),
|
||||||
|
vec![(String::from("start.1234"), String::from(""))]
|
||||||
|
.drain(..)
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(task.is_active());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_is_active_stopped() {
|
||||||
|
let task = Task::new(
|
||||||
|
Uuid::new_v4(),
|
||||||
|
vec![(String::from("start.1234"), String::from("1235"))]
|
||||||
|
.drain(..)
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(!task.is_active());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_taskmap(task: &TaskMut, f: fn(&(&String, &String)) -> bool) -> usize {
|
||||||
|
task.taskmap.iter().filter(f).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_start() {
|
||||||
|
with_mut_task(|mut task| {
|
||||||
|
task.start().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
task.reload().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
// second start doesn't change anything..
|
||||||
|
task.start().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
task.reload().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stop() {
|
||||||
|
with_mut_task(|mut task| {
|
||||||
|
task.start().unwrap();
|
||||||
|
task.stop().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && !v.is_empty()),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
task.reload().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && !v.is_empty()),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stop_multiple() {
|
||||||
|
with_mut_task(|mut task| {
|
||||||
|
// simulate a task that has (through the synchronization process) been started twice
|
||||||
|
task.task
|
||||||
|
.taskmap
|
||||||
|
.insert(String::from("start.1234"), String::from(""));
|
||||||
|
task.task
|
||||||
|
.taskmap
|
||||||
|
.insert(String::from("start.5678"), String::from(""));
|
||||||
|
|
||||||
|
task.stop().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && !v.is_empty()),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
task.reload().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && v.is_empty()),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
count_taskmap(&task, |(k, v)| k.starts_with("start.") && !v.is_empty()),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_priority() {
|
fn test_priority() {
|
||||||
assert_eq!(Priority::L.to_taskmap(), "L");
|
assert_eq!(Priority::L.to_taskmap(), "L");
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue