summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2019-11-09 18:38:12 +0100
committerMatthias Beyer <mail@beyermatthias.de>2019-11-09 18:38:12 +0100
commitead9438c412c6f14fc6f3d77fb88ca40a7634452 (patch)
tree5436297c0c82e7621d4bda64021539db0d4dc11f
parent01db294700a594b9b004855097b3bcfee9946d61 (diff)
parent7873d99df51762c795589da66348e21bb74cfb5f (diff)
Merge branch 'libimagtodo-req-rewrite' into master
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
-rw-r--r--bin/domain/imag-todo/Cargo.toml18
-rw-r--r--bin/domain/imag-todo/etc/on-add.sh4
-rw-r--r--bin/domain/imag-todo/etc/on-modify.sh4
-rw-r--r--bin/domain/imag-todo/src/lib.rs449
-rw-r--r--bin/domain/imag-todo/src/ui.rs183
-rw-r--r--doc/src/05100-lib-todo.md63
-rw-r--r--lib/domain/libimagtodo/Cargo.toml32
-rw-r--r--lib/domain/libimagtodo/src/entry.rs138
-rw-r--r--lib/domain/libimagtodo/src/iter.rs114
-rw-r--r--lib/domain/libimagtodo/src/lib.rs44
-rw-r--r--lib/domain/libimagtodo/src/priority.rs68
-rw-r--r--lib/domain/libimagtodo/src/status.rs72
-rw-r--r--lib/domain/libimagtodo/src/store.rs152
-rw-r--r--lib/domain/libimagtodo/src/task.rs45
-rw-r--r--lib/domain/libimagtodo/src/taskstore.rs184
15 files changed, 1105 insertions, 465 deletions
diff --git a/bin/domain/imag-todo/Cargo.toml b/bin/domain/imag-todo/Cargo.toml
index 12a2719..f1e8dc9 100644
--- a/bin/domain/imag-todo/Cargo.toml
+++ b/bin/domain/imag-todo/Cargo.toml
@@ -1,5 +1,5 @@
[package]
-authors = ["mario <mario-krehl@gmx.de>"]
+authors = ["Matthias Beyer <mail@beyermatthias.de>"]
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"
-
-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" }
+chrono = "0.4"
+filters = "0.3"
+kairos = "0.3"
+resiter = "0.4.0"
+
+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 a58e498..0000000
--- 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 89be96d..0000000
--- 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 16f4226..f6598a7 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();
-
- // implements BufRead which is required for `Store::import_task_from_reader()`
- let stdin = stdin.lock();
-
- let (_, line, uuid ) = rt
- .store()
- .import_task_from_reader(stdin)
- .map_err_trace_exit_unwrap();
-
- writeln!(rt.stdout(), "{}\nTask {} stored in imag", line, uuid)
- .to_exit_code()
- .unwrap_or_exit();
-
- } 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!();
+/// 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<Status>,
+ is_not: Vec<Status>,
+}
+
+impl StatusMatcher {
+ pub fn new() -> Self {
+ StatusMatcher {
+ is: Vec::new(),
+ is_not: Vec::new(),
+ }
+ }
+
+ pub fn is(mut self, s: Status) -> Self {
+ self.add_is(s);
+ self
+ }
+
+ pub fn add_is(&mut self, s: Status) {
+ self.is.push(s);
+ }
+
+ 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 create(rt: &Runtime) -> Result<()> {
+ debug!("Creating todo");
+ let scmd = rt.cli().subcommand().1.unwrap(); // safe by clap
+
+ let scheduled: Option<NaiveDateTime> = get_datetime_arg(&scmd, "create-scheduled")?;
+ let hidden: Option<NaiveDateTime> = get_datetime_arg(&scmd, "create-hidden")?;
+ let due: Option<NaiveDateTime> = get_datetime_arg(&scmd, "create-due")?;
+ let prio: Option<Priority> = 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();
+
+ 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 mut entry = rt.store().create_todo(status, scheduled, hidden, due, prio, true)?;
+ debug!("Created: todo {}", entry.get_uuid()?);
+
+ debug!("Setting content");
+ *entry.get_content_mut() = text.to_string();
+
+ if edit {
+ debug!("Editing content");
+ entry.edit_content(&rt)?;
+ }
+
+ 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::<crate::ui::PathProvider>()?
+ .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!")),
}
}
-fn list(rt: &Runtime) {
- use toml_query::read::TomlValueReadTypeExt;
+/// 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 subcmd = rt.cli().subcommand_matches("list").unwrap();
- let verbose = subcmd.is_present("verbose");
+ let now = {
+ let now = chrono::offset::Local::now();
+ NaiveDateTime::new(now.date().naive_local(), now.time())
+ };
- // Helper for toml_query::read::TomlValueReadExt::read() return value, which does only
- // return Result<T> instead of Result<Option<T>>, which is a real inconvenience.
- //
- let no_identifier = |e: &::toml_query::error::Error| -> bool {
- is_match!(e, &::toml_query::error::Error::IdentifierNotFoundInDocument(_))
+ let filter_hidden = |todo: &FileLockEntry<'_>| -> Result<bool> {
+ Ok(todo.get_hidden()?.map(|hid| hid > now).unwrap_or(true))
};
- 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
- },
+ struct TodoViewer {
+ details: bool,
+ }
+ impl Viewer for TodoViewer {
+ fn view_entry<W>(&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("<empty description>");
+
+ 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
}
- })
- .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| {
- 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));
-
- 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
- writeln!(rt.stdout(), "{}", outstring).to_exit_code().unwrap_or_exit();
- });
-
- res.map_err_trace().ok();
+ }
+ })
+ .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<W>(&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::<crate::ui::PathProvider>()?
+ .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::<Result<Vec<_>>>()?
+ .into_iter()
+ .view::<TodoShow, _>(&mut rt.stdout())
+ .map_err(Error::from)
+}
+
+//
+// utility functions
+//
+
+fn get_datetime_arg(scmd: &ArgMatches, argname: &'static str) -> Result<Option<NaiveDateTime>> {
+ 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: AsRef<str>>(s: S) -> Result<Priority> {
+ 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<Option<NaiveDateTime>>, s: &str) -> RResult<String, libimagentryview::error::Error> {
+ 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 073508c..3299b7d 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("create-hidden")
+ .long("hidden")
+ .short("h")
+ .takes_value(true)
+ .required(false)
+ .help("Set a 'hidden' date/time")
+ )
- .arg(Arg::with_name("delete")
- .long("delete")
+ .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")
+ )
+
+ .arg(Arg::with_name("text")
+ .index(1)
+ .multiple(true)
+ .required(true)
+ .help("Text for the todo")
+ )
+ )
- .group(ArgGroup::with_name("taskwarrior hooks")
- .args(&[ "add",
- "delete",
- ])
- .required(true))
+ .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<Option<Vec<StoreId>>> {
+ 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::<Result<Vec<_>>>()
+ )
+ .transpose()
+ }
+}
+
diff --git a/doc/src/05100-lib-todo.md b/doc/src/05100-lib-todo.md
index 50eedb4..84fe851 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 = "<kairos time spec>"
-hidden = "<kairos time func (optional)>"
-due = "<kairos time func (optional)>"
-depends = [ "list of uuids" ]
-importance = "string"
-uda = {}
+status = "enum { 'deleted', 'done', 'pending' }"
+scheduled = "<NaiveDateTime>" // optional
+hidden = "<NaiveDateTime>" // optional
+due = "<NaiveDateTime>" // 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
-
-
-#### 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.
+* Creating
+* Deleting
+* Get/Retrieving
+* Getting data about the todo
+ * Reading metadata: scheduled, due, waiting, prio, uuid, status,...
+ * Related (via libimagentrylink) todo entries
-As `libimagentrylink` links are bidirectional, they do not suffice for todo
-entry dependency creation.
-As todo entries are stored with the Store IDs "todo/<uuid>", creating a
-`StoreId` from a UUID is trivial.
diff --git a/lib/domain/libimagtodo/Cargo.toml b/lib/domain/libimagtodo/Cargo.toml
index 8b2b9f3..ace432d 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 0000000..35fd9b3
--- /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 <mail@beyermatthias.de> 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<String>,
+ pub(crate) hidden: Option<String>,
+ pub(crate) due: Option<String>,
+ pub(crate) priority: Option<Priority>,
+}
+
+impl<'a> Partial<'a> for TodoHeader {
+ const LOCATION: &'static str = "todo";
+ type Output = Self;
+}
+
+pub trait Todo {
+ fn is_todo(&self) -> Result<bool>;
+ fn get_uuid(&self) -> Result<Uuid>;
+ fn get_status(&self) -> Result<Status>;
+ fn set_status(&mut self, status: Status) -> Result<()>;
+ fn get_scheduled(&self) -> Result<Option<NaiveDateTime>>;
+ fn set_scheduled(&mut self, scheduled: NaiveDateTime) -> Result<()>;
+ fn get_hidden(&self) -> Result<Option<NaiveDateTime>>;
+ fn set_hidden(&mut self, hidden: NaiveDateTime) -> Result<()>;
+ fn get_due(&self) -> Result<Option<NaiveDateTime>>;
+ fn set_due(&mut self, due: NaiveDateTime) -> Result<()>;
+ fn get_priority(&self) -> Result<Option<Priority>>;
+ 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<bool> {
+ self.is::<IsTodo>().context("Cannot check whether Entry is a todo").map_err(From::from)
+ }
+
+ fn get_uuid(&self) -> Result<Uuid> {
+ get_header(self).map(|hdr| hdr.uuid)
+ }
+
+ fn get_status(&self) -> Result<Status> {
+ 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<Option<NaiveDateTime>> {
+ 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<Option<NaiveDateTime>> {
+ 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<Option<NaiveDateTime>> {
+ 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<Option<Priority>> {
+ 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<TodoHeader> {
+ entry.get_header()
+ .read_partial::<TodoHeader>()?
+ .ok_or_else(|| {
+ format_err!("{} does not contain a TODO header", entry.get_location())
+ })
+}
+
+fn get_optional_ndt<F>(entry: &Entry, extractor: F)
+ -> Result<Option<NaiveDateTime>>
+ where F: FnOnce(TodoHeader) -> Option<String>
+{
+ 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
index 45d7a7c..6137996 100644
--- a/lib/domain/libimagtodo/src/iter.rs
+++ b/lib/domain/libimagtodo/src/iter.rs
@@ -17,35 +17,117 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
-use libimagstore::iter::Entries;
-use libimagstore::storeid::StoreId;
+use std::result::Result as RResult;
use failure::Fallible as Result;
+use failure::Error;
+use filters::failable::filter::FailableFilter;
-pub struct TaskIdIterator<'a>(Entries<'a>);
+use libimagstore::store::FileLockEntry;
+use libimagstore::store::Entry;
-impl<'a> TaskIdIterator<'a> {
+use crate::entry::Todo;
+use crate::status::Status;
+use crate::priority::Priority;
- pub fn new(inner: Entries<'a>) -> Self {
- TaskIdIterator(inner)
- }
+/// Iterator adaptor which filters an Iterator<Item = FileLockEntry> so that only todos are left
+pub struct OnlyTodos<'a>(Box<dyn Iterator<Item = FileLockEntry<'a>>>);
+impl<'a> OnlyTodos<'a> {
+ pub fn new(it: Box<dyn Iterator<Item = FileLockEntry<'a>>>) -> Self {
+ OnlyTodos(it)
+ }
}
-impl<'a> Iterator for TaskIdIterator<'a> {
- type Item = Result<StoreId>;
+impl<'a> Iterator for OnlyTodos<'a> {
+ type Item = Result<FileLockEntry<'a>>;
fn next(&mut self) -> Option<Self::Item> {
- 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
+ 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<Item = FileLockEntry> of Todos by status
+///
+pub struct StatusFilter(Status);
+
+impl FailableFilter<Entry> for StatusFilter {
+ type Error = Error;
+
+ fn filter(&self, entry: &Entry) -> RResult<bool, Self::Error> {
+ Ok(entry.get_status()? == self.0)
}
+}
+
+/// Helper filter type
+///
+/// Can be used to filter an Iterator<Item = FileLockEntry> of Todos for scheduled todos
+///
+pub struct IsScheduledFilter;
+
+impl FailableFilter<Entry> for IsScheduledFilter {
+ type Error = Error;
+
+ fn filter(&self, entry: &Entry) -> RResult<bool, Self::Error> {
+ entry.get_scheduled().map(|s| s.is_some())
+ }
+}
+
+/// Helper filter type
+///
+/// Can be used to filter an Iterator<Item = FileLockEntry> of Todos for hidden todos
+///
+pub struct IsHiddenFilter;
+impl FailableFilter<Entry> for IsHiddenFilter {
+ type Error = Error;
+
+ fn filter(&self, entry: &Entry) -> RResult<bool, Self::Error> {
+ entry.get_hidden().map(|s| s.is_some())
+ }
+}
+
+
+/// Helper filter type
+///
+/// Can be used to filter an Iterator<Item = FileLockEntry> of Todos for due todos
+///
+pub struct IsDueFilter;
+
+impl FailableFilter<Entry> for IsDueFilter {
+ type Error = Error;
+
+ fn filter(&self, entry: &Entry) -> RResult<bool, Self::Error> {
+ entry.get_due().map(|s| s.is_some())
+ }
+}
+
+
+/// Helper filter type
+///
+/// Can be used to filter an Iterator<Item = FileLockEntry> of Todos for priority
+///
+/// # Warning
+///
+/// If no priority is set for the entry, this filters out the entry
+///
+pub struct PriorityFilter(Priority);
+
+impl FailableFilter<Entry> for PriorityFilter {
+ type Error = Error;
+
+ fn filter(&self, entry: &Entry) -> RResult<bool, Self::Error> {
+ 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
index bde8b81..8901077 100644
--- a/lib/domain/libimagtodo/src/lib.rs
+++ b/lib/domain/libimagtodo/src/lib.rs
@@ -17,40 +17,28 @@
// 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 chrono;
+extern crate serde;
extern crate toml;
extern crate toml_query;
-#[macro_use] extern crate log;
-extern crate serde_json;
+extern crate uuid;
+extern crate filters;
+
+#[macro_use] extern crate serde_derive;
#[macro_use] extern crate failure;
+#[macro_use] extern crate log;
-#[macro_use] extern crate libimagstore;
extern crate libimagerror;
-extern crate task_hookrs;
-
-module_entry_path_mod!("todo");
+extern crate libimagutil;
+#[macro_use] extern crate libimagstore;
+#[macro_use] extern crate libimagentryutil;
-pub mod task;
-pub mod taskstore;
+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 0000000..e612093
--- /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 <mail@beyermatthias.de> 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<Ordering> {
+ 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 0000000..63d8fa0
--- /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 <mail@beyermatthias.de> 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<Self> {
+ 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 0000000..cc1f2aa
--- /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 <mail@beyermatthias.de> 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<NaiveDateTime>,
+ hidden: Option<NaiveDateTime>,
+ due: Option<NaiveDateTime>,
+ prio: Option<Priority>,
+ check_sanity: bool) -> Result<FileLockEntry<'a>>;
+
+ fn get_todo_by_uuid(&'a self, uuid: &Uuid) -> Result<Option<FileLockEntry<'a>>>;
+
+ fn get_todos(&self) -> Result<Entries>;
+}
+
+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<NaiveDateTime>,
+ hidden: Option<NaiveDateTime>,
+ due: Option<NaiveDateTime>,
+ prio: Option<Priority>,
+ check_sanity: bool) -> Result<FileLockEntry<'a>>
+ {
+ 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::<IsTodo>()?;
+
+ Ok(entry)
+ }
+
+ fn get_todo_by_uuid(&'a self, uuid: &Uuid) -> Result<Option<FileLockEntry<'a>>> {
+ 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<Entries> {
+ 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(())
+}
+
diff --git a/lib/domain/libimagtodo/src/task.rs b/lib/domain/libimagtodo/src/task.rs
deleted file mode 100644
index f62ef13..0000000
--- 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 <mail@beyermatthias.de> 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<Uuid>;
-}
-
-impl Task for Entry {
- fn get_uuid(&self) -> Result<Uuid> {
- 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 fbddecf..0000000
--- 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 <mail@beyermatthias.de> 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<R: BufRead>(&'a self, r: R) -> Result<(FileLockEntry<'a>, String, Uuid)>;
- fn get_task_from_import<R: BufRead>(&'a self, r: R) -> Result<RResult<FileLockEntry<'a>, String>>;
- fn get_task_from_string(&'a self, s: String) -> Result<RResult<FileLockEntry<'a>, String>>;
- fn get_task_from_uuid(&'a self, uuid: Uuid) -> Result<Option<FileLockEntry<'a>>>;
- fn retrieve_task_from_import<R: BufRead>(&'a self, r: R) -> Result<FileLockEntry<'a>>;
- fn retrieve_task_from_string(&'a self, s: String) -> Result<FileLockEntry<'a>>;
- fn delete_tasks_by_imports<R: BufRead>(&self, r: R) -> Result<()>;
- fn delete_task_by_uuid(&self, uuid: Uuid) -> Result<()>;
- fn all_tasks(&self) -> Result<TaskIdIterator>;
- fn new_from_twtask(&'a self, task: TTask) -> Result<FileLockEntry<'a>>;
-}
-
-impl<'a> TaskStore<'a> for Store {
-
- fn import_task_from_reader<R: BufRead>(&'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<R: BufRead>(&'a self, mut r: R) -> Result<RResult<FileLockEntry<'a>, 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<RResult<FileLockEntry<'a>, 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<Option<FileLockEntry<'a>>> {
- 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<R: BufRead>(&'a self, mut r: R) -> Result<FileLockEntry<'a>> {
- 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<FileLockEntry<'a>> {
- 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<R: BufRead>(&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<TaskIdIterator> {
- self.entries().map(TaskIdIterator::new)
- }
-
- fn new_from_twtask(&'a self, task: TTask) -> Result<FileLockEntry<'a>> {
- 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)
- })
- })
-
- }
-
-}
-