Merge branch 'libimagtodo-req-rewrite' into master
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
This commit is contained in:
commit
ead9438c41
15 changed files with 1101 additions and 461 deletions
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
authors = ["mario <mario-krehl@gmx.de>"]
|
authors = ["Matthias Beyer <mail@beyermatthias.de>"]
|
||||||
name = "imag-todo"
|
name = "imag-todo"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
|
||||||
|
@ -25,10 +25,18 @@ toml = "0.5.1"
|
||||||
toml-query = "0.9.2"
|
toml-query = "0.9.2"
|
||||||
is-match = "0.1.0"
|
is-match = "0.1.0"
|
||||||
failure = "0.1.5"
|
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" }
|
libimagrt = { version = "0.10.0", path = "../../../lib/core/libimagrt" }
|
||||||
libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" }
|
libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" }
|
||||||
libimagtodo = { version = "0.10.0", path = "../../../lib/domain/libimagtodo" }
|
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]
|
[dependencies.clap]
|
||||||
version = "2.33.0"
|
version = "2.33.0"
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
#/!usr/bin/env bash
|
|
||||||
|
|
||||||
imag todo tw-hook --add
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
#/!usr/bin/env bash
|
|
||||||
|
|
||||||
imag todo tw-hook --delete
|
|
||||||
|
|
|
@ -35,30 +35,48 @@
|
||||||
)]
|
)]
|
||||||
|
|
||||||
extern crate clap;
|
extern crate clap;
|
||||||
#[macro_use] extern crate log;
|
|
||||||
extern crate toml;
|
extern crate toml;
|
||||||
extern crate toml_query;
|
extern crate toml_query;
|
||||||
#[macro_use] extern crate is_match;
|
extern crate chrono;
|
||||||
extern crate failure;
|
extern crate filters;
|
||||||
|
extern crate kairos;
|
||||||
|
#[macro_use] extern crate log;
|
||||||
|
#[macro_use] extern crate failure;
|
||||||
|
extern crate resiter;
|
||||||
|
|
||||||
extern crate libimagrt;
|
extern crate libimagrt;
|
||||||
|
extern crate libimagstore;
|
||||||
extern crate libimagerror;
|
extern crate libimagerror;
|
||||||
|
extern crate libimagentryedit;
|
||||||
extern crate libimagtodo;
|
extern crate libimagtodo;
|
||||||
|
extern crate libimagutil;
|
||||||
|
extern crate libimagentryview;
|
||||||
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
use std::io::stdin;
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::result::Result as RResult;
|
||||||
|
|
||||||
|
use clap::ArgMatches;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
use failure::Error;
|
use failure::Error;
|
||||||
use failure::Fallible as Result;
|
use failure::Fallible as Result;
|
||||||
|
use failure::err_msg;
|
||||||
use clap::App;
|
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 libimagrt::application::ImagApplication;
|
||||||
use libimagtodo::taskstore::TaskStore;
|
use libimagrt::runtime::Runtime;
|
||||||
use libimagerror::trace::{MapErrTrace, trace_error};
|
use libimagstore::iter::get::*;
|
||||||
use libimagerror::iter::TraceIterator;
|
use libimagstore::store::Entry;
|
||||||
use libimagerror::exit::ExitUnwrap;
|
use libimagstore::store::FileLockEntry;
|
||||||
use libimagerror::io::ToExitCode;
|
use libimagtodo::entry::Todo;
|
||||||
|
use libimagtodo::priority::Priority;
|
||||||
|
use libimagtodo::status::Status;
|
||||||
|
use libimagtodo::store::TodoStore;
|
||||||
|
use libimagutil::date::datetime_to_string;
|
||||||
|
|
||||||
mod ui;
|
mod ui;
|
||||||
|
|
||||||
|
@ -70,21 +88,20 @@ pub enum ImagTodo {}
|
||||||
impl ImagApplication for ImagTodo {
|
impl ImagApplication for ImagTodo {
|
||||||
fn run(rt: Runtime) -> Result<()> {
|
fn run(rt: Runtime) -> Result<()> {
|
||||||
match rt.cli().subcommand_name() {
|
match rt.cli().subcommand_name() {
|
||||||
Some("tw-hook") => tw_hook(&rt),
|
Some("create") => create(&rt),
|
||||||
Some("list") => list(&rt),
|
Some("show") => show(&rt),
|
||||||
Some(other) => {
|
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");
|
debug!("Unknown command");
|
||||||
let _ = rt.handle_unknown_subcommand("imag-todo", other, rt.cli())
|
if rt.handle_unknown_subcommand("imag-todo", other, rt.cli())?.success() {
|
||||||
.map_err_trace_exit_unwrap()
|
Ok(())
|
||||||
.code()
|
} else {
|
||||||
.map(::std::process::exit);
|
Err(err_msg("Failed to handle unknown subcommand"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
None => {
|
} // end match scmd
|
||||||
warn!("No command");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
|
fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
|
||||||
|
@ -104,99 +121,311 @@ impl ImagApplication for ImagTodo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tw_hook(rt: &Runtime) {
|
/// A black- and whitelist for matching statuses of todo entries
|
||||||
let subcmd = rt.cli().subcommand_matches("tw-hook").unwrap();
|
///
|
||||||
if subcmd.is_present("add") {
|
/// The blacklist is checked first, followed by the whitelist.
|
||||||
let stdin = stdin();
|
/// 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>,
|
||||||
|
}
|
||||||
|
|
||||||
// implements BufRead which is required for `Store::import_task_from_reader()`
|
impl StatusMatcher {
|
||||||
let stdin = stdin.lock();
|
pub fn new() -> Self {
|
||||||
|
StatusMatcher {
|
||||||
|
is: Vec::new(),
|
||||||
|
is_not: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (_, line, uuid ) = rt
|
pub fn is(mut self, s: Status) -> Self {
|
||||||
.store()
|
self.add_is(s);
|
||||||
.import_task_from_reader(stdin)
|
self
|
||||||
.map_err_trace_exit_unwrap();
|
}
|
||||||
|
|
||||||
writeln!(rt.stdout(), "{}\nTask {} stored in imag", line, uuid)
|
pub fn add_is(&mut self, s: Status) {
|
||||||
.to_exit_code()
|
self.is.push(s);
|
||||||
.unwrap_or_exit();
|
}
|
||||||
|
|
||||||
} else if subcmd.is_present("delete") {
|
pub fn is_not(mut self, s: Status) -> Self {
|
||||||
// The used hook is "on-modify". This hook gives two json-objects
|
self.add_is_not(s);
|
||||||
// per usage und wants one (the second one) back.
|
self
|
||||||
let stdin = stdin();
|
}
|
||||||
rt.store().delete_tasks_by_imports(stdin.lock()).map_err_trace().ok();
|
|
||||||
} else {
|
pub fn add_is_not(&mut self, s: Status) {
|
||||||
// Should not be possible, as one argument is required via
|
self.is_not.push(s);
|
||||||
// ArgGroup
|
}
|
||||||
unreachable!();
|
|
||||||
|
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) {
|
fn create(rt: &Runtime) -> Result<()> {
|
||||||
use toml_query::read::TomlValueReadTypeExt;
|
debug!("Creating todo");
|
||||||
|
let scmd = rt.cli().subcommand().1.unwrap(); // safe by clap
|
||||||
|
|
||||||
let subcmd = rt.cli().subcommand_matches("list").unwrap();
|
let scheduled: Option<NaiveDateTime> = get_datetime_arg(&scmd, "create-scheduled")?;
|
||||||
let verbose = subcmd.is_present("verbose");
|
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();
|
||||||
|
|
||||||
// Helper for toml_query::read::TomlValueReadExt::read() return value, which does only
|
trace!("Creating todo with these variables:");
|
||||||
// return Result<T> instead of Result<Option<T>>, which is a real inconvenience.
|
trace!("scheduled = {:?}", scheduled);
|
||||||
//
|
trace!("hidden = {:?}", hidden);
|
||||||
let no_identifier = |e: &::toml_query::error::Error| -> bool {
|
trace!("due = {:?}", due);
|
||||||
is_match!(e, &::toml_query::error::Error::IdentifierNotFoundInDocument(_))
|
trace!("prio = {:?}", prio);
|
||||||
};
|
trace!("status = {:?}", status);
|
||||||
|
trace!("edit = {}", edit);
|
||||||
|
trace!("text = {:?}", text);
|
||||||
|
|
||||||
let res = rt.store().all_tasks() // get all tasks
|
let mut entry = rt.store().create_todo(status, scheduled, hidden, due, prio, true)?;
|
||||||
.map(|iter| { // and if this succeeded
|
debug!("Created: todo {}", entry.get_uuid()?);
|
||||||
// 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();
|
|
||||||
|
|
||||||
// compose a `task` call with them, ...
|
debug!("Setting content");
|
||||||
let outstring = if verbose { // ... if verbose
|
*entry.get_content_mut() = text.to_string();
|
||||||
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)
|
if edit {
|
||||||
.unwrap_or_else(|e| panic!("failed to execute: {}", e))
|
debug!("Editing content");
|
||||||
} else { // ... else just join them
|
entry.edit_content(&rt)?;
|
||||||
uuids.join("\n")
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// and then print that
|
rt.report_touched(entry.get_location()).map_err(Error::from)
|
||||||
writeln!(rt.stdout(), "{}", outstring).to_exit_code().unwrap_or_exit();
|
}
|
||||||
});
|
|
||||||
|
fn mark(rt: &Runtime) -> Result<()> {
|
||||||
res.map_err_trace().ok();
|
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!")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<bool> {
|
||||||
|
Ok(todo.get_hidden()?.map(|hid| hid > now).unwrap_or(true))
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,45 +17,184 @@
|
||||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
// 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> {
|
pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
|
||||||
app
|
app
|
||||||
.subcommand(SubCommand::with_name("tw-hook")
|
.subcommand(SubCommand::with_name("create")
|
||||||
.about("For use in a taskwarrior hook")
|
.about("Create task")
|
||||||
.version("0.1")
|
.version("0.1")
|
||||||
|
|
||||||
.arg(Arg::with_name("add")
|
.arg(Arg::with_name("create-scheduled")
|
||||||
.long("add")
|
.long("scheduled")
|
||||||
.short("a")
|
.short("s")
|
||||||
.takes_value(false)
|
.takes_value(true)
|
||||||
.required(false)
|
.required(false)
|
||||||
.help("For use in an on-add hook"))
|
.help("Set a 'scheduled' date/time")
|
||||||
|
)
|
||||||
|
|
||||||
.arg(Arg::with_name("delete")
|
.arg(Arg::with_name("create-hidden")
|
||||||
.long("delete")
|
.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")
|
.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)
|
.takes_value(false)
|
||||||
.required(false)
|
.required(false)
|
||||||
.help("For use in an on-delete hook"))
|
.help("Create and then edit the entry")
|
||||||
|
)
|
||||||
|
|
||||||
.group(ArgGroup::with_name("taskwarrior hooks")
|
.arg(Arg::with_name("text")
|
||||||
.args(&[ "add",
|
.index(1)
|
||||||
"delete",
|
.multiple(true)
|
||||||
])
|
.required(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")
|
.subcommand(SubCommand::with_name("list")
|
||||||
.about("List all tasks")
|
.about("List tasks (default)")
|
||||||
.version("0.1")
|
.version("0.1")
|
||||||
|
|
||||||
.arg(Arg::with_name("verbose")
|
.arg(Arg::with_name("list-table")
|
||||||
.long("verbose")
|
.long("table")
|
||||||
.short("v")
|
.short("T")
|
||||||
.takes_value(false)
|
.takes_value(false)
|
||||||
.required(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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,42 +3,43 @@
|
||||||
The library for the todo module which provides functionality to
|
The library for the todo module which provides functionality to
|
||||||
implement/implements a todomanager in imag.
|
implement/implements a todomanager in imag.
|
||||||
|
|
||||||
|
|
||||||
### Implementation details
|
### Implementation details
|
||||||
|
|
||||||
One todo entry is stored as one imag entry. The ID of the imag entry is generated by
|
One todo entry is stored as one imag entry. The ID of the imag entry is
|
||||||
appending a unique ID (UUID) to "todo/".
|
generated by appending a unique ID (UUID) to "todo/".
|
||||||
|
|
||||||
The unique ID identifies the todo entry.
|
The unique ID identifies the todo entry.
|
||||||
|
|
||||||
|
|
||||||
#### Stored data
|
#### Stored data
|
||||||
|
|
||||||
A todo entry stores the following information:
|
A todo entry stores the following information:
|
||||||
|
|
||||||
* The (UU)ID of the todo entry
|
* The (UU)ID of the todo entry
|
||||||
* A status of the todo entry. Valid values are: "deleted", "done", "pending"
|
* 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
|
* An optional "hidden" value, which specifies a date in the future where this
|
||||||
todo entry should show up. Relative (eg. "-5days"),
|
todo entry should show up.
|
||||||
to the "scheduled" date/datetime
|
* An optional "due" date/datetime
|
||||||
* An optional "due" date/datetime, relative to the "scheduled" time
|
* A "priority"-level, either "h", "m", "l"
|
||||||
* 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)
|
|
||||||
|
|
||||||
The description of the todo entry is stored as plain text.
|
The description of the todo entry is stored as plain text.
|
||||||
|
|
||||||
|
|
||||||
#### Data not stored
|
#### Data not stored
|
||||||
|
|
||||||
Some data is explicitely _not_ stored by the library because there are other
|
Some data is explicitely _not_ stored by the library because there are other
|
||||||
libraries fullfilling that purpose. These are:
|
libraries fullfilling that purpose. These are:
|
||||||
|
|
||||||
|
* Related todos, which can be done via libimagentrylink
|
||||||
* Tags, which can be done with libimagentrytag
|
* Tags, which can be done with libimagentrytag
|
||||||
* Category, which can be done with libimagentrycategory
|
* Category, which can be done with libimagentrycategory
|
||||||
* Project belonging, which can be done with libimagentrylink (by linking to a
|
* 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)
|
project file - note that "project" is a domain not yet implemented by imag)
|
||||||
* Annotations, which can be stored with libimagentryannotation
|
* Annotations, which can be stored with libimagentryannotation
|
||||||
|
|
||||||
|
|
||||||
#### Header format
|
#### Header format
|
||||||
|
|
||||||
The header partial for libimagtodo is as follows:
|
The header partial for libimagtodo is as follows:
|
||||||
|
@ -46,42 +47,24 @@ The header partial for libimagtodo is as follows:
|
||||||
```
|
```
|
||||||
[todo]
|
[todo]
|
||||||
uuid = "string"
|
uuid = "string"
|
||||||
status = "string"
|
status = "enum { 'deleted', 'done', 'pending' }"
|
||||||
scheduled = "<kairos time spec>"
|
scheduled = "<NaiveDateTime>" // optional
|
||||||
hidden = "<kairos time func (optional)>"
|
hidden = "<NaiveDateTime>" // optional
|
||||||
due = "<kairos time func (optional)>"
|
due = "<NaiveDateTime>" // optional
|
||||||
depends = [ "list of uuids" ]
|
priority = "enum { 'h', 'm', 'l' }" // optional
|
||||||
importance = "string"
|
|
||||||
uda = {}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
#### Functionality
|
#### Functionality
|
||||||
|
|
||||||
The provided functionality of this library includes, but is not limited to:
|
The provided functionality of this library includes, but is not limited to:
|
||||||
|
|
||||||
* Creating new todo entries in the store
|
* Creating
|
||||||
* Deleting todo entries from the store
|
* Deleting
|
||||||
* get/retrieving todo entries from the store
|
* Get/Retrieving
|
||||||
* Turning an entry into a todo entry
|
* Getting data about the todo
|
||||||
* Getting todo details from an entry
|
* Reading metadata: scheduled, due, waiting, prio, uuid, status,...
|
||||||
* scheduled, due, waiting date/datetime
|
* Related (via libimagentrylink) todo entries
|
||||||
* 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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
|
@ -20,13 +20,31 @@ is-it-maintained-open-issues = { repository = "matthiasbeyer/imag" }
|
||||||
maintenance = { status = "actively-developed" }
|
maintenance = { status = "actively-developed" }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
task-hookrs = "0.6.0"
|
failure = "0.1"
|
||||||
uuid = "0.7.4"
|
filters = "0.3"
|
||||||
toml = "0.5.1"
|
log = "0.4"
|
||||||
toml-query = "0.9.2"
|
serde = "1"
|
||||||
log = "0.4.6"
|
serde_derive = "1"
|
||||||
serde_json = "1.0.39"
|
serde_json = "1"
|
||||||
failure = "0.1.5"
|
toml = "0.5"
|
||||||
|
|
||||||
libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" }
|
libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" }
|
||||||
libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" }
|
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"]
|
||||||
|
|
||||||
|
|
138
lib/domain/libimagtodo/src/entry.rs
Normal file
138
lib/domain/libimagtodo/src/entry.rs
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -17,35 +17,117 @@
|
||||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
//
|
//
|
||||||
|
|
||||||
use libimagstore::iter::Entries;
|
use std::result::Result as RResult;
|
||||||
use libimagstore::storeid::StoreId;
|
|
||||||
|
|
||||||
use failure::Fallible as Result;
|
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 {
|
/// Iterator adaptor which filters an Iterator<Item = FileLockEntry> so that only todos are left
|
||||||
TaskIdIterator(inner)
|
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> {
|
impl<'a> Iterator for OnlyTodos<'a> {
|
||||||
type Item = Result<StoreId>;
|
type Item = Result<FileLockEntry<'a>>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
loop {
|
while let Some(next) = self.0.next() {
|
||||||
match self.0.next() {
|
match next.is_todo() {
|
||||||
None => return None,
|
Ok(true) => return Some(Ok(next)),
|
||||||
Some(Err(e)) => return Some(Err(e)),
|
Ok(false) => continue,
|
||||||
Some(Ok(n)) => if n.is_in_collection(&["todo", "taskwarrior"]) {
|
Err(e) => return Some(Err(e)),
|
||||||
return Some(Ok(n))
|
|
||||||
}, // else continue
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,40 +17,28 @@
|
||||||
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
//
|
//
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
|
||||||
|
|
||||||
#![recursion_limit="256"]
|
extern crate chrono;
|
||||||
|
extern crate serde;
|
||||||
#![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;
|
||||||
extern crate toml_query;
|
extern crate toml_query;
|
||||||
#[macro_use] extern crate log;
|
extern crate uuid;
|
||||||
extern crate serde_json;
|
extern crate filters;
|
||||||
#[macro_use] extern crate failure;
|
|
||||||
|
#[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 libimagerror;
|
||||||
extern crate task_hookrs;
|
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");
|
module_entry_path_mod!("todo");
|
||||||
|
|
||||||
pub mod task;
|
|
||||||
pub mod taskstore;
|
|
||||||
pub mod iter;
|
|
||||||
|
|
||||||
|
|
68
lib/domain/libimagtodo/src/priority.rs
Normal file
68
lib/domain/libimagtodo/src/priority.rs
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
72
lib/domain/libimagtodo/src/status.rs
Normal file
72
lib/domain/libimagtodo/src/status.rs
Normal file
|
@ -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());
|
||||||
|
}
|
152
lib/domain/libimagtodo/src/store.rs
Normal file
152
lib/domain/libimagtodo/src/store.rs
Normal file
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue