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;