Merge branch 'libimagtodo-req-rewrite' into master

Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
This commit is contained in:
Matthias Beyer 2019-11-09 18:38:12 +01:00
commit ead9438c41
15 changed files with 1101 additions and 461 deletions

View File

@ -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"

View File

@ -1,4 +0,0 @@
#/!usr/bin/env bash
imag todo tw-hook --add

View File

@ -1,4 +0,0 @@
#/!usr/bin/env bash
imag todo tw-hook --delete

View File

@ -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()))
} }

View File

@ -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()
}
}

View File

@ -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.

View File

@ -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"]

View 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)
}

View File

@ -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))
}
} }

View File

@ -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;

View 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
}
}

View 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());
}

View 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(())
}

View File

@ -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)
})
}
}

View File

@ -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)
})
})
}
}