Merge pull request #1169 from matthiasbeyer/imag-habit/init

Imag habit/init
This commit is contained in:
Matthias Beyer 2017-12-22 16:20:09 +01:00 committed by GitHub
commit 7a12d82552
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 680 additions and 0 deletions

View file

@ -14,6 +14,7 @@ members = [
"bin/domain/imag-bookmark",
"bin/domain/imag-contact",
"bin/domain/imag-diary",
"bin/domain/imag-habit",
"bin/domain/imag-mail",
"bin/domain/imag-notes",
"bin/domain/imag-timetrack",

View file

@ -0,0 +1,34 @@
[package]
name = "imag-habit"
version = "0.5.0"
authors = ["Matthias Beyer <mail@beyermatthias.de>"]
description = "Part of the imag core distribution: imag-habit command"
keywords = ["imag", "PIM", "personal", "information", "management"]
readme = "../../../README.md"
license = "LGPL-2.1"
documentation = "https://matthiasbeyer.github.io/imag/imag_documentation/index.html"
repository = "https://github.com/matthiasbeyer/imag"
homepage = "http://imag-pim.org"
[dependencies]
chrono = "0.4"
version = "2.0"
clap = ">=2.17"
log = "0.3"
toml = "0.4"
toml-query = "^0.4"
kairos = "0.1.0-beta-2"
libimagerror = { version = "0.5.0", path = "../../../lib/core/libimagerror" }
libimagstore = { version = "0.5.0", path = "../../../lib/core/libimagstore" }
libimagrt = { version = "0.5.0", path = "../../../lib/core/libimagrt" }
libimagentryedit = { version = "0.5.0", path = "../../../lib/entry/libimagentryedit" }
libimagentrylist = { version = "0.5.0", path = "../../../lib/entry/libimagentrylist" }
libimaginteraction = { version = "0.5.0", path = "../../../lib/etc/libimaginteraction" }
libimagutil = { version = "0.5.0", path = "../../../lib/etc/libimagutil" }
libimagtimeui = { version = "0.5.0", path = "../../../lib/etc/libimagtimeui" }
libimaghabit = { version = "0.5.0", path = "../../../lib/domain/libimaghabit" }

View file

@ -0,0 +1,489 @@
//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015, 2016 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
//
#![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;
#[macro_use] extern crate version;
extern crate toml;
extern crate toml_query;
extern crate kairos;
extern crate chrono;
extern crate libimaghabit;
extern crate libimagstore;
extern crate libimagrt;
extern crate libimagerror;
extern crate libimagutil;
extern crate libimagentrylist;
extern crate libimaginteraction;
use std::process::exit;
use libimagrt::runtime::Runtime;
use libimagrt::setup::generate_runtime_setup;
use libimagerror::trace::{MapErrTrace, trace_error};
use libimaghabit::store::HabitStore;
use libimaghabit::habit::builder::HabitBuilder;
use libimaghabit::habit::HabitTemplate;
use libimagstore::store::FileLockEntry;
use libimagstore::store::Store;
use libimagstore::storeid::StoreId;
use libimagentrylist::listers::table::TableLister;
use libimagentrylist::lister::Lister;
use libimaginteraction::ask::ask_bool;
mod ui;
fn main() {
let rt = generate_runtime_setup("imag-habit",
&version!()[..],
"Habit tracking tool",
ui::build_ui);
let _ = rt
.cli()
.subcommand_name()
.map(|name| {
debug!("Call {}", name);
match name {
"create" => create(&rt),
"delete" => delete(&rt),
"list" => list(&rt),
"today" => today(&rt, false),
"status" => today(&rt, true),
"show" => show(&rt),
"done" => done(&rt),
_ => {
debug!("Unknown command"); // More error handling
exit(1)
},
}
})
.unwrap_or_else(|| today(&rt, true));
}
fn create(rt: &Runtime) {
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).map_err_trace_exit_unwrap(1) {
Parsed::TimeType(tt) => match tt.calculate() {
Ok(tt) => match tt.get_moment() {
Some(mom) => mom.date(),
None => {
debug!("TimeType yielded: '{:?}'", tt);
error!("Error: '{}' parameter does not yield a point in time", pname);
exit(1);
},
},
Err(e) => {
error!("Error: '{:?}'", e);
exit(1);
}
},
_ => {
error!("Error: '{}' parameter does not yield a point in time", pname);
exit(1);
},
};
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
};
hb.build(rt.store()).map_err_trace_exit_unwrap(1);
info!("Ok");
}
fn delete(rt: &Runtime) {
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 _ = rt
.store()
.all_habit_templates()
.map_err_trace_exit_unwrap(1)
.map(|sid| (sid.clone(), rt.store().get(sid).map_err_trace_exit_unwrap(1))) // get the FileLockEntry
.filter(|&(_, ref habit)| match habit { // filter for name of habit == name we look for
&Some(ref h) => h.habit_name().map_err_trace_exit_unwrap(1) == name,
&None => false,
})
.filter_map(|(a, o)| o.map(|x| (a, x))) // map: (a, Option<b>) -> Option<(a, b)> -> (a, b)
.map(|(sid, fle)| {
if delete_instances {
// if this does not succeed, we did something terribly wrong
let t_name = fle.habit_name().map_err_trace_exit_unwrap(1);
assert_eq!(t_name, name);
let get_instance = |iid| rt.store().get(iid).map_err_trace_exit_unwrap(1);
let has_template_name = |i: &FileLockEntry| t_name == i.get_template_name().map_err_trace_exit_unwrap(1);
let instance_location = |i: FileLockEntry| i.get_location().clone();
let delete_instance_by_id = |id| {
let do_delete = |id| rt.store().delete(id).map_err_trace_exit_unwrap(1);
if !yes {
let q = format!("Really delete {}", id);
if ask_bool(&q, Some(false)) {
let _ = do_delete(id);
}
} else {
let _ = do_delete(id);
}
};
fle
.linked_instances()
.map_err_trace_exit_unwrap(1)
.filter_map(get_instance)
.filter(has_template_name)
.map(instance_location)
.map(delete_instance_by_id)
.collect::<Vec<_>>();
}
drop(fle);
let do_delete_template = |sid| rt.store().delete(sid).map_err_trace_exit_unwrap(1);
if !yes {
let q = format!("Really delete template {}", sid);
if ask_bool(&q, Some(false)) {
let _ = do_delete_template(sid);
}
} else {
let _ = do_delete_template(sid);
}
})
.collect::<Vec<_>>();
info!("Done");
}
// 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) {
use libimaghabit::error::ResultExt;
use libimaghabit::error::HabitErrorKind as HEK;
let future = {
if !future {
rt.cli().subcommand_matches("today").unwrap().is_present("today-show-future")
} else {
true
}
};
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()
.map_err_trace_exit_unwrap(1)
.filter_map(|id| match rt.store().get(id.clone()) {
Ok(Some(h)) => Some(h),
Ok(None) => {
error!("No habit found for {:?}", id);
None
},
Err(e) => {
trace_error(&e);
None
},
})
.filter(|h| {
let due = h.next_instance_date().map_err_trace_exit_unwrap(1);
// today or in future
due.map(|d| d == today || (future && d > today)).unwrap_or(false)
})
.collect();
// unwrap is safe because we filtered above
relevant.sort_by_key(|h| h.next_instance_date().map_err_trace_exit_unwrap(1).unwrap());
relevant
};
let any_today_relevant = relevant
.iter()
.filter(|h| {
let due = h.next_instance_date().map_err_trace_exit_unwrap(1);
due.map(|d| d == today).unwrap_or(false) // relevant today
})
.count() == 0;
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>()
.chain_err(|| HEK::from(format!("Cannot parse String '{}' to integer", x)))
.map_err_trace_exit_unwrap(1)
})
}).unwrap_or(5);
info!("No Habits due today.");
info!("Upcoming:");
// list `n` which are relevant in the future.
for element in relevant.iter().take(n) {
let date = element.next_instance_date().map_err_trace_exit_unwrap(1);
let name = element.habit_name().map_err_trace_exit_unwrap(1);
if let Some(date) = date { // if there is a date
info!(" * {date}: {name}", date = date, name = name);
}
}
} else {
fn lister_fn(h: &FileLockEntry) -> Vec<String> {
debug!("Listing: {:?}", h);
let name = h.habit_name().map_err_trace_exit_unwrap(1);
let basedate = h.habit_basedate().map_err_trace_exit_unwrap(1);
let recur = h.habit_recur_spec().map_err_trace_exit_unwrap(1);
let due = h.next_instance_date().map_err_trace_exit_unwrap(1)
.map(date_to_string_helper)
.unwrap_or_else(|| String::from("<finished>"));
let comm = h.habit_comment().map_err_trace_exit_unwrap(1);
let v = vec![name, basedate, recur, due, comm];
debug!(" -> {:?}", v);
v
}
fn lister_header() -> Vec<String> {
["Name", "Basedate", "Recurr", "Next Due", "Comment"]
.iter().map(|x| String::from(*x)).collect()
}
TableLister::new(lister_fn)
.with_header(lister_header())
.with_idx(true)
.print_empty(false)
.list(relevant.into_iter())
.map_err_trace_exit_unwrap(1);
}
}
fn list(rt: &Runtime) {
fn lister_fn(h: &FileLockEntry) -> Vec<String> {
debug!("Listing: {:?}", h);
let name = h.habit_name().map_err_trace_exit_unwrap(1);
let basedate = h.habit_basedate().map_err_trace_exit_unwrap(1);
let recur = h.habit_recur_spec().map_err_trace_exit_unwrap(1);
let comm = h.habit_comment().map_err_trace_exit_unwrap(1);
let due = h.next_instance_date().map_err_trace_exit_unwrap(1)
.map(date_to_string_helper)
.unwrap_or_else(|| String::from("<finished>"));
let v = vec![name, basedate, recur, comm, due];
debug!(" -> {:?}", v);
v
}
fn lister_header() -> Vec<String> {
["Name", "Basedate", "Recurr", "Comment", "Next Due"].iter().map(|x| String::from(*x)).collect()
}
let iter = rt
.store()
.all_habit_templates()
.map_err_trace_exit_unwrap(1)
.filter_map(|id| match rt.store().get(id.clone()) {
Ok(Some(h)) => Some(h),
Ok(None) => {
error!("No habit found for {:?}", id);
None
},
Err(e) => {
trace_error(&e);
None
},
});
TableLister::new(lister_fn)
.with_header(lister_header())
.with_idx(true)
.print_empty(false)
.list(iter)
.map_err_trace_exit_unwrap(1);
}
fn show(rt: &Runtime) {
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_header() -> Vec<String> {
["Date", "Comment"].iter().map(|x| String::from(*x)).collect()
}
fn instance_lister_fn(i: &FileLockEntry) -> Vec<String> {
use libimaghabit::util::date_to_string;
use libimaghabit::instance::HabitInstance;
let date = date_to_string(&i.get_date().map_err_trace_exit_unwrap(1));
let comm = i.get_comment().map_err_trace_exit_unwrap(1);
vec![date, comm]
}
let _ = rt
.store()
.all_habit_templates()
.map_err_trace_exit_unwrap(1)
.filter_map(|id| get_from_store(rt.store(), id))
.filter(|h| h.habit_name().map(|n| name == n).map_err_trace_exit_unwrap(1))
.enumerate()
.map(|(i, habit)| {
let name = habit.habit_name().map_err_trace_exit_unwrap(1);
let basedate = habit.habit_basedate().map_err_trace_exit_unwrap(1);
let recur = habit.habit_recur_spec().map_err_trace_exit_unwrap(1);
let comm = habit.habit_comment().map_err_trace_exit_unwrap(1);
println!("{i} - {name}\nBase : {b},\nRecurrence: {r}\nComment : {c}\n",
i = i,
name = name,
b = basedate,
r = recur,
c = comm);
let instances_iter = habit
.linked_instances()
.map_err_trace_exit_unwrap(1)
.filter_map(|instance_id| {
debug!("Getting: {:?}", instance_id);
rt.store().get(instance_id).map_err_trace_exit_unwrap(1)
});
TableLister::new(instance_lister_fn)
.with_header(instance_lister_header())
.with_idx(true)
.print_empty(false)
.list(instances_iter)
.map_err_trace_exit_unwrap(1);
})
.collect::<Vec<_>>();
}
fn done(rt: &Runtime) {
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()
.map_err_trace_exit_unwrap(1)
.filter_map(|id| get_from_store(rt.store(), id))
.filter(|h| {
let due = h.next_instance_date().map_err_trace_exit_unwrap(1);
due.map(|d| (d == today || d < today) || scmd.is_present("allow-future"))
.unwrap_or(false)
})
.filter(|h| {
names.contains(&h.habit_name().map_err_trace_exit_unwrap(1))
})
.collect();
// unwrap is safe because we filtered above
relevant.sort_by_key(|h| h.next_instance_date().map_err_trace_exit_unwrap(1).unwrap());
relevant
};
for r in relevant.iter() {
let next_instance_name = r.habit_name().map_err_trace_exit_unwrap(1);
let next_instance_date = r.next_instance_date().map_err_trace_exit_unwrap(1);
if let Some(next) = next_instance_date {
debug!("Creating new instance on {:?}", next);
r.create_instance_with_date(rt.store(), &next)
.map_err_trace_exit_unwrap(1);
info!("Done on {date}: {name}",
date = libimaghabit::util::date_to_string(&next),
name = next_instance_name);
} else {
info!("Ignoring: {}, because there is no due date (the habit is finised)",
next_instance_name);
}
}
info!("Done.");
}
/// Helper function for `Iterator::filter_map()`ing `all_habit_templates()` and `Store::get` them.
fn get_from_store<'a>(store: &'a Store, id: StoreId) -> Option<FileLockEntry<'a>> {
match store.get(id.clone()) {
Ok(Some(h)) => Some(h),
Ok(None) => {
error!("No habit found for {:?}", id);
None
},
Err(e) => {
trace_error(&e);
None
},
}
}
fn date_to_string_helper(d: chrono::NaiveDate) -> String {
libimaghabit::util::date_to_string(&d)
}

