diff --git a/bin/domain/imag-todo/Cargo.toml b/bin/domain/imag-todo/Cargo.toml index f1e8dc91..bbb5667a 100644 --- a/bin/domain/imag-todo/Cargo.toml +++ b/bin/domain/imag-todo/Cargo.toml @@ -43,6 +43,26 @@ version = "2.33.0" default-features = false features = ["color", "suggestions", "wrap_help"] +[dependencies.task-hookrs] +version = "0.7.0" +optional = true + +[dependencies.uuid] +version = "0.7.4" +features = ["v4"] +optional = true + +[dependencies.libimagentrytag] +version = "0.10.0" +path = "../../../lib/entry/libimagentrytag" +optional = true + +[dependencies.libimagentrylink] +version = "0.10.0" +path = "../../../lib/entry/libimagentrylink" +optional = true + + [lib] name = "libimagtodofrontend" path = "src/lib.rs" @@ -50,3 +70,8 @@ path = "src/lib.rs" [[bin]] name = "imag-todo" path = "src/bin.rs" + +[features] +default = [] +import-taskwarrior = [ "task-hookrs", "uuid", "libimagentrytag", "libimagentrylink" ] + diff --git a/bin/domain/imag-todo/src/import.rs b/bin/domain/imag-todo/src/import.rs new file mode 100644 index 00000000..6194bcbd --- /dev/null +++ b/bin/domain/imag-todo/src/import.rs @@ -0,0 +1,162 @@ +// +// 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; +use failure::err_msg; + +use libimagrt::runtime::Runtime; + +pub fn import(rt: &Runtime) -> Result<()> { + let scmd = rt.cli().subcommand().1.unwrap(); + + match scmd.subcommand_name() { + None => Err(err_msg("No subcommand called")), + Some("taskwarrior") => import_taskwarrior(rt), + Some(other) => { + debug!("Unknown command"); + if rt.handle_unknown_subcommand("imag-todo-import", other, rt.cli())?.success() { + Ok(()) + } else { + Err(err_msg("Failed to handle unknown subcommand")) + } + }, + } +} + +#[allow(unused_variables)] +fn import_taskwarrior(rt: &Runtime) -> Result<()> { + #[cfg(not(feature = "import-taskwarrior"))] + { + Err(err_msg("Binary not compiled with taskwarrior import functionality")) + } + + #[cfg(feature = "import-taskwarrior")] + { + use std::collections::HashMap; + use std::ops::Deref; + + use uuid::Uuid; + + use libimagtodo::status::Status; + use libimagtodo::priority::Priority; + use libimagtodo::store::TodoStore; + use libimagentrytag::tagable::Tagable; + use libimagentrylink::linkable::Linkable; + + use task_hookrs::import::import as taskwarrior_import; + use task_hookrs::priority::TaskPriority; + use task_hookrs::status::TaskStatus; + + let store = rt.store(); + if !rt.input_is_pipe() { + return Err(err_msg("Cannot get stdin for importing tasks")) + } + let stdin = ::std::io::stdin(); + + let translate_status = |twstatus: &TaskStatus| -> Option { + match *twstatus { + TaskStatus::Pending => Some(Status::Pending), + TaskStatus::Completed => Some(Status::Done), + TaskStatus::Deleted => Some(Status::Deleted), + _ => Some(Status::Deleted), // default to deleted if taskwarrior data does not have a status + } + }; + + let translate_prio = |p: &TaskPriority| -> Priority { + match p { + TaskPriority::Low => Priority::Low, + TaskPriority::Medium => Priority::Medium, + TaskPriority::High => Priority::High, + } + }; + + taskwarrior_import(stdin)? + .into_iter() + .map(|task| { + let hash = task.uuid().clone(); + let mut todo = store + .todo_builder() + .with_check_sanity(false) // data should be imported, even if it is not sane + .with_status(translate_status(task.status())) + .with_uuid(Some(task.uuid().clone())) + .with_due(task.due().map(Deref::deref).cloned()) + .with_scheduled(task.scheduled().map(Deref::deref).cloned()) + .with_hidden(task.wait().map(Deref::deref).cloned()) + .with_prio(task.priority().map(|p| translate_prio(p))) + .build(rt.store())?; + + todo.set_content(task.description().clone()); + + if let Some(tags) = task.tags() { + tags.into_iter().map(|tag| { + let tag = tag.clone(); + if libimagentrytag::tag::is_tag_str(&tag).is_err() { + warn!("Not a valid tag, ignoring: {}", tag); + Ok(()) + } else { + todo.add_tag(tag) + } + }).collect::>>()?; + } + + if let Some(annos) = task.annotations() { + // We do not import annotations as imag annotations, but add them as text to + // the entry, which is more sane IMO. + // + // this could be changed into a configurable thing later. + let anno = annos.iter() + .map(|anno| anno.description()) + .map(String::clone) + .collect::>() + .join("\n"); + todo.get_content_mut().push('\n'); + todo.get_content_mut().push_str(&anno); + } + + let dependends = task.depends().cloned().unwrap_or_else(|| vec![]); + Ok((hash, dependends)) + }) + .collect::>>>()? + + // + // We actually _have_ to collect here, because we must ensure that all imported Todo + // entries are in the store before we can continue and link them together (which is + // what happens next) + // + + .iter() + .filter(|(_, list)| !list.is_empty()) + .map(|(key, list)| { + let mut entry = store.get_todo_by_uuid(key)?.ok_or_else(|| { + format_err!("Cannot find todo by UUID: {}", key) + })?; + + list.iter() + .map(move |element| { + store.get_todo_by_uuid(element)? + .ok_or_else(|| { + format_err!("Cannot find todo by UUID: {}", key) + }) + .and_then(|mut target| entry.add_link(&mut target)) + }) + .collect::>() + }) + .collect::>() + } +} diff --git a/bin/domain/imag-todo/src/lib.rs b/bin/domain/imag-todo/src/lib.rs index f6598a7f..2b6d4d15 100644 --- a/bin/domain/imag-todo/src/lib.rs +++ b/bin/domain/imag-todo/src/lib.rs @@ -44,6 +44,18 @@ extern crate kairos; #[macro_use] extern crate failure; extern crate resiter; +#[cfg(feature = "import-taskwarrior")] +extern crate task_hookrs; + +#[cfg(feature = "import-taskwarrior")] +extern crate uuid; + +#[cfg(feature = "import-taskwarrior")] +extern crate libimagentrytag; + +#[cfg(feature = "import-taskwarrior")] +extern crate libimagentrylink; + extern crate libimagrt; extern crate libimagstore; extern crate libimagerror; @@ -79,6 +91,7 @@ use libimagtodo::store::TodoStore; use libimagutil::date::datetime_to_string; mod ui; +mod import; /// Marker enum for implementing ImagApplication on /// @@ -93,6 +106,7 @@ impl ImagApplication for ImagTodo { Some("mark") => mark(&rt), Some("pending") | None => list_todos(&rt, &StatusMatcher::new().is(Status::Pending), false), Some("list") => list(&rt), + Some("import") => import::import(&rt), Some(other) => { debug!("Unknown command"); if rt.handle_unknown_subcommand("imag-todo", other, rt.cli())?.success() { diff --git a/bin/domain/imag-todo/src/ui.rs b/bin/domain/imag-todo/src/ui.rs index 3299b7de..8ed92dba 100644 --- a/bin/domain/imag-todo/src/ui.rs +++ b/bin/domain/imag-todo/src/ui.rs @@ -178,6 +178,16 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { ) ) + + .subcommand(SubCommand::with_name("import") + .about("Import todos from other tool") + .version("0.1") + .subcommand(SubCommand::with_name("taskwarrior") + .about("Import from taskwarrior by piping 'task export' to this subcommand.") + .version("0.1") + ) + ) + } pub struct PathProvider; diff --git a/lib/domain/libimagtodo/src/builder.rs b/lib/domain/libimagtodo/src/builder.rs new file mode 100644 index 00000000..45cc45a7 --- /dev/null +++ b/lib/domain/libimagtodo/src/builder.rs @@ -0,0 +1,130 @@ +// +// 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 chrono::NaiveDateTime; +use failure::Fallible as Result; +use failure::err_msg; +use toml_query::insert::TomlValueInsertExt; +use uuid::Uuid; + +use libimagstore::store::Store; +use libimagstore::store::FileLockEntry; +use libimagentryutil::isa::Is; +use libimagutil::date::datetime_to_string; + +use crate::priority::Priority; +use crate::status::Status; +use crate::entry::IsTodo; +use crate::entry::TodoHeader; +use crate::store::date_sanity_check; + + +pub struct TodoBuilder { + uuid: Option, + status: Option, + scheduled: Option, + hidden: Option, + due: Option, + prio: Option, + check_sanity: bool, +} + +impl TodoBuilder { + pub(crate) fn new() -> Self { + TodoBuilder { + uuid: None, + status: None, + scheduled: None, + hidden: None, + due: None, + prio: None, + check_sanity: true, + } + } + + pub fn build<'a>(self, store: &'a Store) -> Result> { + let uuid = self.uuid.ok_or_else(|| err_msg("Uuid missing"))?; + let status = self.status.ok_or_else(|| err_msg("Status missing"))?; + + if self.check_sanity { + trace!("Checking sanity before creating todo"); + if let Err(s) = date_sanity_check(self.scheduled.as_ref(), self.hidden.as_ref(), self.due.as_ref()) { + trace!("Not sane."); + return Err(format_err!("{}", s)) + } + } + + 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| store.create(id))?; + + let header = TodoHeader { + uuid, + status, + scheduled: self.scheduled.as_ref().map(datetime_to_string), + hidden: self.hidden.as_ref().map(datetime_to_string), + due: self.due.as_ref().map(datetime_to_string), + priority: self.prio + }; + + debug!("Created header for todo: {:?}", header); + + let _ = entry.get_header_mut().insert_serialized("todo", header)?; + let _ = entry.set_isflag::()?; + + Ok(entry) + } + + pub fn with_uuid(mut self, uuid: Option) -> Self { + self.uuid = uuid; + self + } + + pub fn with_status(mut self, status: Option) -> Self { + self.status = status; + self + } + + pub fn with_scheduled(mut self, scheduled: Option) -> Self { + self.scheduled = scheduled; + self + } + + pub fn with_hidden(mut self, hidden: Option) -> Self { + self.hidden = hidden; + self + } + + pub fn with_due(mut self, due: Option) -> Self { + self.due = due; + self + } + + pub fn with_prio(mut self, prio: Option) -> Self { + self.prio = prio; + self + } + + pub fn with_check_sanity(mut self, b: bool) -> Self { + self.check_sanity = b; + self + } + +} diff --git a/lib/domain/libimagtodo/src/lib.rs b/lib/domain/libimagtodo/src/lib.rs index 8901077d..40cf5a87 100644 --- a/lib/domain/libimagtodo/src/lib.rs +++ b/lib/domain/libimagtodo/src/lib.rs @@ -34,6 +34,7 @@ extern crate libimagutil; #[macro_use] extern crate libimagstore; #[macro_use] extern crate libimagentryutil; +pub mod builder; pub mod entry; pub mod iter; pub mod priority; diff --git a/lib/domain/libimagtodo/src/store.rs b/lib/domain/libimagtodo/src/store.rs index cc1f2aa0..0aec566d 100644 --- a/lib/domain/libimagtodo/src/store.rs +++ b/lib/domain/libimagtodo/src/store.rs @@ -22,20 +22,19 @@ 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; +use crate::builder::TodoBuilder; pub trait TodoStore<'a> { + + fn todo_builder(&self) -> TodoBuilder; + fn create_todo(&'a self, status: Status, scheduled: Option, @@ -51,6 +50,13 @@ pub trait TodoStore<'a> { impl<'a> TodoStore<'a> for Store { + /// Get a TodoBuilder instance, which can be used to build a todo object. + /// + /// The TodoBuilder::new() constructor is not exposed, this function should be used instead. + fn todo_builder(&self) -> TodoBuilder { + TodoBuilder::new() + } + /// Create a new todo entry /// /// # Warning @@ -70,35 +76,15 @@ impl<'a> TodoStore<'a> for Store { 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) + TodoBuilder::new() + .with_status(Some(status)) + .with_uuid(Some(Uuid::new_v4())) + .with_scheduled(scheduled) + .with_hidden(hidden) + .with_due(due) + .with_prio(prio) + .with_check_sanity(check_sanity) + .build(&self) } fn get_todo_by_uuid(&'a self, uuid: &Uuid) -> Result>> {