#![forbid(unsafe_code)]
#![deny(
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 clap;
extern crate toml;
extern crate toml_query;
extern crate chrono;
extern crate filters;
extern crate kairos;
#[macro_use] extern crate log;
#[macro_use] extern crate failure;
extern crate resiter;
extern crate handlebars;
extern crate prettytable;
#[cfg(feature = "import-taskwarrior")]
extern crate task_hookrs;
#[cfg(feature = "import-taskwarrior")]
extern crate uuid;
#[cfg(feature = "import-taskwarrior")]
extern crate libimagentrytag;
#[cfg(feature = "import-taskwarrior")]
extern crate libimagentrylink;
extern crate libimagrt;
extern crate libimagstore;
extern crate libimagerror;
extern crate libimagentryedit;
extern crate libimagtodo;
extern crate libimagutil;
extern crate libimagentryview;
extern crate libimaginteraction;
use std::ops::Deref;
use std::io::Write;
use std::result::Result as RResult;
use std::str::FromStr;
use clap::ArgMatches;
use chrono::NaiveDateTime;
use failure::Error;
use failure::Fallible as Result;
use failure::err_msg;
use clap::App;
use resiter::AndThen;
use resiter::IterInnerOkOrElse;
use prettytable::Table;
use prettytable::Cell;
use prettytable::Row;
use libimagentryedit::edit::Edit;
use libimagentryview::viewer::Viewer;
use libimagrt::application::ImagApplication;
use libimagrt::runtime::Runtime;
use libimagstore::iter::get::*;
use libimagstore::store::Entry;
use libimagstore::store::FileLockEntry;
use libimagtodo::entry::Todo;
use libimagtodo::priority::Priority;
use libimagtodo::status::Status;
use libimagtodo::store::TodoStore;
mod ui;
mod import;
mod util;
pub enum ImagTodo {}
impl ImagApplication for ImagTodo {
fn run(rt: Runtime) -> Result<()> {
match rt.cli().subcommand_name() {
Some("create") => create(&rt),
Some("show") => show(&rt),
Some("mark") => mark(&rt),
Some("pending") | None => list_todos(&rt, &StatusMatcher::new().is(Status::Pending), false),
Some("list") => list(&rt),
Some("import") => import::import(&rt),
Some(other) => {
debug!("Unknown command");
if rt.handle_unknown_subcommand("imag-todo", other, rt.cli())?.success() {
Ok(())
} else {
Err(err_msg("Failed to handle unknown subcommand"))
}
}
}
}
fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
ui::build_ui(app)
}
fn name() -> &'static str {
env!("CARGO_PKG_NAME")
}
fn description() -> &'static str {
"Interface with taskwarrior"
}
fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
}
#[derive(Debug, Default)]
pub struct StatusMatcher {
is: Vec<Status>,
is_not: Vec<Status>,
}
impl StatusMatcher {
pub fn new() -> Self {
StatusMatcher { ..Default::default() }
}
pub fn is(mut self, s: Status) -> Self {
self.add_is(s);
self
}
pub fn add_is(&mut self, s: Status) {
self.is.push(s);
}
#[allow(clippy::wrong_self_convention)]
pub fn is_not(mut self, s: Status) -> Self {
self.add_is_not(s);
self
}
pub fn add_is_not(&mut self, s: Status) {
self.is_not.push(s);
}
pub fn matches(&self, todo: Status) -> bool {
if self.is_not.iter().any(|t| *t == todo) {
false
} else {
self.is.is_empty() || self.is.iter().any(|t| *t == todo)
}
}
}
fn create(rt: &Runtime) -> Result<()> {
debug!("Creating todo");
let scmd = rt.cli().subcommand().1.unwrap();
let scheduled: Option<NaiveDateTime> = get_datetime_arg(&scmd, "create-scheduled")?;
let hidden: Option<NaiveDateTime> = get_datetime_arg(&scmd, "create-hidden")?;
let due: Option<NaiveDateTime> = get_datetime_arg(&scmd, "create-due")?;
let prio: Option<Priority> = scmd.value_of("create-prio").map(prio_from_str).transpose()?;
let status: Status = scmd.value_of("create-status").map(Status::from_str).unwrap()?;
let edit = scmd.is_present("create-edit");
let text = scmd.value_of("text").unwrap();
trace!("Creating todo with these variables:");
trace!("scheduled = {:?}", scheduled);
trace!("hidden = {:?}", hidden);
trace!("due = {:?}", due);
trace!("prio = {:?}", prio);
trace!("status = {:?}", status);
trace!("edit = {}", edit);
trace!("text = {:?}", text);
let mut entry = rt.store().create_todo(status, scheduled, hidden, due, prio, true)?;
debug!("Created: todo {}", entry.get_uuid()?);
debug!("Setting content");
*entry.get_content_mut() = text.to_string();
if edit {
debug!("Editing content");
entry.edit_content(&rt)?;
}
rt.report_touched(entry.get_location())
}
fn mark(rt: &Runtime) -> Result<()> {
fn mark_todos_as(rt: &Runtime, status: Status) -> Result<()> {
rt.ids::<crate::ui::PathProvider>()?
.ok_or_else(|| err_msg("No ids supplied"))?
.into_iter()
.map(Ok)
.into_get_iter(rt.store())
.map_inner_ok_or_else(|| err_msg("Did not find one entry"))
.and_then_ok(|e| rt.report_touched(e.get_location()).map(|_| 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("deleted") => mark_todos_as(rt, Status::Deleted),
Some("pending") => mark_todos_as(rt, Status::Pending),
Some(other) => Err(format_err!("Unknown mark type selected: {}", other)),
None => Err(format_err!("No mark type selected, doing nothing!")),
}
}
fn list_todos(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool) -> Result<()> {
use filters::failable::filter::FailableFilter;
debug!("Listing todos with status filter {:?}", matcher);
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 = util::get_dt_str(entry.get_scheduled(), "Not scheduled")?;
let hidden = util::get_dt_str(entry.get_hidden(), "Not hidden")?;
let due = util::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_else(|| "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)
}
}
fn process<'a, I>(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool, iter: I) -> Result<()>
where I: Iterator<Item = Result<FileLockEntry<'a>>> + Sized
{
let viewer = TodoViewer { details: false };
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))
};
iter
.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 !rt.output_is_pipe() && (show_hidden || filter_hidden.filter(&entry)?) {
if let Err(e) = viewer.view_entry(&entry, &mut rt.stdout()) {
use libimagentryview::error::Error;
match e {
Error::Other(e) => return Err(e),
Error::Io(e) => if e.kind() != std::io::ErrorKind::BrokenPipe {
return Err(failure::Error::from(e))
},
}
}
}
rt.report_touched(entry.get_location())
})
.collect()
};
if rt.ids_from_stdin() {
let iter = 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"));
process(&rt, matcher, show_hidden, iter)
} else {
let iter = rt.store().get_todos()?
.into_get_iter()
.map_inner_ok_or_else(|| err_msg("Did not find one entry"));
process(&rt, matcher, show_hidden, iter)
}
}
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); }
list_todos(rt, &matcher, hidden)
}
fn show(rt: &Runtime) -> Result<()> {
let scmd = rt.cli().subcommand_matches("show").unwrap();
let show_format = util::get_todo_print_format("todo.show_format", rt, &scmd)?;
let out = rt.stdout();
let mut outlock = out.lock();
fn show_with_table<'a, I>(rt: &Runtime, iter: I) -> Result<()>
where I: Iterator<Item = FileLockEntry<'a>>
{
const HEADER: &[&str] = &[
"uuid",
"status",
"sched",
"hidden",
"due",
"priority",
"text",
];
let mut table = {
let mut t = Table::new();
let header = HEADER.iter().map(|s| Cell::new(s)).collect::<Vec<Cell>>();
t.set_titles(Row::from(header));
t
};
iter.map(|entry| {
use libimagentryview::error::Error as E;
let uuid = entry.get_uuid().map_err(E::from)?.to_hyphenated().to_string();
let status = entry.get_status().map_err(E::from)?;
let status = status.as_str().to_string();
let sched = util::get_dt_str(entry.get_scheduled(), "Not scheduled")?;
let hidden = util::get_dt_str(entry.get_hidden(), "Not hidden")?;
let due = util::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_else(|| "No prio".to_string());
let text = entry.get_content().to_owned();
let v = [
uuid,
status,
sched,
hidden,
due,
priority,
text,
];
table.add_row(v.iter().map(|s| Cell::new(s)).collect());
Ok(entry)
})
.and_then_ok(|e| rt.report_touched(e.get_location()).map_err(Error::from).map(|_| e))
.collect::<Result<Vec<_>>>()?;
table.print(&mut rt.stdout())
.map(|_| ())
.map_err(Error::from)
}
let iter = 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(|_| e))
.collect::<Result<Vec<_>>>()?
.into_iter();
if scmd.is_present("show-no-table") {
iter.enumerate()
.map(|(i, elem)| {
let data = util::build_data_object_for_handlebars(i, elem.deref())?;
let s = show_format.render("format", &data)?;
writeln!(outlock, "{}", s).map_err(Error::from)
})
.collect()
} else {
show_with_table(rt, iter)
}
}
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)),
}
}