diff --git a/imag-todo/Cargo.toml b/imag-todo/Cargo.toml new file mode 100644 index 00000000..0580c74f --- /dev/null +++ b/imag-todo/Cargo.toml @@ -0,0 +1,26 @@ +[package] +authors = ["mario "] +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" diff --git a/imag-todo/etc/on-add.sh b/imag-todo/etc/on-add.sh new file mode 100644 index 00000000..a58e4989 --- /dev/null +++ b/imag-todo/etc/on-add.sh @@ -0,0 +1,4 @@ +#/!usr/bin/env bash + +imag todo tw-hook --add + diff --git a/imag-todo/etc/on-modify.sh b/imag-todo/etc/on-modify.sh new file mode 100644 index 00000000..89be96d0 --- /dev/null +++ b/imag-todo/etc/on-modify.sh @@ -0,0 +1,4 @@ +#/!usr/bin/env bash + +imag todo tw-hook --delete + diff --git a/imag-todo/src/main.rs b/imag-todo/src/main.rs new file mode 100644 index 00000000..1163fe3c --- /dev/null +++ b/imag-todo/src/main.rs @@ -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); + } +} + diff --git a/imag-todo/src/ui.rs b/imag-todo/src/ui.rs new file mode 100644 index 00000000..f781673e --- /dev/null +++ b/imag-todo/src/ui.rs @@ -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") + ) + ) +} diff --git a/libimagtodo/Cargo.toml b/libimagtodo/Cargo.toml new file mode 100644 index 00000000..7ef12711 --- /dev/null +++ b/libimagtodo/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "libimagtodo" +version = "0.1.0" +authors = ["mario "] + +[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" + diff --git a/libimagtodo/src/error.rs b/libimagtodo/src/error.rs new file mode 100644 index 00000000..8aa42410 --- /dev/null +++ b/libimagtodo/src/error.rs @@ -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; + diff --git a/libimagtodo/src/lib.rs b/libimagtodo/src/lib.rs new file mode 100644 index 00000000..3189021d --- /dev/null +++ b/libimagtodo/src/lib.rs @@ -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; + diff --git a/libimagtodo/src/result.rs b/libimagtodo/src/result.rs new file mode 100644 index 00000000..d14bf499 --- /dev/null +++ b/libimagtodo/src/result.rs @@ -0,0 +1,5 @@ +use error::TodoError; + +use std::result::Result as RResult; + +pub type Result = RResult; diff --git a/libimagtodo/src/task.rs b/libimagtodo/src/task.rs new file mode 100644 index 00000000..1b44259b --- /dev/null +++ b/libimagtodo/src/task.rs @@ -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(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(store: &'a Store, mut r: R) -> Result, 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, 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>> { + 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(store: &'a Store, mut r: R) -> Result> { + 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::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(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 { + store.retrieve_for_module("todo/taskwarrior") + .map_err(|e| TodoError::new(TodoErrorKind::StoreError, Some(Box::new(e)))) + } + + pub fn all(store: &Store) -> Result { + 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>; + +} + +impl<'a> IntoTask<'a> for TTask { + + fn into_task(self, store : &'a Store) -> Result> { + 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>; +} + +impl<'a> FromStoreId for Task<'a> { + + fn from_storeid<'b>(store: &'b Store, id: StoreId) -> Result> { + 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>; + + fn next(&mut self) -> Option>> { + self.iditer.next().map(|id| Task::from_storeid(self.store, id)) + } +} +