summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2017-12-22 16:20:09 +0100
committerGitHub <noreply@github.com>2017-12-22 16:20:09 +0100
commit7a12d82552672707827488d456ffdb23ce537b4d (patch)
tree18e31610669c0bd35ce236fa5e936c5643ef20e2
parent840bc86c901d96ca110f8610818884cdb725ffe3 (diff)
parent05a630f943aa92d4a830c2fbbb17517e2990811e (diff)
downloadimag-7a12d82552672707827488d456ffdb23ce537b4d.zip
imag-7a12d82552672707827488d456ffdb23ce537b4d.tar.gz
Merge pull request #1169 from matthiasbeyer/imag-habit/init
Imag habit/init
-rw-r--r--Cargo.toml1
-rw-r--r--bin/domain/imag-habit/Cargo.toml34
-rw-r--r--bin/domain/imag-habit/src/main.rs489
-rw-r--r--bin/domain/imag-habit/src/ui.rs155
-rw-r--r--doc/src/09020-changelog.md1
5 files changed, 680 insertions, 0 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 0340033..c8a2b9f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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",
diff --git a/bin/domain/imag-habit/Cargo.toml b/bin/domain/imag-habit/Cargo.toml
new file mode 100644
index 0000000..1fd2811
--- /dev/null
+++ b/bin/domain/imag-habit/Cargo.toml
@@ -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" }
+
diff --git a/bin/domain/imag-habit/src/main.rs b/bin/domain/imag-habit/src/main.rs
new file mode 100644
index 0000000..8de4753
--- /dev/null
+++ b/bin/domain/imag-habit/src/main.rs
@@ -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)
+}
+
diff --git a/bin/domain/imag-habit/src/ui.rs b/bin/domain/imag-habit/src/ui.rs
new file mode 100644
index 0000000..d514868
--- /dev/null
+++ b/bin/domain/imag-habit/src/ui.rs
@@ -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."))
+ )
+}
diff --git a/doc/src/09020-changelog.md b/doc/src/09020-changelog.md
index a0004df..bb3246a 100644
--- a/doc/src/09020-changelog.md
+++ b/doc/src/09020-changelog.md
@@ -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`