Merge pull request #1169 from matthiasbeyer/imag-habit/init
Imag habit/init
This commit is contained in:
commit
7a12d82552
5 changed files with 680 additions and 0 deletions
|
@ -14,6 +14,7 @@ members = [
|
||||||
"bin/domain/imag-bookmark",
|
"bin/domain/imag-bookmark",
|
||||||
"bin/domain/imag-contact",
|
"bin/domain/imag-contact",
|
||||||
"bin/domain/imag-diary",
|
"bin/domain/imag-diary",
|
||||||
|
"bin/domain/imag-habit",
|
||||||
"bin/domain/imag-mail",
|
"bin/domain/imag-mail",
|
||||||
"bin/domain/imag-notes",
|
"bin/domain/imag-notes",
|
||||||
"bin/domain/imag-timetrack",
|
"bin/domain/imag-timetrack",
|
||||||
|
|
34
bin/domain/imag-habit/Cargo.toml
Normal file
34
bin/domain/imag-habit/Cargo.toml
Normal 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" }
|
||||||
|
|
489
bin/domain/imag-habit/src/main.rs
Normal file
489
bin/domain/imag-habit/src/main.rs
Normal 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)
|
||||||
|
}
|
||||||
|
|
155
bin/domain/imag-habit/src/ui.rs
Normal file
155
bin/domain/imag-habit/src/ui.rs
Normal 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."))
|
||||||
|
)
|
||||||
|
}
|
|
@ -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
|
Specifying an editor either via CLI or via the `$EDITOR` environment
|
||||||
variable still possible.
|
variable still possible.
|
||||||
* `imag-contact` was added (with basic contact support so far).
|
* `imag-contact` was added (with basic contact support so far).
|
||||||
|
* `imag-habit` was introduced
|
||||||
|
|
||||||
* Minor changes
|
* Minor changes
|
||||||
* `libimagentryannotation` got a rewrite, is not based on `libimagnotes`
|
* `libimagentryannotation` got a rewrite, is not based on `libimagnotes`
|
||||||
|
|
Loading…
Reference in a new issue