commit
fc164a1382
10 changed files with 523 additions and 0 deletions
26
imag-todo/Cargo.toml
Normal file
26
imag-todo/Cargo.toml
Normal file
|
@ -0,0 +1,26 @@
|
|||
[package]
|
||||
authors = ["mario <mario-krehl@gmx.de>"]
|
||||
name = "imag-todo"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
clap = "2.4.3"
|
||||
glob = "0.2.11"
|
||||
log = "0.3.6"
|
||||
semver = "0.2.3"
|
||||
serde_json = "0.7.4"
|
||||
task-hookrs = "0.2.0"
|
||||
toml = "0.1.28"
|
||||
version = "2.0.1"
|
||||
|
||||
[dependencies.libimagrt]
|
||||
path = "../libimagrt"
|
||||
|
||||
[dependencies.libimagstore]
|
||||
path = "../libimagstore"
|
||||
|
||||
[dependencies.libimagtodo]
|
||||
path = "../libimagtodo"
|
||||
|
||||
[dependencies.libimagerror]
|
||||
path = "../libimagerror"
|
4
imag-todo/etc/on-add.sh
Normal file
4
imag-todo/etc/on-add.sh
Normal file
|
@ -0,0 +1,4 @@
|
|||
#/!usr/bin/env bash
|
||||
|
||||
imag todo tw-hook --add
|
||||
|
4
imag-todo/etc/on-modify.sh
Normal file
4
imag-todo/etc/on-modify.sh
Normal file
|
@ -0,0 +1,4 @@
|
|||
#/!usr/bin/env bash
|
||||
|
||||
imag todo tw-hook --delete
|
||||
|
127
imag-todo/src/main.rs
Normal file
127
imag-todo/src/main.rs
Normal file
|
@ -0,0 +1,127 @@
|
|||
extern crate clap;
|
||||
extern crate glob;
|
||||
#[macro_use] extern crate log;
|
||||
extern crate serde_json;
|
||||
extern crate semver;
|
||||
extern crate toml;
|
||||
#[macro_use] extern crate version;
|
||||
|
||||
extern crate task_hookrs;
|
||||
|
||||
extern crate libimagrt;
|
||||
extern crate libimagstore;
|
||||
extern crate libimagerror;
|
||||
extern crate libimagtodo;
|
||||
|
||||
use std::process::exit;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::io::stdin;
|
||||
|
||||
use toml::Value;
|
||||
|
||||
use libimagrt::runtime::Runtime;
|
||||
use libimagrt::setup::generate_runtime_setup;
|
||||
use libimagtodo::task::Task;
|
||||
use libimagerror::trace::trace_error;
|
||||
|
||||
mod ui;
|
||||
|
||||
use ui::build_ui;
|
||||
fn main() {
|
||||
let rt = generate_runtime_setup("imag-todo",
|
||||
&version!()[..],
|
||||
"Interface with taskwarrior",
|
||||
build_ui);
|
||||
|
||||
match rt.cli().subcommand_name() {
|
||||
Some("tw-hook") => tw_hook(&rt),
|
||||
Some("list") => list(&rt),
|
||||
None => {
|
||||
warn!("No command");
|
||||
},
|
||||
_ => unreachable!(),
|
||||
} // end match scmd
|
||||
} // end main
|
||||
|
||||
fn tw_hook(rt: &Runtime) {
|
||||
let subcmd = rt.cli().subcommand_matches("tw-hook").unwrap();
|
||||
if subcmd.is_present("add") {
|
||||
let stdin = stdin();
|
||||
let stdin = stdin.lock(); // implements BufRead which is required for `Task::import()`
|
||||
|
||||
match Task::import(rt.store(), stdin) {
|
||||
Ok((_, line, uuid)) => println!("{}\nTask {} stored in imag", line, uuid),
|
||||
Err(e) => {
|
||||
trace_error(&e);
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
} else if subcmd.is_present("delete") {
|
||||
// The used hook is "on-modify". This hook gives two json-objects
|
||||
// per usage und wants one (the second one) back.
|
||||
let stdin = stdin();
|
||||
Task::delete_by_imports(rt.store(), stdin.lock())
|
||||
.map_err(|e| trace_error(&e))
|
||||
.ok();
|
||||
} else {
|
||||
// Should not be possible, as one argument is required via
|
||||
// ArgGroup
|
||||
unreachable!();
|
||||
}
|
||||
}
|
||||
|
||||
fn list(rt: &Runtime) {
|
||||
let subcmd = rt.cli().subcommand_matches("list").unwrap();
|
||||
let verbose = subcmd.is_present("verbose");
|
||||
|
||||
let res = Task::all(rt.store()) // get all tasks
|
||||
.map(|iter| { // and if this succeeded
|
||||
// filter out the ones were we can read the uuid
|
||||
let uuids : Vec<_> = iter.filter_map(|t| match t {
|
||||
Ok(v) => match v.get_header().read("todo.uuid") {
|
||||
Ok(Some(Value::String(ref u))) => Some(u.clone()),
|
||||
Ok(Some(_)) => {
|
||||
warn!("Header type error");
|
||||
None
|
||||
},
|
||||
Ok(None) => None,
|
||||
Err(e) => {
|
||||
trace_error(&e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
trace_error(&e);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// compose a `task` call with them, ...
|
||||
let outstring = if verbose { // ... if verbose
|
||||
let output = Command::new("task")
|
||||
.stdin(Stdio::null())
|
||||
.args(&uuids)
|
||||
.spawn()
|
||||
.unwrap_or_else(|e| {
|
||||
trace_error(&e);
|
||||
panic!("Failed to execute `task` on the commandline. I'm dying now.");
|
||||
})
|
||||
.wait_with_output()
|
||||
.unwrap_or_else(|e| panic!("failed to unwrap output: {}", e));
|
||||
|
||||
String::from_utf8(output.stdout)
|
||||
.unwrap_or_else(|e| panic!("failed to execute: {}", e))
|
||||
} else { // ... else just join them
|
||||
uuids.join("\n")
|
||||
};
|
||||
|
||||
// and then print that
|
||||
println!("{}", outstring);
|
||||
});
|
||||
|
||||
if let Err(e) = res {
|
||||
trace_error(&e);
|
||||
}
|
||||
}
|
||||
|
42
imag-todo/src/ui.rs
Normal file
42
imag-todo/src/ui.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use clap::{Arg, App, ArgGroup, SubCommand};
|
||||
|
||||
pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
|
||||
app
|
||||
.subcommand(SubCommand::with_name("tw-hook")
|
||||
.about("For use in a taskwarrior hook")
|
||||
.version("0.1")
|
||||
|
||||
.arg(Arg::with_name("add")
|
||||
.long("add")
|
||||
.short("a")
|
||||
.takes_value(false)
|
||||
.required(false)
|
||||
.help("For use in an on-add hook"))
|
||||
|
||||
.arg(Arg::with_name("delete")
|
||||
.long("delete")
|
||||
.short("d")
|
||||
.takes_value(false)
|
||||
.required(false)
|
||||
.help("For use in an on-delete hook"))
|
||||
|
||||
.group(ArgGroup::with_name("taskwarrior hooks")
|
||||
.args(&[ "add",
|
||||
"delete",
|
||||
])
|
||||
.required(true))
|
||||
)
|
||||
|
||||
.subcommand(SubCommand::with_name("list")
|
||||
.about("List all tasks")
|
||||
.version("0.1")
|
||||
|
||||
.arg(Arg::with_name("verbose")
|
||||
.long("verbose")
|
||||
.short("v")
|
||||
.takes_value(false)
|
||||
.required(false)
|
||||
.help("Asks taskwarrior for all the details")
|
||||
)
|
||||
)
|
||||
}
|
19
libimagtodo/Cargo.toml
Normal file
19
libimagtodo/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "libimagtodo"
|
||||
version = "0.1.0"
|
||||
authors = ["mario <mario-krehl@gmx.de>"]
|
||||
|
||||
[dependencies]
|
||||
semver = "0.2"
|
||||
task-hookrs = "0.2"
|
||||
uuid = "0.2.0"
|
||||
toml = "0.1.28"
|
||||
log = "0.3.6"
|
||||
serde_json = "0.7.3"
|
||||
|
||||
[dependencies.libimagstore]
|
||||
path = "../libimagstore"
|
||||
|
||||
[dependencies.libimagerror]
|
||||
path = "../libimagerror"
|
||||
|
12
libimagtodo/src/error.rs
Normal file
12
libimagtodo/src/error.rs
Normal file
|
@ -0,0 +1,12 @@
|
|||
generate_error_module!(
|
||||
generate_error_types!(TodoError, TodoErrorKind,
|
||||
ConversionError => "Conversion Error",
|
||||
StoreError => "Store Error",
|
||||
ImportError => "Error importing"
|
||||
);
|
||||
);
|
||||
|
||||
pub use self::error::TodoError;
|
||||
pub use self::error::TodoErrorKind;
|
||||
pub use self::error::MapErrInto;
|
||||
|
16
libimagtodo/src/lib.rs
Normal file
16
libimagtodo/src/lib.rs
Normal file
|
@ -0,0 +1,16 @@
|
|||
extern crate semver;
|
||||
extern crate uuid;
|
||||
extern crate toml;
|
||||
#[macro_use] extern crate log;
|
||||
extern crate serde_json;
|
||||
|
||||
#[macro_use] extern crate libimagstore;
|
||||
#[macro_use] extern crate libimagerror;
|
||||
extern crate task_hookrs;
|
||||
|
||||
module_entry_path_mod!("todo", "0.1.0");
|
||||
|
||||
pub mod error;
|
||||
pub mod result;
|
||||
pub mod task;
|
||||
|
5
libimagtodo/src/result.rs
Normal file
5
libimagtodo/src/result.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
use error::TodoError;
|
||||
|
||||
use std::result::Result as RResult;
|
||||
|
||||
pub type Result<T> = RResult<T, TodoError>;
|
268
libimagtodo/src/task.rs
Normal file
268
libimagtodo/src/task.rs
Normal file
|
@ -0,0 +1,268 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::io::BufRead;
|
||||
use std::result::Result as RResult;
|
||||
|
||||
use toml::Value;
|
||||
use uuid::Uuid;
|
||||
|
||||
use task_hookrs::task::Task as TTask;
|
||||
use task_hookrs::import::{import_task, import_tasks};
|
||||
|
||||
use libimagstore::store::{FileLockEntry, Store};
|
||||
use libimagstore::storeid::{IntoStoreId, StoreIdIterator, StoreId};
|
||||
use module_path::ModuleEntryPath;
|
||||
|
||||
use error::{TodoError, TodoErrorKind, MapErrInto};
|
||||
use result::Result;
|
||||
|
||||
/// Task struct containing a `FileLockEntry`
|
||||
#[derive(Debug)]
|
||||
pub struct Task<'a>(FileLockEntry<'a>);
|
||||
|
||||
impl<'a> Task<'a> {
|
||||
|
||||
/// Concstructs a new `Task` with a `FileLockEntry`
|
||||
pub fn new(fle: FileLockEntry<'a>) -> Task<'a> {
|
||||
Task(fle)
|
||||
}
|
||||
|
||||
pub fn import<R: BufRead>(store: &'a Store, mut r: R) -> Result<(Task<'a>, String, Uuid)> {
|
||||
let mut line = String::new();
|
||||
r.read_line(&mut line);
|
||||
import_task(&line.as_str())
|
||||
.map_err_into(TodoErrorKind::ImportError)
|
||||
.and_then(|t| {
|
||||
let uuid = t.uuid().clone();
|
||||
t.into_task(store).map(|t| (t, line, uuid))
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a task from an import string. That is: read the imported string, get the UUID from it
|
||||
/// and try to load this UUID from store.
|
||||
///
|
||||
/// Possible return values are:
|
||||
///
|
||||
/// * Ok(Ok(Task))
|
||||
/// * Ok(Err(String)) - where the String is the String read from the `r` parameter
|
||||
/// * Err(_) - where the error is an error that happened during evaluation
|
||||
///
|
||||
pub fn get_from_import<R: BufRead>(store: &'a Store, mut r: R) -> Result<RResult<Task<'a>, String>>
|
||||
{
|
||||
let mut line = String::new();
|
||||
r.read_line(&mut line);
|
||||
Task::get_from_string(store, line)
|
||||
}
|
||||
|
||||
/// Get a task from a String. The String is expected to contain the JSON-representation of the
|
||||
/// Task to get from the store (only the UUID really matters in this case)
|
||||
///
|
||||
/// For an explanation on the return values see `Task::get_from_import()`.
|
||||
pub fn get_from_string(store: &'a Store, s: String) -> Result<RResult<Task<'a>, String>> {
|
||||
import_task(s.as_str())
|
||||
.map_err_into(TodoErrorKind::ImportError)
|
||||
.map(|t| t.uuid().clone())
|
||||
.and_then(|uuid| Task::get_from_uuid(store, uuid))
|
||||
.and_then(|o| match o {
|
||||
None => Ok(Err(s)),
|
||||
Some(t) => Ok(Ok(t)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a task from an UUID.
|
||||
///
|
||||
/// If there is no task with this UUID, this returns `Ok(None)`.
|
||||
pub fn get_from_uuid(store: &'a Store, uuid: Uuid) -> Result<Option<Task<'a>>> {
|
||||
let store_id = ModuleEntryPath::new(format!("taskwarrior/{}", uuid)).into_storeid();
|
||||
|
||||
store.get(store_id)
|
||||
.map(|o| o.map(Task::new))
|
||||
.map_err_into(TodoErrorKind::StoreError)
|
||||
}
|
||||
|
||||
/// Same as Task::get_from_import() but uses Store::retrieve() rather than Store::get(), to
|
||||
/// implicitely create the task if it does not exist.
|
||||
pub fn retrieve_from_import<R: BufRead>(store: &'a Store, mut r: R) -> Result<Task<'a>> {
|
||||
let mut line = String::new();
|
||||
r.read_line(&mut line);
|
||||
Task::retrieve_from_string(store, line)
|
||||
}
|
||||
|
||||
/// Retrieve a task from a String. The String is expected to contain the JSON-representation of
|
||||
/// the Task to retrieve from the store (only the UUID really matters in this case)
|
||||
pub fn retrieve_from_string(store: &'a Store, s: String) -> Result<Task<'a>> {
|
||||
Task::get_from_string(store, s)
|
||||
.and_then(|opt| match opt {
|
||||
Ok(task) => Ok(task),
|
||||
Err(string) => import_task(string.as_str())
|
||||
.map_err_into(TodoErrorKind::ImportError)
|
||||
.and_then(|t| t.into_task(store)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete_by_imports<R: BufRead>(store: &Store, r: R) -> Result<()> {
|
||||
use serde_json::ser::to_string as serde_to_string;
|
||||
use task_hookrs::status::TaskStatus;
|
||||
|
||||
for (counter, res_ttask) in import_tasks(r).into_iter().enumerate() {
|
||||
match res_ttask {
|
||||
Ok(ttask) => {
|
||||
if counter % 2 == 1 {
|
||||
// Only every second task is needed, the first one is the
|
||||
// task before the change, and the second one after
|
||||
// the change. The (maybe modified) second one is
|
||||
// expected by taskwarrior.
|
||||
match serde_to_string(&ttask).map_err_into(TodoErrorKind::ImportError) {
|
||||
// use println!() here, as we talk with TW
|
||||
Ok(val) => println!("{}", val),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
|
||||
// Taskwarrior does not have the concept of deleted tasks, but only modified
|
||||
// ones.
|
||||
//
|
||||
// Here we check if the status of a task is deleted and if yes, we delete it
|
||||
// from the store.
|
||||
if *ttask.status() == TaskStatus::Deleted {
|
||||
match Task::delete_by_uuid(store, *ttask.uuid()) {
|
||||
Ok(_) => info!("Deleted task {}", *ttask.uuid()),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
} // end if c % 2
|
||||
},
|
||||
Err(e) => return Err(e).map_err_into(TodoErrorKind::ImportError),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_by_uuid(store: &Store, uuid: Uuid) -> Result<()> {
|
||||
store.delete(ModuleEntryPath::new(format!("taskwarrior/{}", uuid)).into_storeid())
|
||||
.map_err(|e| TodoError::new(TodoErrorKind::StoreError, Some(Box::new(e))))
|
||||
}
|
||||
|
||||
pub fn all_as_ids(store: &Store) -> Result<StoreIdIterator> {
|
||||
store.retrieve_for_module("todo/taskwarrior")
|
||||
.map_err(|e| TodoError::new(TodoErrorKind::StoreError, Some(Box::new(e))))
|
||||
}
|
||||
|
||||
pub fn all(store: &Store) -> Result<TaskIterator> {
|
||||
Task::all_as_ids(store)
|
||||
.map(|iter| TaskIterator::new(store, iter))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<'a> Deref for Task<'a> {
|
||||
type Target = FileLockEntry<'a>;
|
||||
|
||||
fn deref(&self) -> &FileLockEntry<'a> {
|
||||
&self.0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<'a> DerefMut for Task<'a> {
|
||||
|
||||
fn deref_mut(&mut self) -> &mut FileLockEntry<'a> {
|
||||
&mut self.0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// A trait to get a `libimagtodo::task::Task` out of the implementing object.
|
||||
pub trait IntoTask<'a> {
|
||||
|
||||
/// # Usage
|
||||
/// ```ignore
|
||||
/// use std::io::stdin;
|
||||
///
|
||||
/// use task_hookrs::task::Task;
|
||||
/// use task_hookrs::import::import;
|
||||
/// use libimagstore::store::{Store, FileLockEntry};
|
||||
///
|
||||
/// if let Ok(task_hookrs_task) = import(stdin()) {
|
||||
/// // Store is given at runtime
|
||||
/// let task = task_hookrs_task.into_filelockentry(store);
|
||||
/// println!("Task with uuid: {}", task.flentry.get_header().get("todo.uuid"));
|
||||
/// }
|
||||
/// ```
|
||||
fn into_task(self, store : &'a Store) -> Result<Task<'a>>;
|
||||
|
||||
}
|
||||
|
||||
impl<'a> IntoTask<'a> for TTask {
|
||||
|
||||
fn into_task(self, store : &'a Store) -> Result<Task<'a>> {
|
||||
let uuid = self.uuid();
|
||||
let store_id = ModuleEntryPath::new(format!("taskwarrior/{}", uuid)).into_storeid();
|
||||
|
||||
match store.retrieve(store_id) {
|
||||
Err(e) => return Err(TodoError::new(TodoErrorKind::StoreError, Some(Box::new(e)))),
|
||||
Ok(mut fle) => {
|
||||
{
|
||||
let mut header = fle.get_header_mut();
|
||||
match header.read("todo") {
|
||||
Ok(None) => {
|
||||
if let Err(e) = header.set("todo", Value::Table(BTreeMap::new())) {
|
||||
return Err(TodoError::new(TodoErrorKind::StoreError, Some(Box::new(e))))
|
||||
}
|
||||
}
|
||||
Ok(Some(_)) => { }
|
||||
Err(e) => {
|
||||
return Err(TodoError::new(TodoErrorKind::StoreError, Some(Box::new(e))))
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = header.set("todo.uuid", Value::String(format!("{}",uuid))) {
|
||||
return Err(TodoError::new(TodoErrorKind::StoreError, Some(Box::new(e))))
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the errors above have returned the function, everything is fine
|
||||
Ok(Task::new(fle))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
trait FromStoreId {
|
||||
fn from_storeid<'a>(&'a Store, StoreId) -> Result<Task<'a>>;
|
||||
}
|
||||
|
||||
impl<'a> FromStoreId for Task<'a> {
|
||||
|
||||
fn from_storeid<'b>(store: &'b Store, id: StoreId) -> Result<Task<'b>> {
|
||||
match store.retrieve(id) {
|
||||
Err(e) => Err(TodoError::new(TodoErrorKind::StoreError, Some(Box::new(e)))),
|
||||
Ok(c) => Ok(Task::new( c )),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TaskIterator<'a> {
|
||||
store: &'a Store,
|
||||
iditer: StoreIdIterator,
|
||||
}
|
||||
|
||||
impl<'a> TaskIterator<'a> {
|
||||
|
||||
pub fn new(store: &'a Store, iditer: StoreIdIterator) -> TaskIterator<'a> {
|
||||
TaskIterator {
|
||||
store: store,
|
||||
iditer: iditer,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl<'a> Iterator for TaskIterator<'a> {
|
||||
type Item = Result<Task<'a>>;
|
||||
|
||||
fn next(&mut self) -> Option<Result<Task<'a>>> {
|
||||
self.iditer.next().map(|id| Task::from_storeid(self.store, id))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in a new issue