From adca7a4a1cbb9ff421b411f6946238dedb6288dd Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 29 Jun 2019 21:51:58 +0200 Subject: [PATCH 1/4] Remove old implementation Signed-off-by: Matthias Beyer --- lib/domain/libimagtodo/src/iter.rs | 51 ------- lib/domain/libimagtodo/src/lib.rs | 56 -------- lib/domain/libimagtodo/src/task.rs | 45 ------ lib/domain/libimagtodo/src/taskstore.rs | 184 ------------------------ 4 files changed, 336 deletions(-) delete mode 100644 lib/domain/libimagtodo/src/iter.rs delete mode 100644 lib/domain/libimagtodo/src/lib.rs delete mode 100644 lib/domain/libimagtodo/src/task.rs delete mode 100644 lib/domain/libimagtodo/src/taskstore.rs diff --git a/lib/domain/libimagtodo/src/iter.rs b/lib/domain/libimagtodo/src/iter.rs deleted file mode 100644 index 45d7a7c8..00000000 --- a/lib/domain/libimagtodo/src/iter.rs +++ /dev/null @@ -1,51 +0,0 @@ -// -// imag - the personal information management suite for the commandline -// Copyright (C) 2015-2019 Matthias Beyer and contributors -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; version -// 2.1 of the License. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// - -use libimagstore::iter::Entries; -use libimagstore::storeid::StoreId; - -use failure::Fallible as Result; - -pub struct TaskIdIterator<'a>(Entries<'a>); - -impl<'a> TaskIdIterator<'a> { - - pub fn new(inner: Entries<'a>) -> Self { - TaskIdIterator(inner) - } - -} - -impl<'a> Iterator for TaskIdIterator<'a> { - type Item = Result; - - fn next(&mut self) -> Option { - loop { - match self.0.next() { - None => return None, - Some(Err(e)) => return Some(Err(e)), - Some(Ok(n)) => if n.is_in_collection(&["todo", "taskwarrior"]) { - return Some(Ok(n)) - }, // else continue - } - } - } - -} - diff --git a/lib/domain/libimagtodo/src/lib.rs b/lib/domain/libimagtodo/src/lib.rs deleted file mode 100644 index bde8b818..00000000 --- a/lib/domain/libimagtodo/src/lib.rs +++ /dev/null @@ -1,56 +0,0 @@ -// -// imag - the personal information management suite for the commandline -// Copyright (C) 2015-2019 Matthias Beyer and contributors -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; version -// 2.1 of the License. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// - -#![forbid(unsafe_code)] - -#![recursion_limit="256"] - -#![deny( - dead_code, - non_camel_case_types, - non_snake_case, - path_statements, - trivial_numeric_casts, - unstable_features, - unused_allocation, - unused_import_braces, - unused_imports, - unused_must_use, - unused_mut, - unused_qualifications, - while_true, -)] - -extern crate uuid; -extern crate toml; -extern crate toml_query; -#[macro_use] extern crate log; -extern crate serde_json; -#[macro_use] extern crate failure; - -#[macro_use] extern crate libimagstore; -extern crate libimagerror; -extern crate task_hookrs; - -module_entry_path_mod!("todo"); - -pub mod task; -pub mod taskstore; -pub mod iter; - diff --git a/lib/domain/libimagtodo/src/task.rs b/lib/domain/libimagtodo/src/task.rs deleted file mode 100644 index f62ef13e..00000000 --- a/lib/domain/libimagtodo/src/task.rs +++ /dev/null @@ -1,45 +0,0 @@ -// -// imag - the personal information management suite for the commandline -// Copyright (C) 2015-2019 Matthias Beyer and contributors -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; version -// 2.1 of the License. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// - -use failure::ResultExt; -use failure::Error; -use failure::err_msg; -use failure::Fallible as Result; - -use libimagstore::store::Entry; -use libimagerror::errors::ErrorMsg as EM; - -use uuid::Uuid; -use toml_query::read::TomlValueReadTypeExt; - -pub trait Task { - fn get_uuid(&self) -> Result; -} - -impl Task for Entry { - fn get_uuid(&self) -> Result { - self.get_header() - .read_string("todo.uuid")? - .ok_or_else(|| Error::from(EM::EntryHeaderFieldMissing("todo.uuid"))) - .and_then(|u| { - Uuid::parse_str(&u).context(err_msg("UUID Parser error")).map_err(Error::from) - }) - } -} - diff --git a/lib/domain/libimagtodo/src/taskstore.rs b/lib/domain/libimagtodo/src/taskstore.rs deleted file mode 100644 index fbddecf6..00000000 --- a/lib/domain/libimagtodo/src/taskstore.rs +++ /dev/null @@ -1,184 +0,0 @@ -// -// imag - the personal information management suite for the commandline -// Copyright (C) 2015-2019 Matthias Beyer and contributors -// -// This library is free software; you can redistribute it and/or -// modify it under the terms of the GNU Lesser General Public -// License as published by the Free Software Foundation; version -// 2.1 of the License. -// -// This library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -// Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public -// License along with this library; if not, write to the Free Software -// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -// - -use std::io::BufRead; -use std::result::Result as RResult; - -use toml::Value; -use toml::map::Map; -use uuid::Uuid; - -use task_hookrs::task::Task as TTask; -use task_hookrs::import::{import_task, import_tasks}; -use failure::Fallible as Result; -use failure::ResultExt; -use failure::Error; -use failure::err_msg; - -use libimagstore::store::{FileLockEntry, Store}; -use libimagerror::errors::ErrorMsg as EM; - -use crate::iter::TaskIdIterator; - -/// Task struct containing a `FileLockEntry` -pub trait TaskStore<'a> { - fn import_task_from_reader(&'a self, r: R) -> Result<(FileLockEntry<'a>, String, Uuid)>; - fn get_task_from_import(&'a self, r: R) -> Result, String>>; - fn get_task_from_string(&'a self, s: String) -> Result, String>>; - fn get_task_from_uuid(&'a self, uuid: Uuid) -> Result>>; - fn retrieve_task_from_import(&'a self, r: R) -> Result>; - fn retrieve_task_from_string(&'a self, s: String) -> Result>; - fn delete_tasks_by_imports(&self, r: R) -> Result<()>; - fn delete_task_by_uuid(&self, uuid: Uuid) -> Result<()>; - fn all_tasks(&self) -> Result; - fn new_from_twtask(&'a self, task: TTask) -> Result>; -} - -impl<'a> TaskStore<'a> for Store { - - fn import_task_from_reader(&'a self, mut r: R) -> Result<(FileLockEntry<'a>, String, Uuid)> { - let mut line = String::new(); - r.read_line(&mut line).context(EM::UTF8Error)?; - import_task(&line.as_str()) - .context(err_msg("Error importing")) - .map_err(Error::from) - .and_then(|t| { - let uuid = *t.uuid(); - self.new_from_twtask(t).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 - /// - fn get_task_from_import(&'a self, mut r: R) -> Result, String>> { - let mut line = String::new(); - r.read_line(&mut line).context(EM::UTF8Error)?; - self.get_task_from_string(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()`. - fn get_task_from_string(&'a self, s: String) -> Result, String>> { - import_task(s.as_str()) - .context(err_msg("Import error")) - .map_err(Error::from) - .map(|t| *t.uuid()) - .and_then(|uuid| self.get_task_from_uuid(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)`. - fn get_task_from_uuid(&'a self, uuid: Uuid) -> Result>> { - crate::module_path::new_id(format!("taskwarrior/{}", uuid)).and_then(|store_id| self.get(store_id)) - } - - /// Same as Task::get_from_import() but uses Store::retrieve() rather than Store::get(), to - /// implicitely create the task if it does not exist. - fn retrieve_task_from_import(&'a self, mut r: R) -> Result> { - let mut line = String::new(); - r.read_line(&mut line).context(EM::UTF8Error)?; - self.retrieve_task_from_string(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) - fn retrieve_task_from_string(&'a self, s: String) -> Result> { - self.get_task_from_string(s) - .and_then(|opt| match opt { - Ok(task) => Ok(task), - Err(string) => import_task(string.as_str()) - .context(err_msg("Import error")) - .map_err(Error::from) - .and_then(|t| self.new_from_twtask(t)), - }) - } - - fn delete_tasks_by_imports(&self, r: R) -> Result<()> { - use task_hookrs::status::TaskStatus; - - for (counter, res_ttask) in import_tasks(r).into_iter().enumerate() { - let ttask = res_ttask.context(err_msg("Import error"))?; - - 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. - // - // 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 { - self.delete_task_by_uuid(*ttask.uuid())?; - info!("Deleted task {}", *ttask.uuid()); - } - } - } - Ok(()) - } - - fn delete_task_by_uuid(&self, uuid: Uuid) -> Result<()> { - crate::module_path::new_id(format!("taskwarrior/{}", uuid)).and_then(|id| self.delete(id)) - } - - fn all_tasks(&self) -> Result { - self.entries().map(TaskIdIterator::new) - } - - fn new_from_twtask(&'a self, task: TTask) -> Result> { - use toml_query::read::TomlValueReadExt; - use toml_query::set::TomlValueSetExt; - - let uuid = task.uuid(); - crate::module_path::new_id(format!("taskwarrior/{}", uuid)).and_then(|id| { - self.retrieve(id).and_then(|mut fle| { - { - let hdr = fle.get_header_mut(); - if hdr.read("todo")?.is_none() { - hdr.set("todo", Value::Table(Map::new()))?; - } - - hdr.set("todo.uuid", Value::String(format!("{}",uuid)))?; - } - - // If none of the errors above have returned the function, everything is fine - Ok(fle) - }) - }) - - } - -} - From 21cc901d06c8ad5ab8bdc5f995296053e025d7d0 Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 15 Jun 2019 17:02:42 +0200 Subject: [PATCH 2/4] Rewrite what libimagtodo should implement Signed-off-by: Matthias Beyer --- doc/src/05100-lib-todo.md | 63 ++++++++++++++------------------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/doc/src/05100-lib-todo.md b/doc/src/05100-lib-todo.md index 50eedb41..84fe851d 100644 --- a/doc/src/05100-lib-todo.md +++ b/doc/src/05100-lib-todo.md @@ -3,42 +3,43 @@ The library for the todo module which provides functionality to implement/implements a todomanager in imag. + ### Implementation details -One todo entry is stored as one imag entry. The ID of the imag entry is generated by -appending a unique ID (UUID) to "todo/". +One todo entry is stored as one imag entry. The ID of the imag entry is +generated by appending a unique ID (UUID) to "todo/". The unique ID identifies the todo entry. + #### Stored data A todo entry stores the following information: * The (UU)ID of the todo entry * A status of the todo entry. Valid values are: "deleted", "done", "pending" -* A "scheduled" date/datetime, can also yield an iterator +* An optional "scheduled" date/datetime * An optional "hidden" value, which specifies a date in the future where this - todo entry should show up. Relative (eg. "-5days"), - to the "scheduled" date/datetime -* An optional "due" date/datetime, relative to the "scheduled" time -* A list of dependencies of the entry -* A "importance"-level, config file defined (strings in an array, index used for - ranking them) -* User defined value (a map of key-value string pairs) + todo entry should show up. +* An optional "due" date/datetime +* A "priority"-level, either "h", "m", "l" The description of the todo entry is stored as plain text. + #### Data not stored Some data is explicitely _not_ stored by the library because there are other libraries fullfilling that purpose. These are: +* Related todos, which can be done via libimagentrylink * Tags, which can be done with libimagentrytag * Category, which can be done with libimagentrycategory * Project belonging, which can be done with libimagentrylink (by linking to a project file - note that "project" is a domain not yet implemented by imag) * Annotations, which can be stored with libimagentryannotation + #### Header format The header partial for libimagtodo is as follows: @@ -46,42 +47,24 @@ The header partial for libimagtodo is as follows: ``` [todo] uuid = "string" -status = "string" -scheduled = "" -hidden = "" -due = "" -depends = [ "list of uuids" ] -importance = "string" -uda = {} +status = "enum { 'deleted', 'done', 'pending' }" +scheduled = "" // optional +hidden = "" // optional +due = "" // optional +priority = "enum { 'h', 'm', 'l' }" // optional ``` + #### Functionality The provided functionality of this library includes, but is not limited to: -* Creating new todo entries in the store -* Deleting todo entries from the store -* get/retrieving todo entries from the store -* Turning an entry into a todo entry -* Getting todo details from an entry - * scheduled, due, waiting date/datetime - * priority - * UUID - * status - * An iterator over all dependend todo entries (as `StoreIdIterator`) -* Calculating a "urgency" of a todo entry from a formula weighted by configurable factors +* Creating +* Deleting +* Get/Retrieving +* Getting data about the todo + * Reading metadata: scheduled, due, waiting, prio, uuid, status,... + * Related (via libimagentrylink) todo entries -#### Dependencies between tasks - -Dependencies between todo entries are created by putting the UUID of a dependent todo entry into -the `todo.depends` header. -This way, a unidirectional link is created. A link (as in `libimagentrylink`) is -_also_ created, but this can be turned off explicitely. - -As `libimagentrylink` links are bidirectional, they do not suffice for todo -entry dependency creation. - -As todo entries are stored with the Store IDs "todo/", creating a -`StoreId` from a UUID is trivial. From 775d3f0a809e46aa71f9b38e7bd58d76af973b5b Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sat, 29 Jun 2019 22:53:06 +0200 Subject: [PATCH 3/4] Reimplement libimagtodo Signed-off-by: Matthias Beyer --- lib/domain/libimagtodo/Cargo.toml | 32 ++++-- lib/domain/libimagtodo/src/entry.rs | 138 ++++++++++++++++++++++ lib/domain/libimagtodo/src/iter.rs | 133 ++++++++++++++++++++++ lib/domain/libimagtodo/src/lib.rs | 44 +++++++ lib/domain/libimagtodo/src/priority.rs | 68 +++++++++++ lib/domain/libimagtodo/src/status.rs | 72 ++++++++++++ lib/domain/libimagtodo/src/store.rs | 152 +++++++++++++++++++++++++ 7 files changed, 632 insertions(+), 7 deletions(-) create mode 100644 lib/domain/libimagtodo/src/entry.rs create mode 100644 lib/domain/libimagtodo/src/iter.rs create mode 100644 lib/domain/libimagtodo/src/lib.rs create mode 100644 lib/domain/libimagtodo/src/priority.rs create mode 100644 lib/domain/libimagtodo/src/status.rs create mode 100644 lib/domain/libimagtodo/src/store.rs diff --git a/lib/domain/libimagtodo/Cargo.toml b/lib/domain/libimagtodo/Cargo.toml index 8b2b9f39..ace432dc 100644 --- a/lib/domain/libimagtodo/Cargo.toml +++ b/lib/domain/libimagtodo/Cargo.toml @@ -20,13 +20,31 @@ is-it-maintained-open-issues = { repository = "matthiasbeyer/imag" } maintenance = { status = "actively-developed" } [dependencies] -task-hookrs = "0.6.0" -uuid = "0.7.4" -toml = "0.5.1" -toml-query = "0.9.2" -log = "0.4.6" -serde_json = "1.0.39" -failure = "0.1.5" +failure = "0.1" +filters = "0.3" +log = "0.4" +serde = "1" +serde_derive = "1" +serde_json = "1" +toml = "0.5" libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" } libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" } +libimagentryutil = { version = "0.10.0", path = "../../../lib/entry/libimagentryutil" } +libimagutil = { version = "0.10.0", path = "../../../lib/etc/libimagutil" } + +[dependencies.toml-query] +version = "0.9" +default-features = false +features = ["typed"] + +[dependencies.chrono] +version = "0.4" +default-features = false +features = ["serde"] + +[dependencies.uuid] +version = "0.7" +default-features = false +features = ["serde", "v4"] + diff --git a/lib/domain/libimagtodo/src/entry.rs b/lib/domain/libimagtodo/src/entry.rs new file mode 100644 index 00000000..35fd9b3a --- /dev/null +++ b/lib/domain/libimagtodo/src/entry.rs @@ -0,0 +1,138 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use libimagentryutil::isa::Is; +use libimagentryutil::isa::IsKindHeaderPathProvider; +use libimagstore::store::Entry; +use libimagutil::date::datetime_from_string; + +use failure::Fallible as Result; +use failure::Error; +use failure::ResultExt; +use chrono::NaiveDateTime; +use toml_query::read::Partial; +use toml_query::read::TomlValueReadExt; +use toml_query::insert::TomlValueInsertExt; +use uuid::Uuid; + +use crate::status::Status; +use crate::priority::Priority; + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct TodoHeader { + pub(crate) uuid: Uuid, + pub(crate) status: Status, + pub(crate) scheduled: Option, + pub(crate) hidden: Option, + pub(crate) due: Option, + pub(crate) priority: Option, +} + +impl<'a> Partial<'a> for TodoHeader { + const LOCATION: &'static str = "todo"; + type Output = Self; +} + +pub trait Todo { + fn is_todo(&self) -> Result; + fn get_uuid(&self) -> Result; + fn get_status(&self) -> Result; + fn set_status(&mut self, status: Status) -> Result<()>; + fn get_scheduled(&self) -> Result>; + fn set_scheduled(&mut self, scheduled: NaiveDateTime) -> Result<()>; + fn get_hidden(&self) -> Result>; + fn set_hidden(&mut self, hidden: NaiveDateTime) -> Result<()>; + fn get_due(&self) -> Result>; + fn set_due(&mut self, due: NaiveDateTime) -> Result<()>; + fn get_priority(&self) -> Result>; + fn set_priority(&mut self, prio: Priority) -> Result<()>; +} + +provide_kindflag_path!(pub IsTodo, "todo.is_todo"); + +impl Todo for Entry { + fn is_todo(&self) -> Result { + self.is::().context("Cannot check whether Entry is a todo").map_err(From::from) + } + + fn get_uuid(&self) -> Result { + get_header(self).map(|hdr| hdr.uuid) + } + + fn get_status(&self) -> Result { + get_header(self).map(|hdr| hdr.status) + } + + fn set_status(&mut self, status: Status) -> Result<()> { + self.get_header_mut().insert_serialized("todo.status", status)?; + Ok(()) + } + + fn get_scheduled(&self) -> Result> { + get_optional_ndt(self, |hdr| hdr.scheduled) + } + + fn set_scheduled(&mut self, scheduled: NaiveDateTime) -> Result<()> { + self.get_header_mut().insert_serialized("todo.scheduled", scheduled)?; + Ok(()) + } + + fn get_hidden(&self) -> Result> { + get_optional_ndt(self, |hdr| hdr.hidden) + } + + fn set_hidden(&mut self, hidden: NaiveDateTime) -> Result<()> { + self.get_header_mut().insert_serialized("todo.hidden", hidden)?; + Ok(()) + } + + fn get_due(&self) -> Result> { + get_optional_ndt(self, |hdr| hdr.due) + } + + fn set_due(&mut self, due: NaiveDateTime) -> Result<()> { + self.get_header_mut().insert_serialized("todo.due", due)?; + Ok(()) + } + + fn get_priority(&self) -> Result> { + get_header(self).map(|hdr| hdr.priority) + } + + fn set_priority(&mut self, priority: Priority) -> Result<()> { + self.get_header_mut().insert_serialized("todo.priority", priority)?; + Ok(()) + } + +} + +fn get_header(entry: &Entry) -> Result { + entry.get_header() + .read_partial::()? + .ok_or_else(|| { + format_err!("{} does not contain a TODO header", entry.get_location()) + }) +} + +fn get_optional_ndt(entry: &Entry, extractor: F) + -> Result> + where F: FnOnce(TodoHeader) -> Option +{ + get_header(entry).map(extractor)?.map(datetime_from_string).transpose().map_err(Error::from) +} diff --git a/lib/domain/libimagtodo/src/iter.rs b/lib/domain/libimagtodo/src/iter.rs new file mode 100644 index 00000000..61379964 --- /dev/null +++ b/lib/domain/libimagtodo/src/iter.rs @@ -0,0 +1,133 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::result::Result as RResult; + +use failure::Fallible as Result; +use failure::Error; +use filters::failable::filter::FailableFilter; + +use libimagstore::store::FileLockEntry; +use libimagstore::store::Entry; + +use crate::entry::Todo; +use crate::status::Status; +use crate::priority::Priority; + +/// Iterator adaptor which filters an Iterator so that only todos are left +pub struct OnlyTodos<'a>(Box>>); + +impl<'a> OnlyTodos<'a> { + pub fn new(it: Box>>) -> Self { + OnlyTodos(it) + } +} + +impl<'a> Iterator for OnlyTodos<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + while let Some(next) = self.0.next() { + match next.is_todo() { + Ok(true) => return Some(Ok(next)), + Ok(false) => continue, + Err(e) => return Some(Err(e)), + } + } + + None + } +} + +/// Helper filter type +/// +/// Can be used to filter an Iterator of Todos by status +/// +pub struct StatusFilter(Status); + +impl FailableFilter for StatusFilter { + type Error = Error; + + fn filter(&self, entry: &Entry) -> RResult { + Ok(entry.get_status()? == self.0) + } +} + +/// Helper filter type +/// +/// Can be used to filter an Iterator of Todos for scheduled todos +/// +pub struct IsScheduledFilter; + +impl FailableFilter for IsScheduledFilter { + type Error = Error; + + fn filter(&self, entry: &Entry) -> RResult { + entry.get_scheduled().map(|s| s.is_some()) + } +} + +/// Helper filter type +/// +/// Can be used to filter an Iterator of Todos for hidden todos +/// +pub struct IsHiddenFilter; + +impl FailableFilter for IsHiddenFilter { + type Error = Error; + + fn filter(&self, entry: &Entry) -> RResult { + entry.get_hidden().map(|s| s.is_some()) + } +} + + +/// Helper filter type +/// +/// Can be used to filter an Iterator of Todos for due todos +/// +pub struct IsDueFilter; + +impl FailableFilter for IsDueFilter { + type Error = Error; + + fn filter(&self, entry: &Entry) -> RResult { + entry.get_due().map(|s| s.is_some()) + } +} + + +/// Helper filter type +/// +/// Can be used to filter an Iterator of Todos for priority +/// +/// # Warning +/// +/// If no priority is set for the entry, this filters out the entry +/// +pub struct PriorityFilter(Priority); + +impl FailableFilter for PriorityFilter { + type Error = Error; + + fn filter(&self, entry: &Entry) -> RResult { + Ok(entry.get_priority()?.map(|p| p == self.0).unwrap_or(false)) + } +} + diff --git a/lib/domain/libimagtodo/src/lib.rs b/lib/domain/libimagtodo/src/lib.rs new file mode 100644 index 00000000..8901077d --- /dev/null +++ b/lib/domain/libimagtodo/src/lib.rs @@ -0,0 +1,44 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + + +extern crate chrono; +extern crate serde; +extern crate toml; +extern crate toml_query; +extern crate uuid; +extern crate filters; + +#[macro_use] extern crate serde_derive; +#[macro_use] extern crate failure; +#[macro_use] extern crate log; + +extern crate libimagerror; +extern crate libimagutil; +#[macro_use] extern crate libimagstore; +#[macro_use] extern crate libimagentryutil; + +pub mod entry; +pub mod iter; +pub mod priority; +pub mod status; +pub mod store; + +module_entry_path_mod!("todo"); + diff --git a/lib/domain/libimagtodo/src/priority.rs b/lib/domain/libimagtodo/src/priority.rs new file mode 100644 index 00000000..e612093d --- /dev/null +++ b/lib/domain/libimagtodo/src/priority.rs @@ -0,0 +1,68 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::cmp::PartialOrd; +use std::cmp::Ordering; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum Priority { + #[serde(rename = "h")] + High, + + #[serde(rename = "m")] + Medium, + + #[serde(rename = "l")] + Low, +} + +impl Priority { + pub fn as_str(&self) -> &str { + match self { + Priority::High => "h", + Priority::Medium => "m", + Priority::Low => "l", + } + } +} + +impl PartialOrd for Priority { + fn partial_cmp(&self, other: &Priority) -> Option { + Some(match (self, other) { + (Priority::Low, Priority::Low) => Ordering::Equal, + (Priority::Low, Priority::Medium) => Ordering::Less, + (Priority::Low, Priority::High) => Ordering::Less, + + (Priority::Medium, Priority::Low) => Ordering::Greater, + (Priority::Medium, Priority::Medium) => Ordering::Equal, + (Priority::Medium, Priority::High) => Ordering::Less, + + (Priority::High, Priority::Low) => Ordering::Greater, + (Priority::High, Priority::Medium) => Ordering::Greater, + (Priority::High, Priority::High) => Ordering::Equal, + }) + } +} + +impl Ord for Priority { + fn cmp(&self, other: &Self) -> Ordering { + self.partial_cmp(&other).unwrap() // save by impl above + } +} + diff --git a/lib/domain/libimagtodo/src/status.rs b/lib/domain/libimagtodo/src/status.rs new file mode 100644 index 00000000..63d8fa03 --- /dev/null +++ b/lib/domain/libimagtodo/src/status.rs @@ -0,0 +1,72 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use failure::Fallible as Result; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +pub enum Status { + #[serde(rename = "pending")] + Pending, + + #[serde(rename = "done")] + Done, + + #[serde(rename = "deleted")] + Deleted, +} + +impl Status { + pub fn as_str(&self) -> &str { + match self { + Status::Pending => "pending", + Status::Done => "done", + Status::Deleted => "deleted", + } + } + + pub fn from_str(s: &str) -> Result { + match s { + "pending" => Ok(Status::Pending), + "done" => Ok(Status::Done), + "deleted" => Ok(Status::Deleted), + other => Err(format_err!("{} is not a valid status", other)), + } + } +} + +#[test] +fn test_serializing() { + assert_eq!(Status::Pending.as_str(), "pending"); + assert_eq!(Status::Done.as_str(), "done"); + assert_eq!(Status::Deleted.as_str(), "deleted"); +} + +#[test] +fn test_deserializing() { + assert_eq!(Status::from_str("pending").unwrap(), Status::Pending); + assert_eq!(Status::from_str("done").unwrap(), Status::Done); + assert_eq!(Status::from_str("deleted").unwrap(), Status::Deleted); +} + +#[test] +fn test_serializing_deserializing() { + assert_eq!(Status::Pending.as_str(), Status::from_str("pending").unwrap().as_str()); + assert_eq!(Status::Done.as_str(), Status::from_str("done").unwrap().as_str()); + assert_eq!(Status::Deleted.as_str(), Status::from_str("deleted").unwrap().as_str()); +} diff --git a/lib/domain/libimagtodo/src/store.rs b/lib/domain/libimagtodo/src/store.rs new file mode 100644 index 00000000..cc1f2aa0 --- /dev/null +++ b/lib/domain/libimagtodo/src/store.rs @@ -0,0 +1,152 @@ +// +// imag - the personal information management suite for the commandline +// Copyright (C) 2015-2019 Matthias Beyer and contributors +// +// This library is free software; you can redistribute it and/or +// modify it under the terms of the GNU Lesser General Public +// License as published by the Free Software Foundation; version +// 2.1 of the License. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +// + +use std::result::Result as RResult; + +use failure::Fallible as Result; +use chrono::NaiveDateTime; +use uuid::Uuid; +use toml_query::insert::TomlValueInsertExt; + +use libimagstore::store::FileLockEntry; +use libimagstore::store::Store; +use libimagstore::iter::Entries; +use libimagutil::date::datetime_to_string; +use libimagentryutil::isa::Is; + +use crate::status::Status; +use crate::priority::Priority; +use crate::entry::TodoHeader; +use crate::entry::IsTodo; + +pub trait TodoStore<'a> { + fn create_todo(&'a self, + status: Status, + scheduled: Option, + hidden: Option, + due: Option, + prio: Option, + check_sanity: bool) -> Result>; + + fn get_todo_by_uuid(&'a self, uuid: &Uuid) -> Result>>; + + fn get_todos(&self) -> Result; +} + +impl<'a> TodoStore<'a> for Store { + + /// Create a new todo entry + /// + /// # Warning + /// + /// If check_sanity is set to false, this does not sanity-check the scheduled/hidden/due dates. + /// This might result in unintended behaviour (hidden after due date, scheduled before hidden + /// date... etc) + /// + /// An user of this function might want to use `date_sanity_check()` to perform sanity checks + /// before calling TodoStore::create_todo() and show the Err(String) as a warning to user in an + /// interactive way. + fn create_todo(&'a self, + status: Status, + scheduled: Option, + hidden: Option, + due: Option, + prio: Option, + check_sanity: bool) -> Result> + { + if check_sanity { + trace!("Checking sanity before creating todo"); + if let Err(s) = date_sanity_check(scheduled.as_ref(), hidden.as_ref(), due.as_ref()) { + trace!("Not sane."); + return Err(format_err!("{}", s)) + } + } + + let uuid = Uuid::new_v4(); + let uuid_s = format!("{}", uuid.to_hyphenated_ref()); // TODO: not how it is supposed to be + debug!("Created new UUID for todo = {}", uuid_s); + + let mut entry = crate::module_path::new_id(uuid_s).and_then(|id| self.create(id))?; + + let header = TodoHeader { + uuid, + status, + scheduled: scheduled.as_ref().map(datetime_to_string), + hidden: hidden.as_ref().map(datetime_to_string), + due: due.as_ref().map(datetime_to_string), + priority: prio + }; + + debug!("Created header for todo: {:?}", header); + + let _ = entry.get_header_mut().insert_serialized("todo", header)?; + let _ = entry.set_isflag::()?; + + Ok(entry) + } + + fn get_todo_by_uuid(&'a self, uuid: &Uuid) -> Result>> { + let uuid_s = format!("{}", uuid.to_hyphenated_ref()); // TODO: not how it is supposed to be + debug!("Created new UUID for todo = {}", uuid_s); + let id = crate::module_path::new_id(uuid_s)?; + self.get(id) + } + + /// Get all todos using Store::entries() + fn get_todos(&self) -> Result { + self.entries().and_then(|es| es.in_collection("todo")) + } +} + +/// Perform a sanity check on the scheduled/hidden/due dates +/// +/// This function returns a String as error, which can be shown as a warning to the user or as an +/// error. +pub fn date_sanity_check(scheduled: Option<&NaiveDateTime>, + hidden: Option<&NaiveDateTime>, + due: Option<&NaiveDateTime>) + -> RResult<(), String> +{ + if let (Some(sched), Some(hid)) = (scheduled.as_ref(), hidden.as_ref()) { + if sched > hid { + return Err(format!("Scheduled date after hidden date: {s}, {h}", + s = sched, + h = hid)) + } + } + + if let (Some(hid), Some(due)) = (hidden.as_ref(), due.as_ref()) { + if hid > due { + return Err(format!("Hidden date after due date: {h}, {d}", + h = hid, + d = due)) + } + } + + if let (Some(sched), Some(due)) = (scheduled.as_ref(), due.as_ref()) { + if sched > due { + return Err(format!("Scheduled date after due date: {s}, {d}", + s = sched, + d = due)) + } + } + + Ok(()) +} + From 7873d99df51762c795589da66348e21bb74cfb5f Mon Sep 17 00:00:00 2001 From: Matthias Beyer Date: Sun, 14 Jul 2019 12:28:17 +0200 Subject: [PATCH 4/4] Reimplement imag-todo Parts of this commit were written by Leon, but in the process we needed to squash. Here's his original commit message: > Change todo listing behaviours > > This commit changes the todo binary to have the following behaviour: > - `imag-todo`: Print all non-hidden pending todos > - `imag-todo pending`: Print all non-hidden pending todos > - `imag-todo list`: Print all non-hidden non-done todos > - `--done`: Include done > - `--no-pending`: Exclude pending > > Each and every command respects the hidden attribute only on the view > layer, but still pipes the hidden entries to stdout. > > Internally, this introduces a black- and whitelist todo-state matcher, > that can be configured to match only certain todos and thereby > improves reusability of functions over the domain binary. Signed-off-by: Leon Schuermann Signed-off-by: Matthias Beyer --- bin/domain/imag-todo/Cargo.toml | 16 +- bin/domain/imag-todo/etc/on-add.sh | 4 - bin/domain/imag-todo/etc/on-modify.sh | 4 - bin/domain/imag-todo/src/lib.rs | 439 ++++++++++++++++++++------ bin/domain/imag-todo/src/ui.rs | 183 +++++++++-- 5 files changed, 507 insertions(+), 139 deletions(-) delete mode 100644 bin/domain/imag-todo/etc/on-add.sh delete mode 100644 bin/domain/imag-todo/etc/on-modify.sh diff --git a/bin/domain/imag-todo/Cargo.toml b/bin/domain/imag-todo/Cargo.toml index 12a2719a..f1e8dc91 100644 --- a/bin/domain/imag-todo/Cargo.toml +++ b/bin/domain/imag-todo/Cargo.toml @@ -1,5 +1,5 @@ [package] -authors = ["mario "] +authors = ["Matthias Beyer "] name = "imag-todo" version = "0.10.0" @@ -25,10 +25,18 @@ toml = "0.5.1" toml-query = "0.9.2" is-match = "0.1.0" failure = "0.1.5" +chrono = "0.4" +filters = "0.3" +kairos = "0.3" +resiter = "0.4.0" -libimagrt = { version = "0.10.0", path = "../../../lib/core/libimagrt" } -libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" } -libimagtodo = { version = "0.10.0", path = "../../../lib/domain/libimagtodo" } +libimagrt = { version = "0.10.0", path = "../../../lib/core/libimagrt" } +libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" } +libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" } +libimagentryedit = { version = "0.10.0", path = "../../../lib/entry/libimagentryedit" } +libimagtodo = { version = "0.10.0", path = "../../../lib/domain/libimagtodo" } +libimagutil = { version = "0.10.0", path = "../../../lib/etc/libimagutil" } +libimagentryview = { version = "0.10.0", path = "../../../lib/entry/libimagentryview" } [dependencies.clap] version = "2.33.0" diff --git a/bin/domain/imag-todo/etc/on-add.sh b/bin/domain/imag-todo/etc/on-add.sh deleted file mode 100644 index a58e4989..00000000 --- a/bin/domain/imag-todo/etc/on-add.sh +++ /dev/null @@ -1,4 +0,0 @@ -#/!usr/bin/env bash - -imag todo tw-hook --add - diff --git a/bin/domain/imag-todo/etc/on-modify.sh b/bin/domain/imag-todo/etc/on-modify.sh deleted file mode 100644 index 89be96d0..00000000 --- a/bin/domain/imag-todo/etc/on-modify.sh +++ /dev/null @@ -1,4 +0,0 @@ -#/!usr/bin/env bash - -imag todo tw-hook --delete - diff --git a/bin/domain/imag-todo/src/lib.rs b/bin/domain/imag-todo/src/lib.rs index 16f42262..f6598a7f 100644 --- a/bin/domain/imag-todo/src/lib.rs +++ b/bin/domain/imag-todo/src/lib.rs @@ -35,30 +35,48 @@ )] extern crate clap; -#[macro_use] extern crate log; extern crate toml; extern crate toml_query; -#[macro_use] extern crate is_match; -extern crate failure; +extern crate chrono; +extern crate filters; +extern crate kairos; +#[macro_use] extern crate log; +#[macro_use] extern crate failure; +extern crate resiter; extern crate libimagrt; +extern crate libimagstore; extern crate libimagerror; +extern crate libimagentryedit; extern crate libimagtodo; +extern crate libimagutil; +extern crate libimagentryview; -use std::process::{Command, Stdio}; -use std::io::stdin; use std::io::Write; +use std::result::Result as RResult; + +use clap::ArgMatches; +use chrono::NaiveDateTime; use failure::Error; use failure::Fallible as Result; +use failure::err_msg; use clap::App; +use resiter::AndThen; +use resiter::IterInnerOkOrElse; -use libimagrt::runtime::Runtime; +use libimagentryedit::edit::Edit; +use libimagentryview::viewer::ViewFromIter; +use libimagentryview::viewer::Viewer; use libimagrt::application::ImagApplication; -use libimagtodo::taskstore::TaskStore; -use libimagerror::trace::{MapErrTrace, trace_error}; -use libimagerror::iter::TraceIterator; -use libimagerror::exit::ExitUnwrap; -use libimagerror::io::ToExitCode; +use libimagrt::runtime::Runtime; +use libimagstore::iter::get::*; +use libimagstore::store::Entry; +use libimagstore::store::FileLockEntry; +use libimagtodo::entry::Todo; +use libimagtodo::priority::Priority; +use libimagtodo::status::Status; +use libimagtodo::store::TodoStore; +use libimagutil::date::datetime_to_string; mod ui; @@ -70,21 +88,20 @@ pub enum ImagTodo {} impl ImagApplication for ImagTodo { fn run(rt: Runtime) -> Result<()> { match rt.cli().subcommand_name() { - Some("tw-hook") => tw_hook(&rt), - Some("list") => list(&rt), - Some(other) => { + Some("create") => create(&rt), + Some("show") => show(&rt), + Some("mark") => mark(&rt), + Some("pending") | None => list_todos(&rt, &StatusMatcher::new().is(Status::Pending), false), + Some("list") => list(&rt), + Some(other) => { debug!("Unknown command"); - let _ = rt.handle_unknown_subcommand("imag-todo", other, rt.cli()) - .map_err_trace_exit_unwrap() - .code() - .map(::std::process::exit); + if rt.handle_unknown_subcommand("imag-todo", other, rt.cli())?.success() { + Ok(()) + } else { + Err(err_msg("Failed to handle unknown subcommand")) + } } - None => { - warn!("No command"); - }, - }; - - Ok(()) + } // end match scmd } fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> { @@ -104,99 +121,311 @@ impl ImagApplication for ImagTodo { } } -fn tw_hook(rt: &Runtime) { - let subcmd = rt.cli().subcommand_matches("tw-hook").unwrap(); - if subcmd.is_present("add") { - let stdin = stdin(); +/// A black- and whitelist for matching statuses of todo entries +/// +/// The blacklist is checked first, followed by the whitelist. +/// In case the whitelist is empty, the StatusMatcher works with a +/// blacklist-only approach. +#[derive(Debug)] +pub struct StatusMatcher { + is: Vec, + is_not: Vec, +} - // implements BufRead which is required for `Store::import_task_from_reader()` - let stdin = stdin.lock(); +impl StatusMatcher { + pub fn new() -> Self { + StatusMatcher { + is: Vec::new(), + is_not: Vec::new(), + } + } - let (_, line, uuid ) = rt - .store() - .import_task_from_reader(stdin) - .map_err_trace_exit_unwrap(); + pub fn is(mut self, s: Status) -> Self { + self.add_is(s); + self + } - writeln!(rt.stdout(), "{}\nTask {} stored in imag", line, uuid) - .to_exit_code() - .unwrap_or_exit(); + pub fn add_is(&mut self, s: Status) { + self.is.push(s); + } - } 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(); - rt.store().delete_tasks_by_imports(stdin.lock()).map_err_trace().ok(); - } else { - // Should not be possible, as one argument is required via - // ArgGroup - unreachable!(); + pub fn is_not(mut self, s: Status) -> Self { + self.add_is_not(s); + self + } + + pub fn add_is_not(&mut self, s: Status) { + self.is_not.push(s); + } + + pub fn matches(&self, todo: Status) -> bool { + if self.is_not.iter().find(|t| **t == todo).is_some() { + // On blacklist + false + } else if self.is.len() < 1 || self.is.iter().find(|t| **t == todo).is_some() { + // No whitelist or on whitelist + true + } else { + // Not on blacklist, but whitelist exists and not on it either + false + } } } -fn list(rt: &Runtime) { - use toml_query::read::TomlValueReadTypeExt; +fn create(rt: &Runtime) -> Result<()> { + debug!("Creating todo"); + let scmd = rt.cli().subcommand().1.unwrap(); // safe by clap - let subcmd = rt.cli().subcommand_matches("list").unwrap(); - let verbose = subcmd.is_present("verbose"); + let scheduled: Option = get_datetime_arg(&scmd, "create-scheduled")?; + let hidden: Option = get_datetime_arg(&scmd, "create-hidden")?; + let due: Option = get_datetime_arg(&scmd, "create-due")?; + let prio: Option = scmd.value_of("create-prio").map(prio_from_str).transpose()?; + let status: Status = scmd.value_of("create-status").map(Status::from_str).unwrap()?; + let edit = scmd.is_present("create-edit"); + let text = scmd.value_of("text").unwrap(); - // Helper for toml_query::read::TomlValueReadExt::read() return value, which does only - // return Result instead of Result>, which is a real inconvenience. - // - let no_identifier = |e: &::toml_query::error::Error| -> bool { - is_match!(e, &::toml_query::error::Error::IdentifierNotFoundInDocument(_)) - }; + trace!("Creating todo with these variables:"); + trace!("scheduled = {:?}", scheduled); + trace!("hidden = {:?}", hidden); + trace!("due = {:?}", due); + trace!("prio = {:?}", prio); + trace!("status = {:?}", status); + trace!("edit = {}", edit); + trace!("text = {:?}", text); - let res = rt.store().all_tasks() // get all tasks - .map(|iter| { // and if this succeeded - // filter out the ones were we can read the uuid - let uuids : Vec<_> = iter.trace_unwrap_exit().filter_map(|storeid| { - match rt.store().retrieve(storeid) { - Ok(fle) => { - match fle.get_header().read_string("todo.uuid") { - Ok(Some(ref u)) => Some(u.clone()), - Ok(None) => { - error!("Header missing field in {}", fle.get_location()); - None - }, - Err(e) => { - if !no_identifier(&e) { - trace_error(&Error::from(e)); - } - None - } - } - }, - Err(e) => { - trace_error(&e); - None - }, - } - }) - .collect(); + let mut entry = rt.store().create_todo(status, scheduled, hidden, due, prio, true)?; + debug!("Created: todo {}", entry.get_uuid()?); - // 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| { - error!("Failed to execute `task` on the commandline: {:?}. I'm dying now.", e); - ::std::process::exit(1) - }) - .wait_with_output() - .unwrap_or_else(|e| panic!("failed to unwrap output: {}", e)); + debug!("Setting content"); + *entry.get_content_mut() = text.to_string(); - String::from_utf8(output.stdout) - .unwrap_or_else(|e| panic!("failed to execute: {}", e)) - } else { // ... else just join them - uuids.join("\n") - }; + if edit { + debug!("Editing content"); + entry.edit_content(&rt)?; + } - // and then print that - writeln!(rt.stdout(), "{}", outstring).to_exit_code().unwrap_or_exit(); - }); - - res.map_err_trace().ok(); + rt.report_touched(entry.get_location()).map_err(Error::from) +} + +fn mark(rt: &Runtime) -> Result<()> { + fn mark_todos_as(rt: &Runtime, status: Status) -> Result<()> { + rt.ids::()? + .ok_or_else(|| err_msg("No ids supplied"))? + .into_iter() + .map(Ok) + .into_get_iter(rt.store()) + .map_inner_ok_or_else(|| err_msg("Did not find one entry")) + .and_then_ok(|e| rt.report_touched(e.get_location()).map_err(Error::from).map(|_| e)) + .and_then_ok(|mut e| e.set_status(status.clone())) + .collect() + } + + let scmd = rt.cli().subcommand().1.unwrap(); + match scmd.subcommand_name() { + Some("done") => mark_todos_as(rt, Status::Done), + Some("delete") => mark_todos_as(rt, Status::Deleted), + Some("pending") => mark_todos_as(rt, Status::Pending), + Some(other) => Err(format_err!("Unknown mark type selected: {}", other)), + None => Err(format_err!("No mark type selected, doing nothing!")), + } +} + +/// Generic todo listing function +/// +/// Supports filtering of todos by status using the passed in StatusMatcher +fn list_todos(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool) -> Result<()> { + use filters::failable::filter::FailableFilter; + debug!("Listing todos with status filter {:?}", matcher); + + let now = { + let now = chrono::offset::Local::now(); + NaiveDateTime::new(now.date().naive_local(), now.time()) + }; + + let filter_hidden = |todo: &FileLockEntry<'_>| -> Result { + Ok(todo.get_hidden()?.map(|hid| hid > now).unwrap_or(true)) + }; + + struct TodoViewer { + details: bool, + } + impl Viewer for TodoViewer { + fn view_entry(&self, entry: &Entry, sink: &mut W) -> RResult<(), libimagentryview::error::Error> + where W: Write + { + use libimagentryview::error::Error as E; + + if !entry.is_todo().map_err(E::from)? { + return Err(format_err!("Not a Todo: {}", entry.get_location())).map_err(E::from); + } + + let uuid = entry.get_uuid().map_err(E::from)?; + let status = entry.get_status().map_err(E::from)?; + let status = status.as_str(); + let first_line = entry.get_content() + .lines() + .next() + .unwrap_or(""); + + if !self.details { + writeln!(sink, "{uuid} - {status} : {first_line}", + uuid = uuid, + status = status, + first_line = first_line) + } else { + let sched = get_dt_str(entry.get_scheduled(), "Not scheduled")?; + let hidden = get_dt_str(entry.get_hidden(), "Not hidden")?; + let due = get_dt_str(entry.get_due(), "No due")?; + let priority = entry.get_priority().map_err(E::from)?.map(|p| p.as_str().to_string()) + .unwrap_or("No prio".to_string()); + + writeln!(sink, "{uuid} - {status} - {sched} - {hidden} - {due} - {prio}: {first_line}", + uuid = uuid, + status = status, + sched = sched, + hidden = hidden, + due = due, + prio = priority, + first_line = first_line) + } + .map_err(libimagentryview::error::Error::from) + } + } + + let viewer = TodoViewer { details: false }; + + rt.store() + .get_todos()? + .into_get_iter() + .map_inner_ok_or_else(|| err_msg("Did not find one entry")) + .filter_map(|r| { + match r.and_then(|e| e.get_status().map(|s| (s, e))) { + Err(e) => Some(Err(e)), + Ok((st, e)) => if matcher.matches(st) { + Some(Ok(e)) + } else { + None + } + } + }) + .and_then_ok(|entry| { + if show_hidden || filter_hidden.filter(&entry)? { + viewer.view_entry(&entry, &mut rt.stdout())?; + } + + rt.report_touched(entry.get_location()).map_err(Error::from) + }) + .collect() +} + +/// Generic todo items list function +/// +/// This sets up filtes based on the command line and prints out a list of todos +fn list(rt: &Runtime) -> Result<()> { + debug!("Listing todo"); + let scmd = rt.cli().subcommand().1; + let table = scmd.map(|s| s.is_present("list-table")).unwrap_or(true); + let hidden = scmd.map(|s| s.is_present("list-hidden")).unwrap_or(false); + let done = scmd.map(|s| s.is_present("list-done")).unwrap_or(false); + let nopending = scmd.map(|s| s.is_present("list-nopending")).unwrap_or(true); + + trace!("table = {}", table); + trace!("hidden = {}", hidden); + trace!("done = {}", done); + trace!("nopending = {}", nopending); + + let mut matcher = StatusMatcher::new(); + if !done { matcher.add_is_not(Status::Done); } + if nopending { matcher.add_is_not(Status::Pending); } + + // TODO: Support printing as ASCII table + list_todos(rt, &matcher, hidden) +} + +fn show(rt: &Runtime) -> Result<()> { + #[derive(Default)] + struct TodoShow; + impl Viewer for TodoShow { + + fn view_entry(&self, entry: &Entry, sink: &mut W) -> RResult<(), libimagentryview::error::Error> + where W: Write + { + use libimagentryview::error::Error as E; + + if !entry.is_todo().map_err(E::from)? { + return Err(format_err!("Not a Todo: {}", entry.get_location())).map_err(E::from); + } + + let uuid = entry.get_uuid().map_err(E::from)?; + let status = entry.get_status().map_err(E::from)?; + let status = status.as_str(); + let text = entry.get_content(); + let sched = get_dt_str(entry.get_scheduled(), "Not scheduled")?; + let hidden = get_dt_str(entry.get_hidden(), "Not hidden")?; + let due = get_dt_str(entry.get_due(), "No due")?; + let priority = entry.get_priority().map_err(E::from)?.map(|p| p.as_str().to_string()) + .unwrap_or("No prio".to_string()); + + writeln!(sink, "{uuid}\nStatus: {status}\nPriority: {prio}\nScheduled: {sched}\nHidden: {hidden}\nDue: {due}\n\n{text}", + uuid = uuid, + status = status, + sched = sched, + hidden = hidden, + due = due, + prio = priority, + text = text) + .map_err(Error::from) + .map_err(libimagentryview::error::Error::from) + } + } + + rt.ids::()? + .ok_or_else(|| err_msg("No ids supplied"))? + .into_iter() + .map(Ok) + .into_get_iter(rt.store()) + .map_inner_ok_or_else(|| err_msg("Did not find one entry")) + .and_then_ok(|e| rt.report_touched(e.get_location()).map_err(Error::from).map(|_| e)) + .collect::>>()? + .into_iter() + .view::(&mut rt.stdout()) + .map_err(Error::from) +} + +// +// utility functions +// + +fn get_datetime_arg(scmd: &ArgMatches, argname: &'static str) -> Result> { + use kairos::timetype::TimeType; + use kairos::parser; + + match scmd.value_of(argname) { + None => Ok(None), + Some(v) => match parser::parse(v)? { + parser::Parsed::TimeType(TimeType::Moment(moment)) => Ok(Some(moment)), + parser::Parsed::TimeType(other) => { + Err(format_err!("You did not pass a date, but a {}", other.name())) + }, + parser::Parsed::Iterator(_) => { + Err(format_err!("Argument {} results in a list of dates, but we need a single date.", v)) + } + } + } +} + +fn prio_from_str>(s: S) -> Result { + match s.as_ref() { + "h" => Ok(Priority::High), + "m" => Ok(Priority::Medium), + "l" => Ok(Priority::Low), + other => Err(format_err!("Unsupported Priority: '{}'", other)), + } +} + +fn get_dt_str(d: Result>, s: &str) -> RResult { + Ok(d.map_err(libimagentryview::error::Error::from)? + .map(|v| datetime_to_string(&v)) + .unwrap_or(s.to_string())) } diff --git a/bin/domain/imag-todo/src/ui.rs b/bin/domain/imag-todo/src/ui.rs index 073508cf..3299b7de 100644 --- a/bin/domain/imag-todo/src/ui.rs +++ b/bin/domain/imag-todo/src/ui.rs @@ -17,45 +17,184 @@ // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // -use clap::{Arg, App, ArgGroup, SubCommand}; +use std::path::PathBuf; +use clap::{Arg, ArgMatches, App, SubCommand}; +use failure::Fallible as Result; + +use libimagstore::storeid::StoreId; +use libimagstore::storeid::IntoStoreId; +use libimagrt::runtime::IdPathProvider; + 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") + .subcommand(SubCommand::with_name("create") + .about("Create task") .version("0.1") - .arg(Arg::with_name("add") - .long("add") - .short("a") - .takes_value(false) + .arg(Arg::with_name("create-scheduled") + .long("scheduled") + .short("s") + .takes_value(true) .required(false) - .help("For use in an on-add hook")) + .help("Set a 'scheduled' date/time") + ) - .arg(Arg::with_name("delete") - .long("delete") + .arg(Arg::with_name("create-hidden") + .long("hidden") + .short("h") + .takes_value(true) + .required(false) + .help("Set a 'hidden' date/time") + ) + + .arg(Arg::with_name("create-due") + .long("due") .short("d") + .takes_value(true) + .required(false) + .help("Set a 'due' date/time") + ) + + .arg(Arg::with_name("create-prio") + .long("prio") + .short("p") + .takes_value(true) + .required(false) + .help("Set a priority") + .possible_values(&["h", "m", "l"]) + ) + + .arg(Arg::with_name("create-status") + .long("status") + .takes_value(true) + .required(false) + .help("Set a status, useful if the task is already done") + .default_value("pending") + .possible_values(&["pending", "done", "deleted"]) + ) + + .arg(Arg::with_name("create-edit") + .long("edit") + .short("e") .takes_value(false) .required(false) - .help("For use in an on-delete hook")) + .help("Create and then edit the entry") + ) - .group(ArgGroup::with_name("taskwarrior hooks") - .args(&[ "add", - "delete", - ]) - .required(true)) + .arg(Arg::with_name("text") + .index(1) + .multiple(true) + .required(true) + .help("Text for the todo") + ) + ) + + .subcommand(SubCommand::with_name("pending") + .arg(Arg::with_name("todos") + .index(1) + .takes_value(true) + .required(false) + .help("List pending todos (same as 'list' command without arguments)") + ) ) .subcommand(SubCommand::with_name("list") - .about("List all tasks") + .about("List tasks (default)") .version("0.1") - .arg(Arg::with_name("verbose") - .long("verbose") - .short("v") + .arg(Arg::with_name("list-table") + .long("table") + .short("T") .takes_value(false) .required(false) - .help("Asks taskwarrior for all the details") + .help("Print a nice ascii-table") ) - ) + + .arg(Arg::with_name("list-hidden") + .long("hidden") + .short("H") + .takes_value(false) + .required(false) + .help("Print also hidden todos") + ) + + .arg(Arg::with_name("list-done") + .long("done") + .short("D") + .takes_value(false) + .required(false) + .help("Print also done todos") + ) + + .arg(Arg::with_name("list-nopending") + .long("no-pending") + .short("P") + .takes_value(false) + .required(false) + .help("Do not print pending tasks") + ) + + ) + + .subcommand(SubCommand::with_name("show") + .arg(Arg::with_name("todos") + .index(1) + .takes_value(true) + .required(false) + .help("Show the passed todos") + ) + ) + + .subcommand(SubCommand::with_name("mark") + .about("Mark tasks as pending, done or deleted") + .version("0.1") + + .subcommand(SubCommand::with_name("pending") + .arg(Arg::with_name("todos") + .index(1) + .takes_value(true) + .required(false) + .help("List pending todos (same as 'list' command without arguments)") + ) + ) + + .subcommand(SubCommand::with_name("done") + .arg(Arg::with_name("todos") + .index(1) + .takes_value(true) + .required(false) + .help("Mark the passed todos as done") + ) + ) + + .subcommand(SubCommand::with_name("deleted") + .arg(Arg::with_name("todos") + .index(1) + .takes_value(true) + .required(false) + .help("Mark the passed todos as deleted") + ) + ) + ) + } + +pub struct PathProvider; +impl IdPathProvider for PathProvider { + fn get_ids(matches: &ArgMatches) -> Result>> { + match matches.subcommand() { + ("show", Some(scmd)) => scmd.values_of("todos"), + ("show", None) => unimplemented!(), + _ => unimplemented!() + } + .map(|v| v + .into_iter() + .map(PathBuf::from) + .map(|pb| pb.into_storeid()) + .collect::>>() + ) + .transpose() + } +} +