// // imag - the personal information management suite for the commandline // Copyright (C) 2015-2019 Matthias Beyer 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 // #![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; /// Marker enum for implementing ImagApplication on /// /// This is used by binaries crates to execute business logic /// or to build a CLI completion. 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(); // safe by call from main() let name = scmd.value_of("create-name").map(String::from).unwrap(); // safe by clap let recu = scmd.value_of("create-date-recurr-spec").map(String::from).unwrap(); // safe by clap let comm = scmd.value_of("create-comment").map(String::from).unwrap(); // safe by clap let date = scmd.value_of("create-date").unwrap(); // safe by clap 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(); // safe by call from main() let name = scmd.value_of("delete-name").map(String::from).unwrap(); // safe by clap 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)))) // get the FileLockEntry .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 { // if this does not succeed, we did something terribly wrong 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::>>() .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::>>() .map(|_| ()) } // Almost the same as `list()` but with other lister functions and an additional filter for only // listing entries which are due today. // // if `future` is false, the `rt.cli()` will be checked or a subcommand "today" and the related // future flag. If it is true, the check will not be performed and it is assumed that `--future` // was passed. 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<_> = { // scope, to have variable non-mutable in outer scope 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()?; // today or in future 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::>>()?; 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::>>()? .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::() .context(format_err!("Cannot parse String '{}' to integer", x)) .map_err(Error::from) }) }).unwrap_or(Ok(5))?; info!("No Habits due today."); info!("Upcoming:"); // list `n` which are relevant in the future. 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::>>() .map(|_| ()) } else { fn lister_fn(h: &FileLockEntry) -> Result> { 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("")); 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::>(); 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 = 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::>>()?; if !empty { let _ = table.print(&mut rt.stdout())?; } Ok(()) } } fn list(rt: &Runtime) -> Result<()> { fn lister_fn(h: &FileLockEntry) -> Result> { 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 { // "finished" as in "the habit is closed" (String::from(""), 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::>(); 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 = 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::>>()?; if !empty { let _ = table.print(&mut rt.stdout())?; } Ok(()) } fn show(rt: &Runtime) -> Result<()> { let scmd = rt.cli().subcommand_matches("show").unwrap(); // safe by call from main() let name = scmd .value_of("show-name") .map(String::from) .unwrap(); // safe by clap fn instance_lister_fn(rt: &Runtime, i: &FileLockEntry) -> Result> { 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::>(); 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; habit.linked_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::>>()?; if !empty { let _ = table.print(&mut rt.stdout()).map_err(Error::from); } i += 1; Ok(()) }) .collect::>>() .map(|_| ()) } fn done(rt: &Runtime) -> Result<()> { let scmd = rt.cli().subcommand_matches("done").unwrap(); // safe by call from main() let names : Vec<_> = scmd.values_of("done-name").unwrap().map(String::from).collect(); let today = ::chrono::offset::Local::today().naive_local(); let relevant : Vec<_> = { // scope, to have variable non-mutable in outer scope 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::>>()?; // unwrap is safe because we filtered above 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(()) }