#![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;
#[macro_use] extern crate log;
extern crate toml;
extern crate toml_query;
extern crate kairos;
extern crate resiter;
extern crate chrono;
extern crate prettytable;
#[macro_use] extern crate failure;
extern crate result_inspect;
extern crate libimaghabit;
extern crate libimagstore;
extern crate libimagrt;
extern crate libimagerror;
extern crate libimagutil;
extern crate libimaginteraction;
use std::io::Write;
use prettytable::Table;
use prettytable::Cell;
use prettytable::Row;
use failure::Error;
use failure::Fallible as Result;
use failure::err_msg;
use resiter::AndThen;
use resiter::FilterMap;
use resiter::Filter;
use resiter::IterInnerOkOrElse;
use clap::App;
use chrono::NaiveDate;
use result_inspect::*;
use libimagrt::runtime::Runtime;
use libimagrt::application::ImagApplication;
use libimaghabit::store::HabitStore;
use libimaghabit::habit::builder::HabitBuilder;
use libimaghabit::habit::HabitTemplate;
use libimagstore::store::FileLockEntry;
use libimagstore::iter::get::StoreIdGetIteratorExtension;
use libimaginteraction::ask::ask_bool;
mod ui;
pub enum ImagHabit {}
impl ImagApplication for ImagHabit {
fn run(rt: Runtime) -> Result<()> {
match rt.cli().subcommand_name().ok_or_else(|| err_msg("No subcommand called"))? {
"create" => create(&rt),
"delete" => delete(&rt),
"list" => list(&rt),
"today" => today(&rt, false),
"status" => today(&rt, true),
"show" => show(&rt),
"done" => done(&rt),
other => {
debug!("Unknown command");
if rt.handle_unknown_subcommand("imag-contact", 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 {
"Habit tracking tool"
}
fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
}
fn create(rt: &Runtime) -> Result<()> {
use kairos::parser::parse as kairos_parse;
use kairos::parser::Parsed;
let scmd = rt.cli().subcommand_matches("create").unwrap();
let name = scmd.value_of("create-name").map(String::from).unwrap();
let recu = scmd.value_of("create-date-recurr-spec").map(String::from).unwrap();
let comm = scmd.value_of("create-comment").map(String::from).unwrap();
let date = scmd.value_of("create-date").unwrap();
let parsedate = |d, pname| match kairos_parse(d)? {
Parsed::TimeType(tt) => tt.calculate()
.inspect(|y| debug!("TimeType yielded: '{:?}'", y))?
.get_moment()
.ok_or_else(|| {
format_err!("Error: '{}' parameter does not yield a point in time", pname)
})
.map(|p| p.date()),
_ => {
Err(format_err!("Error: '{}' parameter does not yield a point in time", pname))
},
};
debug!("Building habit: name = {name}, basedate = {date}, recurr = {recu}, comment = {comm}",
name = name,
date = date,
recu = recu,
comm = comm);
let hb = HabitBuilder::default()
.with_name(name)
.with_basedate(parsedate(date, "date")?)
.with_recurspec(recu)
.with_comment(comm);
let hb = if let Some(until) = scmd.value_of("create-until") {
hb.with_until(parsedate(until, "until")?)
} else {
hb
};
debug!("Builder = {:?}", hb);
let fle = hb.build(rt.store())?;
rt.report_touched(fle.get_location()).map_err(Error::from)
}
fn delete(rt: &Runtime) -> Result<()> {
use libimaghabit::instance::HabitInstance;
let scmd = rt.cli().subcommand_matches("delete").unwrap();
let name = scmd.value_of("delete-name").map(String::from).unwrap();
let yes = scmd.is_present("delete-yes");
let delete_instances = scmd.is_present("delete-instances");
let mut input = rt.stdin().ok_or_else(|| err_msg("No input stream. Cannot ask for permission"))?;
let mut output = rt.stdout();
rt.store()
.all_habit_templates()?
.and_then_ok(|sid| rt.store().get(sid.clone()).map(|e| e.map(|e| (sid, e))))
.map_inner_ok_or_else(|| err_msg("Did not find one entry"))
.and_then_ok(|(sid, h)| {
let filter_result = h.habit_name()? == name;
Ok((filter_result, sid, h))
})
.and_then_ok(|(filter, id, fle)| {
if !filter {
return Ok(())
}
if delete_instances {
let t_name = fle.habit_name()?;
assert_eq!(t_name, name);
fle.linked_instances()?
.and_then_ok(|instance| {
let instance = rt.store().get(instance.clone())?.ok_or_else(|| {
format_err!("Failed to find instance: {}", instance)
})?;
if instance.get_template_name()? == t_name {
if !yes {
let q = format!("Really delete {}", id);
if ask_bool(&q, Some(false), &mut input, &mut output)? {
rt.store().delete(id.clone())
} else {
Ok(())
}
} else {
rt.store().delete(id.clone())
}
} else {
Ok(())
}
})
.collect::<Result<Vec<_>>>()
.map(|_| ())?;
}
drop(fle);
let do_delete_template = |sid| rt.store().delete(sid);
if !yes {
let q = format!("Really delete template {}", id);
if ask_bool(&q, Some(false), &mut input, &mut output)? {
do_delete_template(id)
} else {
Ok(())
}
} else {
do_delete_template(id)
}
})
.collect::<Result<Vec<_>>>()
.map(|_| ())
}
fn today(rt: &Runtime, future: bool) -> Result<()> {
use failure::ResultExt;
let (future, show_done) = {
if !future {
let scmd = rt.cli().subcommand_matches("today").unwrap();
let futu = scmd.is_present("today-show-future");
let done = scmd.is_present("today-done");
(futu, done)
} else if let Some(status) = rt.cli().subcommand_matches("status") {
(true, status.is_present("status-done"))
} else {
(true, false)
}
};
let today = ::chrono::offset::Local::today().naive_local();
let relevant : Vec<_> = {
let mut relevant = rt
.store()
.all_habit_templates()?
.into_get_iter(rt.store())
.map_inner_ok_or_else(|| err_msg("Did not find one entry"))
.and_then_ok(|h| {
let due = h.next_instance_date()?;
debug!("Checking {due:?} == {today:?} or (future = {fut} && {due:?} > {today:?}",
due = due, today = today, fut = future);
let take = due.map(|d| d == today || (future && d > today)).unwrap_or(false);
Ok((take, h))
})
.filter_ok(|tpl| tpl.0)
.and_then_ok(|tpl| tpl.1.next_instance_date().map(|d| d.map(|d| (d, tpl.1))))
.filter_map(|e| e.transpose())
.collect::<Result<Vec<(NaiveDate, FileLockEntry)>>>()?;
relevant.sort_by_key(|t| t.0);
relevant
};
debug!("relevant = {:?}", relevant);
let any_today_relevant = show_done || !relevant
.iter()
.map(|tpl| tpl.1.next_instance_date())
.filter_map(|e| e.transpose())
.filter_ok(|due| *due == today)
.collect::<Result<Vec<_>>>()?
.is_empty();
debug!("Any today relevant = {}", any_today_relevant);
if !any_today_relevant {
let n = rt
.cli()
.subcommand_matches("today")
.and_then(|am| {
am.value_of("today-show-next-n")
.map(|x| {
x.parse::<usize>()
.context(format_err!("Cannot parse String '{}' to integer", x))
.map_err(Error::from)
})
}).unwrap_or(Ok(5))?;
info!("No Habits due today.");
info!("Upcoming:");
relevant.iter()
.take(n)
.map(|(_, element)| {
let date = element.next_instance_date()?;
let name = element.habit_name()?;
if let Some(date) = date {
let is_done = element.instance_exists_for_date(date)?;
if show_done || !is_done {
info!(" * {date}: {name}", date = date, name = name);
}
}
Ok(())
})
.collect::<Result<Vec<_>>>()
.map(|_| ())
} else {
fn lister_fn(h: &FileLockEntry) -> Result<Vec<String>> {
debug!("Listing: {:?}", h);
let name = h.habit_name()?;
let basedate = h.habit_basedate()?;
let recur = h.habit_recur_spec()?;
let due = h.next_instance_date()?
.map(libimagutil::date::date_to_string)
.unwrap_or_else(|| String::from("<finished>"));
let comm = h.habit_comment()?;
let v = vec![name, basedate, recur, due, comm];
debug!(" -> {:?}", v);
Ok(v)
}
let header = ["#", "Name", "Basedate", "Recurr", "Next Due", "Comment"]
.iter()
.map(|s| Cell::new(s))
.collect::<Vec<Cell>>();
let mut table = Table::new();
table.set_titles(Row::new(header));
let mut empty = true;
let mut i = 0;
relevant
.into_iter()
.filter(|tpl| show_done || {
let instance_exists = tpl.1
.next_instance_date()
.and_then(|date| {
match date {
None => Ok(false),
Some(d) => {
let instance_exists = tpl.1.instance_exists_for_date(d)?;
debug!("instance exists for {:?} for {:?} = {:?}",
tpl.1.get_location().local_display_string(),
date,
instance_exists);
Ok(instance_exists)
}
}
})
.unwrap_or(false);
!instance_exists
})
.map(|(_, e)| {
let mut v = vec![format!("{}", i)];
let mut list : Vec<String> = lister_fn(&e)?;
rt.report_touched(e.get_location())?;
v.append(&mut list);
table.add_row(v.iter().map(|s| Cell::new(s)).collect());
empty = false;
i += 1;
Ok(())
})
.collect::<Result<Vec<_>>>()?;
if !empty {
let _ = table.print(&mut rt.stdout())?;
}
Ok(())
}
}
fn list(rt: &Runtime) -> Result<()> {
fn lister_fn(h: &FileLockEntry) -> Result<Vec<String>> {
debug!("Listing: {:?}", h);
let name = h.habit_name()?;
let basedate = h.habit_basedate()?;
let recur = h.habit_recur_spec()?;
let comm = h.habit_comment()?;
let (due, done) = if let Some(date) = h.next_instance_date()? {
let done = h.instance_exists_for_date(date)
.map(|b| if b { "x" } else { "" })
.map(String::from)?;
(libimagutil::date::date_to_string(date), done)
} else {
(String::from("<finished>"), String::from(""))
};
let v = vec![name, basedate, recur, comm, due, done];
debug!(" -> {:?}", v);
Ok(v)
}
let header = ["#", "Name", "Basedate", "Recurr", "Comment", "Next Due", "Done"]
.iter()
.map(|s| Cell::new(s))
.collect::<Vec<Cell>>();
let mut empty = true;
let mut table = Table::new();
let mut i = 0;
table.set_titles(Row::new(header));
rt
.store()
.all_habit_templates()?
.filter_map_ok(|id| match rt.store().get(id.clone()) {
Ok(Some(h)) => Some(Ok(h)),
Ok(None) => Some(Err(format_err!("No habit found for {:?}", id))),
Err(e) => Some(Err(e)),
})
.and_then_ok(|r| r)
.and_then_ok(|e: FileLockEntry| {
let mut v = vec![format!("{}", i)];
let mut list : Vec<String> = lister_fn(&e)?;
rt.report_touched(e.get_location())?;
v.append(&mut list);
table.add_row(v.iter().map(|s| Cell::new(s)).collect());
empty = false;
i += 1;
Ok(())
})
.collect::<Result<Vec<_>>>()?;
if !empty {
let _ = table.print(&mut rt.stdout())?;
}
Ok(())
}
fn show(rt: &Runtime) -> Result<()> {
let scmd = rt.cli().subcommand_matches("show").unwrap();
let name = scmd
.value_of("show-name")
.map(String::from)
.unwrap();
fn instance_lister_fn(rt: &Runtime, i: &FileLockEntry) -> Result<Vec<String>> {
use libimagutil::date::date_to_string;
use libimaghabit::instance::HabitInstance;
let date = date_to_string(i.get_date()?);
let comm = i.get_comment(rt.store())?;
Ok(vec![date, comm])
}
let header = ["#", "Date", "Comment"]
.iter()
.map(|s| Cell::new(s))
.collect::<Vec<Cell>>();
let mut table = Table::new();
table.set_titles(Row::new(header));
let mut i = 0;
rt.store()
.all_habit_templates()?
.into_get_iter(rt.store())
.map_inner_ok_or_else(|| err_msg("Did not find one habit template"))
.filter_ok(|h| h.habit_name().map(|n| name == n).unwrap_or(false))
.and_then_ok(|habit| {
let name = habit.habit_name()?;
let basedate = habit.habit_basedate()?;
let recur = habit.habit_recur_spec()?;
let comm = habit.habit_comment()?;
writeln!(rt.stdout(),
"{i} - {name}\nBase : {b},\nRecurrence: {r}\nComment : {c}\n",
i = i,
name = name,
b = basedate,
r = recur,
c = comm)?;
let mut j = 0;
let mut empty = true;
let instances = habit.linked_instances()?;
drop(habit);
instances
.into_get_iter(rt.store())
.map_inner_ok_or_else(|| err_msg("Did not find one habit template"))
.and_then_ok(|e| {
let mut v = vec![format!("{}", j)];
let mut instances = instance_lister_fn(&rt, &e)?;
rt.report_touched(e.get_location())?;
v.append(&mut instances);
table.add_row(v.iter().map(|s| Cell::new(s)).collect());
empty = false;
j += 1;
Ok(())
})
.collect::<Result<Vec<_>>>()?;
if !empty {
let _ = table.print(&mut rt.stdout()).map_err(Error::from);
}
i += 1;
Ok(())
})
.collect::<Result<Vec<_>>>()
.map(|_| ())
}
fn done(rt: &Runtime) -> Result<()> {
let scmd = rt.cli().subcommand_matches("done").unwrap();
let names : Vec<_> = scmd.values_of("done-name").unwrap().map(String::from).collect();
let today = ::chrono::offset::Local::today().naive_local();
let relevant : Vec<_> = {
let mut relevant : Vec<_> = rt
.store()
.all_habit_templates()?
.into_get_iter(rt.store())
.map_inner_ok_or_else(|| err_msg("Did not find one entry"))
.and_then_ok(|h| {
let due = h.next_instance_date()?;
let take = due.map(|d| d <= today || scmd.is_present("allow-future")).unwrap_or(false);
Ok((take, h))
})
.filter_ok(|tpl| tpl.0)
.and_then_ok(|tpl| Ok((names.contains(&tpl.1.habit_name()?), tpl.1)))
.filter_ok(|tpl| tpl.0)
.and_then_ok(|tpl| Ok((tpl.1.next_instance_date()?, tpl.1)))
.collect::<Result<Vec<(_, _)>>>()?;
relevant.sort_by_key(|tpl| tpl.0);
relevant
};
for tpl in relevant {
let mut r = tpl.1;
let next_instance_name = r.habit_name()?;
let next_instance_date = r.next_instance_date()?;
if let Some(next) = next_instance_date {
debug!("Creating new instance on {:?}", next);
r.create_instance_with_date(rt.store(), next)?;
info!("Done on {date}: {name}",
date = libimagutil::date::date_to_string(next),
name = next_instance_name);
} else {
info!("Ignoring: {}, because there is no due date (the habit is finised)",
next_instance_name);
}
rt.report_touched(r.get_location())?;
}
info!("Done.");
Ok(())
}