View file

@ -0,0 +1,155 @@
//
// imag - the personal information management suite for the commandline
// Copyright (C) 2015, 2016 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 clap::{Arg, App, SubCommand};
pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
app
.subcommand(SubCommand::with_name("status")
.about("Show the current status. Remind of not-yet-done habits, shows upcoming. Default if no command is passed. Also alias for 'today --future'")
.version("0.1")
)
.subcommand(SubCommand::with_name("create")
.about("Create a new Habit")
.version("0.1")
.arg(Arg::with_name("create-name")
.long("name")
.short("n")
.multiple(false)
.required(true)
.takes_value(true)
.value_name("NAME")
.help("Name of the new habit"))
.arg(Arg::with_name("create-date")
.long("date")
.short("d")
.multiple(false)
.required(true)
.takes_value(true)
.value_name("DATE")
.help("Date when the first instance should be done"))
.arg(Arg::with_name("create-date-recurr-spec")
.long("recurr")
.short("r")
.multiple(false)
.required(true)
.takes_value(true)
.value_name("RECURRENCE-SPEC")
.help("Spec how the habit should recur (eg: 'weekly', 'monthly', '5days', '12hours')"))
.arg(Arg::with_name("create-until")
.long("until")
.short("u")
.multiple(false)
.required(false)
.takes_value(true)
.value_name("UNTIL")
.help("Until-Date for the habit"))
.arg(Arg::with_name("create-comment")
.long("comment")
.short("c")
.multiple(true)
.required(true)
.takes_value(true)
.value_name("COMMENT")
.help("Comment for the habit"))
)
.subcommand(SubCommand::with_name("delete")
.about("Delete a Habit (and its instances)")
.version("0.1")
.arg(Arg::with_name("delete-instances")
.long("instances")
.short("I")
.multiple(false)
.required(false)
.takes_value(false)
.help("Delete instances as well"))
.arg(Arg::with_name("delete-yes")
.long("yes")
.multiple(false)
.required(false)
.takes_value(false)
.help("Do not ask for confirmation"))
.arg(Arg::with_name("delete-name")
.index(1)
.multiple(false)
.required(true)
.takes_value(true)
.value_name("NAME")
.help("Name of the habit"))
)
.subcommand(SubCommand::with_name("list")
.about("List Habits")
.version("0.1")
.arg(Arg::with_name("list-long")
.long("long")
.short("l")
.multiple(false)
.required(false)
.takes_value(false)
.help("List with details (how many instances)"))
)
.subcommand(SubCommand::with_name("show")
.about("Show a Habit and its instances")
.version("0.1")
.arg(Arg::with_name("show-name")
.index(1)
.multiple(false)
.required(true)
.takes_value(true)
.value_name("NAME")
.help("Name of the habit to show"))
)
.subcommand(SubCommand::with_name("today")
.about("List habits which are due today (default command)")
.version("0.1")
.arg(Arg::with_name("today-show-future")
.long("future")
.short("f")
.multiple(false)
.required(false)
.takes_value(false)
.help("Also show the future"))
.arg(Arg::with_name("today-show-next-n")
.long("show")
.short("s")
.multiple(false)
.required(false)
.takes_value(true)
.value_name("N")
.help("Show the N next relevant entries. Default = 5"))
)
.subcommand(SubCommand::with_name("done")
.about("Mark one or more habits (which are pending) as done")
.version("0.1")
.arg(Arg::with_name("done-name")
.index(1)
.multiple(true)
.required(true)
.takes_value(true)
.value_name("NAME")
.help("The names of the habits to be marked as done."))
)
}

View file

@ -34,6 +34,7 @@ This section contains the changelog from the last release to the next release.
Specifying an editor either via CLI or via the `$EDITOR` environment
variable still possible.
* `imag-contact` was added (with basic contact support so far).
* `imag-habit` was introduced
* Minor changes
* `libimagentryannotation` got a rewrite, is not based on `libimagnotes`