summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml4
-rw-r--r--Cargo.toml1
-rw-r--r--bin/domain/imag-contact/Cargo.toml1
-rw-r--r--bin/domain/imag-contact/src/create.rs270
-rw-r--r--bin/domain/imag-contact/src/edit.rs64
-rw-r--r--bin/domain/imag-contact/src/lib.rs320
-rw-r--r--bin/domain/imag-contact/src/util.rs41
-rw-r--r--bin/domain/imag-mail/Cargo.toml1
-rw-r--r--bin/domain/imag-mail/src/lib.rs133
-rw-r--r--bin/domain/imag-notes/Cargo.toml1
-rw-r--r--bin/domain/imag-notes/src/lib.rs118
-rw-r--r--bin/domain/imag-todo/Cargo.toml3
-rw-r--r--bin/domain/imag-todo/src/lib.rs214
-rw-r--r--bin/domain/imag-todo/src/ui.rs22
-rw-r--r--bin/domain/imag-todo/src/util.rs87
-rw-r--r--doc/src/01000-intro.md79
-rw-r--r--doc/src/01001-intro-problem.md21
-rw-r--r--doc/src/01002-intro-approach.md15
-rw-r--r--doc/src/01003-intro-implementation.md13
-rw-r--r--doc/src/01004-intro-alternatives.md26
-rw-r--r--doc/src/01010-architecture.md95
-rw-r--r--doc/src/02000-store.md31
-rw-r--r--doc/src/03010-conventions.md4
-rw-r--r--doc/src/09010-contributing.md2
-rw-r--r--doc/src/09020-changelog.md7
-rw-r--r--imagrc.toml13
-rw-r--r--tests/ui/src/imag_view.rs39
-rw-r--r--tests/version-sync/Cargo.toml18
-rw-r--r--tests/version-sync/src/lib.rs116
29 files changed, 995 insertions, 764 deletions
diff --git a/.travis.yml b/.travis.yml
index f0213c2b..763c55c2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -28,7 +28,7 @@ matrix:
script:
- cd doc/user && mdbook build || exit 1
- language: rust
- rust: 1.36.0
+ rust: 1.37.0
cache:
directories:
- /home/travis/.cargo
@@ -38,7 +38,7 @@ matrix:
- cargo build --all --all-features -j 1 || exit 1
- cargo test --all --all-features -j 1 || exit 1
- language: rust
- rust: 1.37.0
+ rust: 1.38.0
cache:
directories:
- /home/travis/.cargo
diff --git a/Cargo.toml b/Cargo.toml
index c0d00e03..f50f39a1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -63,4 +63,5 @@ members = [
"lib/etc/libimagutil",
"tests/ui",
+ "tests/version-sync",
]
diff --git a/bin/domain/imag-contact/Cargo.toml b/bin/domain/imag-contact/Cargo.toml
index 6260fbb8..da559479 100644
--- a/bin/domain/imag-contact/Cargo.toml
+++ b/bin/domain/imag-contact/Cargo.toml
@@ -28,6 +28,7 @@ walkdir = "2.2.8"
uuid = { version = "0.7.4", features = ["v4"] }
serde_json = "1.0.39"
failure = "0.1.5"
+resiter = "0.4"
libimagrt = { version = "0.10.0", path = "../../../lib/core/libimagrt" }
libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" }
diff --git a/bin/domain/imag-contact/src/create.rs b/bin/domain/imag-contact/src/create.rs
index bcd87730..b75a167e 100644
--- a/bin/domain/imag-contact/src/create.rs
+++ b/bin/domain/imag-contact/src/create.rs
@@ -33,7 +33,6 @@
)]
use std::collections::BTreeMap;
-use std::process::exit;
use std::io::Read;
use std::io::Write;
use std::path::PathBuf;
@@ -48,12 +47,11 @@ use toml::Value;
use uuid::Uuid;
use failure::Error;
use failure::err_msg;
+use failure::Fallible as Result;
+use failure::ResultExt;
use libimagcontact::store::ContactStore;
use libimagrt::runtime::Runtime;
-use libimagerror::trace::MapErrTrace;
-use libimagerror::trace::trace_error;
-use libimagerror::exit::ExitUnwrap;
use libimagutil::warn_result::WarnResult;
const TEMPLATE : &str = include_str!("../static/new-contact-template.toml");
@@ -76,32 +74,26 @@ mod test {
}
}
-fn ask_continue(inputstream: &mut dyn Read, outputstream: &mut dyn Write) -> bool {
+fn ask_continue(inputstream: &mut dyn Read, outputstream: &mut dyn Write) -> Result<bool> {
::libimaginteraction::ask::ask_bool("Edit tempfile", Some(true), inputstream, outputstream)
- .map_err_trace_exit_unwrap()
}
-pub fn create(rt: &Runtime) {
+pub fn create(rt: &Runtime) -> Result<()> {
let scmd = rt.cli().subcommand_matches("create").unwrap();
let mut template = String::from(TEMPLATE);
let collection_name = rt.cli().value_of("ref-collection-name").unwrap_or("contacts");
let collection_name = String::from(collection_name);
let ref_config = rt // TODO: Re-Deserialize to libimagentryref::reference::Config
.config()
- .ok_or_else(|| err_msg("Configuration missing, cannot continue!"))
- .map_err_trace_exit_unwrap()
- .read_partial::<libimagentryref::reference::Config>()
- .map_err(Error::from)
- .map_err_trace_exit_unwrap()
- .ok_or_else(|| format_err!("Configuration missing: {}", libimagentryref::reference::Config::LOCATION))
- .map_err_trace_exit_unwrap();
+ .ok_or_else(|| err_msg("Configuration missing, cannot continue!"))?
+ .read_partial::<libimagentryref::reference::Config>()?
+ .ok_or_else(|| format_err!("Configuration missing: {}", libimagentryref::reference::Config::LOCATION))?;
// TODO: Refactor the above to libimagutil or libimagrt?
let (mut dest, location, uuid) : (Box<dyn Write>, Option<PathBuf>, String) = {
if let Some(mut fl) = scmd.value_of("file-location").map(PathBuf::from) {
let uuid = if fl.is_file() {
- error!("File does exist, cannot create/override");
- exit(1)
+ return Err(err_msg("File does exist, cannot create/override"))
} else if fl.is_dir() {
let uuid = Uuid::new_v4().to_hyphenated().to_string();
fl.push(uuid.clone());
@@ -121,7 +113,7 @@ pub fn create(rt: &Runtime) {
.map(|f| format!(" '{}' ", f)) // ugly
.unwrap_or_else(|| String::from(" ")); // hack
- warn!("File{}has no extension 'vcf'", f); // ahead
+ warn!("File {} has no extension 'vcf'", f); // ahead
warn!("other tools might not recognize this as contact.");
warn!("Continuing...");
}
@@ -135,20 +127,16 @@ pub fn create(rt: &Runtime) {
.write(true)
.create_new(true)
.open(fl.clone())
- .map_warn_err_str("Cannot create/open destination File. Stopping.")
.map_err(Error::from)
- .map_err_trace_exit_unwrap();
-
- let uuid_string = uuid
- .unwrap_or_else(|| {
- fl.file_name()
- .and_then(|fname| fname.to_str())
- .map(String::from)
- .unwrap_or_else(|| {
- error!("Cannot calculate UUID for vcard");
- exit(1)
- })
- });
+ .context("Cannot create/open destination File. Stopping.")?;
+
+ let uuid_string = match uuid {
+ Some(s) => s,
+ None => fl.file_name()
+ .and_then(|fname| fname.to_str())
+ .map(String::from)
+ .ok_or_else(|| err_msg("Cannot calculate UUID for vcard"))?,
+ };
(Box::new(file), Some(fl), uuid_string)
} else {
@@ -158,56 +146,47 @@ pub fn create(rt: &Runtime) {
}
};
- let mut input = rt.stdin().unwrap_or_else(|| {
- error!("No input stream. Cannot ask for permission");
- exit(1)
- });
+ let mut input = rt.stdin().ok_or_else(|| {
+ err_msg("No input stream. Cannot ask for permission")
+ })?;
let mut output = rt.stdout();
loop {
- ::libimagentryedit::edit::edit_in_tmpfile(&rt, &mut template)
- .map_warn_err_str("Editing failed.")
- .map_err_trace_exit_unwrap();
+ ::libimagentryedit::edit::edit_in_tmpfile(&rt, &mut template)?;
if template == TEMPLATE || template.is_empty() {
- error!("No (changed) content in tempfile. Not doing anything.");
- exit(2);
+ return Err(err_msg("No (changed) content in tempfile. Not doing anything."))
}
match ::toml::de::from_str(&template)
- .map(|toml| parse_toml_into_vcard(&mut output, &mut input, toml, uuid.clone()))
.map_err(Error::from)
+ .and_then(|toml| parse_toml_into_vcard(&mut output, &mut input, toml, uuid.clone()))
{
Err(e) => {
error!("Error parsing template");
- trace_error(&e);
- if ask_continue(&mut input, &mut output) {
+ if ask_continue(&mut input, &mut output)? {
continue;
} else {
- exit(1)
+ return Err(e)
}
},
Ok(None) => continue,
Ok(Some(vcard)) => {
if template == TEMPLATE || template.is_empty() {
- if ::libimaginteraction::ask::ask_bool("Abort contact creating", Some(false), &mut input, &mut output)
- .map_err_trace_exit_unwrap()
- {
- exit(1)
+ let q = "Abort contact creating";
+
+ if ::libimaginteraction::ask::ask_bool(q, Some(false), &mut input, &mut output)? {
+ return Ok(())
} else {
continue;
}
}
let vcard_string = write_component(&vcard);
- dest
- .write_all(&vcard_string.as_bytes())
- .map_err(Error::from)
- .map_err_trace_exit_unwrap();
-
+ dest.write_all(&vcard_string.as_bytes())?;
break;
}
}
@@ -215,11 +194,8 @@ pub fn create(rt: &Runtime) {
if let Some(location) = location {
if !scmd.is_present("dont-track") {
- let entry = rt.store()
- .create_from_path(&location, &ref_config, &collection_name)
- .map_err_trace_exit_unwrap();
-
- rt.report_touched(entry.get_location()).unwrap_or_exit();
+ let entry = rt.store().create_from_path(&location, &ref_config, &collection_name)?;
+ rt.report_touched(entry.get_location())?;
info!("Created entry in store");
} else {
@@ -230,26 +206,27 @@ pub fn create(rt: &Runtime) {
}
info!("Ready");
+ Ok(())
}
#[clippy::cognitive_complexity = "71"]
-fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Value, uuid: String) -> Option<Vcard> {
+fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Value, uuid: String) -> Result<Option<Vcard>> {
let mut vcard = VcardBuilder::new().with_uid(uuid);
{ // parse name
debug!("Parsing name");
- let firstname = read_str_from_toml(&toml, "name.first", true);
+ let firstname = read_str_from_toml(&toml, "name.first", true)?;
trace!("firstname = {:?}", firstname);
- let lastname = read_str_from_toml(&toml, "name.last", true);
+ let lastname = read_str_from_toml(&toml, "name.last", true)?;
trace!("lastname = {:?}", lastname);
vcard = vcard.with_name(parameters!(),
- read_str_from_toml(&toml, "name.prefix", false),
+ read_str_from_toml(&toml, "name.prefix", false)?,
firstname.clone(),
- read_str_from_toml(&toml, "name.additional", false),
+ read_str_from_toml(&toml, "name.additional", false)?,
lastname.clone(),
- read_str_from_toml(&toml, "name.suffix", false));
+ read_str_from_toml(&toml, "name.suffix", false)?);
if let (Some(first), Some(last)) = (firstname, lastname) {
trace!("Building fullname: '{} {}'", first, last);
@@ -259,7 +236,7 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
{ // parse personal
debug!("Parsing person information");
- let birthday = read_str_from_toml(&toml, "person.birthday", false);
+ let birthday = read_str_from_toml(&toml, "person.birthday", false)?;
trace!("birthday = {:?}", birthday);
if let Some(bday) = birthday {
@@ -269,10 +246,10 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
{ // parse nicknames
debug!("Parsing nicknames");
- match toml.read("nickname").map_err(Error::from).map_err_trace_exit_unwrap() {
+ match toml.read("nickname").map_err(Error::from)? {
Some(&Value::Array(ref ary)) => {
for (i, element) in ary.iter().enumerate() {
- let nicktype = match read_str_from_toml(element, "type", false) {
+ let nicktype = match read_str_from_toml(element, "type", false)? {
None => BTreeMap::new(),
Some(p) => {
let mut m = BTreeMap::new();
@@ -281,14 +258,14 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
},
};
- let name = match read_str_from_toml(element, "name", false) {
+ let name = match read_str_from_toml(element, "name", false)? {
Some(p) => p,
None => {
error!("Key 'nickname.[{}].name' missing", i);
- if ask_continue(input, output) {
- return None
+ if ask_continue(input, output)? {
+ return Ok(None)
} else {
- exit(1)
+ return Err(format_err!("Key 'nickname.[{}].name' missing", i))
}
},
};
@@ -306,10 +283,10 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
Some(_) => {
error!("Type Error: Expected Array or String at 'nickname'");
- if ask_continue(input, output) {
- return None
+ if ask_continue(input, output)? {
+ return Ok(None)
} else {
- exit(1)
+ return Err(format_err!("Type Error: Expected Array or String at 'nickname'"))
}
},
None => {
@@ -321,17 +298,17 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
{ // parse organisation
debug!("Parsing organisation");
- if let Some(orgs) = read_strary_from_toml(&toml, "organisation.name") {
+ if let Some(orgs) = read_strary_from_toml(&toml, "organisation.name")? {
trace!("orgs = {:?}", orgs);
vcard = vcard.with_org(orgs);
}
- if let Some(title) = read_str_from_toml(&toml, "organisation.title", false) {
+ if let Some(title) = read_str_from_toml(&toml, "organisation.title", false)? {
trace!("title = {:?}", title);
vcard = vcard.with_title(title);
}
- if let Some(role) = read_str_from_toml(&toml, "organisation.role", false) {
+ if let Some(role) = read_str_from_toml(&toml, "organisation.role", false)? {
trace!("role = {:?}", role);
vcard = vcard.with_role(role);
}
@@ -339,29 +316,29 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
{ // parse phone
debug!("Parse phone");
- match toml.read("person.phone").map_err(Error::from).map_err_trace_exit_unwrap() {
+ match toml.read("person.phone")? {
Some(&Value::Array(ref ary)) => {
for (i, element) in ary.iter().enumerate() {
- let phonetype = match read_str_from_toml(element, "type", false) {
+ let phonetype = match read_str_from_toml(element, "type", false)? {
Some(p) => p,
None => {
error!("Key 'phones.[{}].type' missing", i);
- if ask_continue(input, output) {
- return None
+ if ask_continue(input, output)? {
+ return Ok(None)
} else {
- exit(1)
+ return Err(format_err!("Key 'phones.[{}].type' missing", i))
}
}
};
- let number = match read_str_from_toml(element, "number", false) {
+ let number = match read_str_from_toml(element, "number", false)? {
Some(p) => p,
None => {
error!("Key 'phones.[{}].number' missing", i);
- if ask_continue(input, output) {
- return None
+ if ask_continue(input, output)? {
+ return Ok(None)
} else {
- exit(1)
+ return Err(format_err!("Key 'phones.[{}].number' missing", i))
}
}
};
@@ -375,10 +352,10 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
Some(_) => {
error!("Expected Array at 'phones'.");
- if ask_continue(input, output) {
- return None
+ if ask_continue(input, output)? {
+ return Ok(None)
} else {
- exit(1)
+ return Err(format_err!("Expected Array at 'phones'."))
}
},
None => {
@@ -389,29 +366,29 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
{ // parse address
debug!("Parsing address");
- match toml.read("addresses").map_err(Error::from).map_err_trace_exit_unwrap() {
+ match toml.read("addresses")? {
Some(&Value::Array(ref ary)) => {
for (i, element) in ary.iter().enumerate() {
- let adrtype = match read_str_from_toml(element, "type", false) {
+ let adrtype = match read_str_from_toml(element, "type", false)? {
None => {
error!("Key 'adresses.[{}].type' missing", i);
- if ask_continue(input, output) {
- return None
+ if ask_continue(input, output)? {
+ return Ok(None)
} else {
- exit(1)
+ return Err(format_err!("Key 'adresses.[{}].type' missing", i))
}
},
Some(p) => p,
};
trace!("adrtype = {:?}", adrtype);
- let bx = read_str_from_toml(element, "box", false);
- let extended = read_str_from_toml(element, "extended", false);
- let street = read_str_from_toml(element, "street", false);
- let code = read_str_from_toml(element, "code", false);
- let city = read_str_from_toml(element, "city", false);
- let region = read_str_from_toml(element, "region", false);
- let country = read_str_from_toml(element, "country", false);
+ let bx = read_str_from_toml(element, "box", false)?;
+ let extended = read_str_from_toml(element, "extended", false)?;
+ let street = read_str_from_toml(element, "street", false)?;
+ let code = read_str_from_toml(element, "code", false)?;
+ let city = read_str_from_toml(element, "city", false)?;
+ let region = read_str_from_toml(element, "region", false)?;
+ let country = read_str_from_toml(element, "country", false)?;
trace!("bx = {:?}", bx);
trace!("extended = {:?}", extended);
@@ -430,10 +407,10 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
Some(_) => {
error!("Type Error: Expected Array at 'addresses'");
- if ask_continue(input, output) {
- return None
+ if ask_continue(input, output)? {
+ return Ok(None)
} else {
- exit(1)
+ return Err(format_err!("Type Error: Expected Array at 'addresses'"))
}
},
None => {
@@ -444,28 +421,28 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
{ // parse email
debug!("Parsing email");
- match toml.read("person.email").map_err(Error::from).map_err_trace_exit_unwrap() {
+ match toml.read("person.email")? {
Some(&Value::Array(ref ary)) => {
for (i, element) in ary.iter().enumerate() {
- let mailtype = match read_str_from_toml(element, "type", false) {
+ let mailtype = match read_str_from_toml(element, "type", false)? {
None => {
error!("Error: 'email.[{}].type' missing", i);
- if ask_continue(input, output) {
- return None
+ if ask_continue(input, output)? {
+ return Ok(None)
} else {
- exit(1)
+ return Err(format_err!("Error: 'email.[{}].type' missing", i))
}
},
Some(p) => p,
}; // TODO: Unused, because unsupported by vobject
- let mail = match read_str_from_toml(element, "addr", false) {
+ let mail = match read_str_from_toml(element, "addr", false)? {
None => {
error!("Error: 'email.[{}].addr' missing", i);
- if ask_continue(input, output) {
- return None
+ if ask_continue(input, output)? {
+ return Ok(None)
} else {
- exit(1)
+ return Err(format_err!("Error: 'email.[{}].addr' missing", i))
}
},
Some(p) => p,
@@ -480,10 +457,10 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
Some(_) => {
error!("Type Error: Expected Array at 'email'");
- if ask_continue(input, output) {
- return None
+ if ask_continue(input, output)? {
+ return Ok(None)
} else {
- exit(1)
+ return Err(format_err!("Type Error: Expected Array at 'email'"))
}
},
None => {
@@ -494,19 +471,19 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
{ // parse others
debug!("Parsing others");
- if let Some(categories) = read_strary_from_toml(&toml, "other.categories") {
+ if let Some(categories) = read_strary_from_toml(&toml, "other.categories")? {
vcard = vcard.with_categories(categories);
} else {
debug!("No categories");
}
- if let Some(webpage) = read_str_from_toml(&toml, "other.webpage", false) {
+ if let Some(webpage) = read_str_from_toml(&toml, "other.webpage", false)? {
vcard = vcard.with_url(webpage);
} else {
debug!("No webpage");
}
- if let Some(note) = read_str_from_toml(&toml, "other.note", false) {
+ if let Some(note) = read_str_from_toml(&toml, "other.note", false)? {
vcard = vcard.with_note(note);
} else {
debug!("No note");
@@ -517,10 +494,10 @@ fn parse_toml_into_vcard(output: &mut dyn Write, input: &mut dyn Read, toml: Val
let vcard = vcard
.build()
.unwrap(); // TODO: This unwrap does not fail with rust-vobject, why is there a Result<> returned?
- Some(vcard)
+ Ok(Some(vcard))
}
-fn read_strary_from_toml(toml: &Value, path: &'static str) -> Option<Vec<String>> {
+fn read_strary_from_toml(toml: &Value, path: &'static str) -> Result<Option<Vec<String>>> {
match toml.read(path).map_err(Error::from).map_warn_err_str(&format!("Failed to read value at '{}'", path)) {
Ok(Some(&Value::Array(ref vec))) => {
let mut v = Vec::new();
@@ -528,48 +505,37 @@ fn read_strary_from_toml(toml: &Value, path: &'static str) -> Option<Vec<String>
match *elem {
Value::String(ref s) => v.push(s.clone()),
_ => {
- error!("Type Error: '{}' must be Array<String>", path);
- return None
+ return Err(format_err!("Type Error: '{}' must be Array<String>", path))
},
}
}
- Some(v)
+ Ok(Some(v))
}
Ok(Some(&Value::String(ref s))) => {
warn!("Having String, wanting Array<String> ... going to auto-fix");
- Some(vec![s.clone()])
+ Ok(Some(vec![s.clone()]))
},
Ok(Some(_)) => {
- error!("Type Error: '{}' must be Array<String>", path);
- None
+ return Err(format_err!("Type Error: '{}' must be Array<String>", path))
},
- Ok(None) => None,
- Err(_) => None,
+ Ok(None) => Ok(None),
+ Err(_) => Ok(None),
}
}
-fn read_str_from_toml(toml: &Value, path: &'static str, must_be_there: bool) -> Option<String> {
- let v = toml.read(path)
- .map_err(Error::from)
- .map_warn_err_str(&format!("Failed to read value at '{}'", path));
-
- match v {
- Ok(Some(&Value::String(ref s))) => Some(s.clone()),
- Ok(Some(_)) => {
- error!("Type Error: '{}' must be String", path);
- None
+fn read_str_from_toml(toml: &Value, path: &'static str, must_be_there: bool) -> Result<Option<String>> {
+ match toml.read(path)? {
+ Some(&Value::String(ref s)) => Ok(Some(s.clone())),
+ Some(_) => {
+ Err(format_err!("Type Error: '{}' must be String", path))
},
- Ok(None) => {
+ None => {
if must_be_there {
- error!("Expected '{}' to be present, but is not.", path);
+ return Err(format_err!("Expected '{}' to be present, but is not.", path))
}
- None
+ Ok(None)
},
- Err(e) => {
- trace_error(&e);
- None
- }
}
}
@@ -585,7 +551,7 @@ mod test_parsing {
fn test_template_names() {
let uid = String::from("uid");
let mut output = Vec::new();
- let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid);
+ let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
@@ -603,7 +569,7 @@ mod test_parsing {
fn test_template_person() {
let uid = String::from("uid");
let mut output = Vec::new();
- let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid);
+ let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
@@ -622,7 +588,7 @@ mod test_parsing {
fn test_template_organization() {
let uid = String::from("uid");
let mut output = Vec::new();
- let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid);
+ let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
@@ -640,7 +606,7 @@ mod test_parsing {
fn test_template_phone() {
let uid = String::from("uid");
let mut output = Vec::new();
- let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid);
+ let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
@@ -656,7 +622,7 @@ mod test_parsing {
fn test_template_email() {
let uid = String::from("uid");
let mut output = Vec::new();
- let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid);
+ let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
@@ -672,7 +638,7 @@ mod test_parsing {
fn test_template_addresses() {
let uid = String::from("uid");
let mut output = Vec::new();
- let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid);
+ let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
@@ -690,7 +656,7 @@ mod test_parsing {
fn test_template_other() {
let uid = String::from("uid");
let mut output = Vec::new();
- let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid);
+ let vcard = parse_toml_into_vcard(&mut output, &mut empty(), ::toml::de::from_str(TEMPLATE).unwrap(), uid).unwrap();
assert!(vcard.is_some(), "Failed to parse test template.");
let vcard = vcard.unwrap();
diff --git a/bin/domain/imag-contact/src/edit.rs b/bin/domain/imag-contact/src/edit.rs
index 78e436ba..ecfeffef 100644
--- a/bin/domain/imag-contact/src/edit.rs
+++ b/bin/domain/imag-contact/src/edit.rs
@@ -32,16 +32,16 @@
while_true,
)]
-use std::process::exit;
use std::io::Read;
use std::io::Write;
-use failure::Error;
use failure::err_msg;
use failure::Fallible as Result;
+use resiter::Filter;
+use resiter::Map;
+use resiter::AndThen;
use libimagrt::runtime::Runtime;
-use libimagerror::trace::MapErrTrace;
use libimagstore::store::FileLockEntry;
use libimagcontact::store::ContactStore;
use libimagentryref::reference::fassade::RefFassade;
@@ -49,61 +49,56 @@ use libimagentryref::hasher::default::DefaultHasher;
use libimagentryref::reference::Ref;
use libimagentryref::reference::Config as RefConfig;
-pub fn edit(rt: &Runtime) {
+pub fn edit(rt: &Runtime) -> Result<()> {
let scmd = rt.cli().subcommand_matches("edit").unwrap();
let collection_name = rt.cli().value_of("contact-ref-collection-name").unwrap(); // default by clap
- let ref_config = libimagentryref::util::get_ref_config(&rt, "imag-contact").map_err_trace_exit_unwrap();
+ let ref_config = libimagentryref::util::get_ref_config(&rt, "imag-contact")?;
let hash = scmd.value_of("hash").map(String::from).unwrap(); // safed by clap
let force_override = true; // when editing, we want to override, right?
let retry = !scmd.is_present("fail-on-parse-error");
if rt.output_is_pipe() {
- error!("Cannot spawn editor if output is a pipe!");
- exit(1);
+ return Err(err_msg("Cannot spawn editor if output is a pipe!"))
}
let mut output = rt.stdout();
- let mut input = rt.stdin().unwrap_or_else(|| {
- error!("No input stream. Cannot ask for permission.");
- exit(1)
- });
+ let mut input = rt.stdin().ok_or_else(|| {
+ err_msg("No input stream. Cannot ask for permission.")
+ })?;
- crate::util::find_contact_by_hash(rt, hash)
- .for_each(|contact| {
+ crate::util::find_contact_by_hash(rt, hash)?
+ .filter_ok(|tpl| tpl.0)
+ .map_ok(|tpl| tpl.1)
+ .and_then_ok(|contact| {
loop {
let res = edit_contact(&rt, &contact, &ref_config, collection_name, force_override);
+
if !retry {
- res.map_err_trace_exit_unwrap();
- } else if ask_continue(&mut input, &mut output) {
- continue;
-} else {
- exit(1)
-}
+ return res
+ } else if ask_continue(&mut input, &mut output)? {
+ continue;
+ } else {
+ return res
+ }
}
- });
+ })
+ .collect::<Result<Vec<_>>>()
+ .map(|_| ())
}
fn edit_contact<'a>(rt: &Runtime, contact: &FileLockEntry<'a>, ref_config: &RefConfig, collection_name: &str, force_override: bool) -> Result<()> {
let filepath = contact
.as_ref_with_hasher::<DefaultHasher>()
- .get_path(ref_config)
- .map_err_trace_exit_unwrap();
+ .get_path(ref_config)?;
- let success = rt.editor()
- .map_err_trace_exit_unwrap()
- .ok_or_else(|| {
- err_msg("I have no editor configured. Cannot continue!")
- })
- .map_err_trace_exit_unwrap()
+ let success = rt.editor()?
+ .ok_or_else(|| err_msg("I have no editor configured. Cannot continue!"))?
.arg(&filepath)
- .status()
- .map_err(Error::from)
- .map_err_trace_exit_unwrap()
+ .status()?
.success();
if !success {
- error!("Editor failed!");
- exit(1);
+ return Err(err_msg("Editor failed!"))
}
rt.store()
@@ -111,8 +106,7 @@ fn edit_contact<'a>(rt: &Runtime, contact: &FileLockEntry<'a>, ref_config: &RefC
.map(|_| ())
}
-fn ask_continue(inputstream: &mut dyn Read, outputstream: &mut dyn Write) -> bool {
+fn ask_continue(inputstream: &mut dyn Read, outputstream: &mut dyn Write) -> Result<bool> {
::libimaginteraction::ask::ask_bool("Edit vcard", Some(true), inputstream, outputstream)
- .map_err_trace_exit_unwrap()
}
diff --git a/bin/domain/imag-contact/src/lib.rs b/bin/domain/imag-contact/src/lib.rs
index 0285f128..873c822a 100644
--- a/bin/domain/imag-contact/src/lib.rs
+++ b/bin/domain/imag-contact/src/lib.rs
@@ -44,6 +44,7 @@ extern crate walkdir;
extern crate uuid;
extern crate serde_json;
#[macro_use] extern crate failure;
+extern crate resiter;
extern crate libimagcontact;
extern crate libimagstore;
@@ -54,7 +55,6 @@ extern crate libimaginteraction;
extern crate libimagentryedit;
extern crate libimagentryref;
-use std::process::exit;
use std::path::PathBuf;
use std::io::Write;
@@ -67,13 +67,13 @@ use walkdir::WalkDir;
use failure::Error;
use failure::err_msg;
use failure::Fallible as Result;
+use resiter::AndThen;
+use resiter::IterInnerOkOrElse;
+use resiter::Map;
+use resiter::Filter;
use libimagrt::runtime::Runtime;
use libimagrt::application::ImagApplication;
-use libimagerror::trace::MapErrTrace;
-use libimagerror::io::ToExitCode;
-use libimagerror::exit::ExitUnwrap;
-use libimagerror::iter::TraceIterator;
use libimagcontact::store::ContactStore;
use libimagcontact::contact::Contact;
use libimagcontact::deser::DeserVcard;
@@ -94,26 +94,22 @@ use crate::edit::edit;
pub enum ImagContact {}
impl ImagApplication for ImagContact {
fn run(rt: Runtime) -> Result<()> {
- if let Some(name) = rt.cli().subcommand_name() {
- debug!("Call {}", name);
- match name {
- "list" => list(&rt),
- "import" => import(&rt),
- "show" => show(&rt),
- "edit" => edit(&rt),
- "find" => find(&rt),
- "create" => create(&rt),
- other => {
- debug!("Unknown command");
- let _ = rt.handle_unknown_subcommand("imag-contact", other, rt.cli())
- .map_err_trace_exit_unwrap()
- .code()
- .map(::std::process::exit);
- },
- }
+ match rt.cli().subcommand_name().ok_or_else(|| err_msg("No subcommand called"))? {
+ "list" => list(&rt),
+ "import" => import(&rt),
+ "show" => show(&rt),
+ "edit" => edit(&rt),
+ "find" => find(&rt),
+ "create" => create(&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"))
+ }
+ },
}
-
- Ok(())
}
fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
@@ -133,128 +129,117 @@ impl ImagApplication for ImagContact {
}
}
-fn list(rt: &Runtime) {
+fn list(rt: &Runtime) -> Result<()> {
let scmd = rt.cli().subcommand_matches("list").unwrap();
- let list_format = get_contact_print_format("contact.list_format", rt, &scmd);
+ let list_format = get_contact_print_format("contact.list_format", rt, &scmd)?;
debug!("List format: {:?}", list_format);
let iterator = rt
.store()
- .all_contacts()
- .map_err_trace_exit_unwrap()
+ .all_contacts()?
.into_get_iter()
- .trace_unwrap_exit()
- .map(|fle| fle.ok_or_else(|| err_msg("StoreId not found".to_owned())))
- .trace_unwrap_exit()
- .map(|fle| {
- rt.report_touched(fle.get_location()).unwrap_or_exit();
- fle
+ .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
+ .and_then_ok(|fle| {
+ rt.report_touched(fle.get_location())?;
+ Ok(fle)
})
- .map(|e| e.deser())
- .trace_unwrap_exit()
- .enumerate();
+ .and_then_ok(|e| e.deser());
if scmd.is_present("json") {
debug!("Listing as JSON");
- let v : Vec<DeserVcard> = iterator.map(|tpl| tpl.1).collect();
-
- match ::serde_json::to_string(&v) {
- Ok(s) => writeln!(rt.stdout(), "{}", s).to_exit_code().unwrap_or_exit(),
- Err(e) => {
- error!("Error generating JSON: {:?}", e);
- ::std::process::exit(1)
- }
- }
+ let v = iterator.collect::<Result<Vec<DeserVcard>>>()?;
+ let s = ::serde_json::to_string(&v)?;
+ writeln!(rt.stdout(), "{}", s).map_err(Error::from)
} else {
debug!("Not listing as JSON");
let output = rt.stdout();
let mut output = output.lock();
+ let mut i = 0;
iterator
- .map(|(i, dvcard)| build_data_object_for_handlebars(i, &dvcard))
- .map(|data| list_format.render("format", &data).map_err(Error::from))
- .trace_unwrap_exit()
- .for_each(|s| {
- writeln!(output, "{}", s).to_exit_code().unwrap_or_exit()
- });
+ .map_ok(|dvcard| {
+ i += 1;
+ build_data_object_for_handlebars(i, &dvcard)
+ })
+ .and_then_ok(|data| list_format.render("format", &data).map_err(Error::from))
+ .and_then_ok(|s| writeln!(output, "{}", s).map_err(Error::from))
+ .collect::<Result<Vec<_>>>()
+ .map(|_| ())
}
}
-fn import(rt: &Runtime) {
+fn import(rt: &Runtime) -> Result<()> {
let scmd = rt.cli().subcommand_matches("import").unwrap(); // secured by main
let force_override = scmd.is_present("force-override");
let path = scmd.value_of("path").map(PathBuf::from).unwrap(); // secured by clap
let collection_name = rt.cli().value_of("contact-ref-collection-name").unwrap(); // default by clap
let ref_config = rt.config()
- .ok_or_else(|| format_err!("No configuration, cannot continue!"))
- .map_err_trace_exit_unwrap()
- .read_partial::<libimagentryref::reference::Config>()
- .map_err(Error::from)
- .map_err_trace_exit_unwrap()
- .ok_or_else(|| format_err!("Configuration missing: {}", libimagentryref::reference::Config::LOCATION))
- .map_err_trace_exit_unwrap();
- // TODO: Refactor the above to libimagutil or libimagrt?
+ .ok_or_else(|| format_err!("No configuration, cannot continue!"))?
+ .read_partial::<libimagentryref::reference::Config>()?
+ .ok_or_else(|| format_err!("Configuration missing: {}", libimagentryref::reference::Config::LOCATION))?;
+ // TODO: Refactor the above to libimagutil or libimagrt?
if !path.exists() {
- error!("Path does not exist");
- exit(1)
+ return Err(format_err!("Path does not exist: {}", path.display()))
}
if path.is_file() {
let entry = rt
.store()
- .retrieve_from_path(&path, &ref_config, &collection_name, force_override)
- .map_err_trace_exit_unwrap();
+ .retrieve_from_path(&path, &ref_config, &collection_name, force_override)?;
- rt.report_touched(entry.get_location()).unwrap_or_exit();
+ rt.report_touched(entry.get_location()).map_err(Error::from)
} else if path.is_dir() {
- for entry in WalkDir::new(path).min_depth(1).into_iter() {
- let entry = entry
- .map_err(Error::from)
- .map_err_trace_exit_unwrap();
-
- if entry.file_type().is_file() {
- let pb = PathBuf::from(entry.path());
- let fle = rt
- .store()
- .retrieve_from_path(&pb, &ref_config, &collection_name, force_override)
- .map_err_trace_exit_unwrap();
-
- rt.report_touched(fle.get_location()).unwrap_or_exit();
- info!("Imported: {}", entry.path().to_str().unwrap_or("<non UTF-8 path>"));
- } else {
- warn!("Ignoring non-file: {}", entry.path().to_str().unwrap_or("<non UTF-8 path>"));
- }
- }
+ WalkDir::new(path)
+ .min_depth(1)
+ .into_iter()
+ .map(|r| r.map_err(Error::from))
+ .and_then_ok(|entry| {
+ if entry.file_type().is_file() {
+ let pb = PathBuf::from(entry.path());
+ let fle = rt
+ .store()
+ .retrieve_from_path(&pb, &ref_config, &collection_name, force_override)?;
+
+ rt.report_touched(fle.get_location())?;
+ info!("Imported: {}", entry.path().to_str().unwrap_or("<non UTF-8 path>"));
+ Ok(())
+ } else {
+ warn!("Ignoring non-file: {}", entry.path().to_str().unwrap_or("<non UTF-8 path>"));
+ Ok(())
+ }
+ })
+ .collect::<Result<Vec<_>>>()
+ .map(|_| ())
} else {
- error!("Path is neither directory nor file");
- exit(1)
+ Err(err_msg("Path is neither directory nor file"))
}
}
-fn show(rt: &Runtime) {
+fn show(rt: &Runtime) -> Result<()> {
let scmd = rt.cli().subcommand_matches("show").unwrap();
let hash = scmd.value_of("hash").map(String::from).unwrap(); // safed by clap
- let show_format = get_contact_print_format("contact.show_format", rt, &scmd);
+ let show_format = get_contact_print_format("contact.show_format", rt, &scmd)?;
let out = rt.stdout();
let mut outlock = out.lock();
- util::find_contact_by_hash(rt, hash)
+ util::find_contact_by_hash(rt, hash)?
+ .filter_ok(|tpl| tpl.0)
+ .map_ok(|tpl| tpl.1)
.enumerate()
- .for_each(|(i, elem)| {
- let elem = elem.deser().map_err_trace_exit_unwrap();
+ .map(|(i, elem)| {
+ let elem = elem?.deser()?;
let data = build_data_object_for_handlebars(i, &elem);
- let s = show_format
- .render("format", &data)
- .map_err(Error::from)
- .map_err_trace_exit_unwrap();
- writeln!(outlock, "{}", s).to_exit_code().unwrap_or_exit();
- });
+ let s = show_format.render("format", &data)?;
+ writeln!(outlock, "{}", s).map_err(Error::from)
+ })
+ .collect::<Result<Vec<_>>>()
+ .map(|_| ())
}
-fn find(rt: &Runtime) {
+fn find(rt: &Runtime) -> Result<()> {
let scmd = rt.cli().subcommand_matches("find").unwrap();
let grepstring = scmd
.values_of("string")
@@ -263,24 +248,16 @@ fn find(rt: &Runtime) {
.collect::<Vec<String>>();
// We don't know yet which we need, but we pay that price for simplicity of the codebase
- let show_format = get_contact_print_format("contact.show_format", rt, &scmd);
- let list_format = get_contact_print_format("contact.list_format", rt, &scmd);
+ let show_format = get_contact_print_format("contact.show_format", rt, &scmd)?;
+ let list_format = get_contact_print_format("contact.list_format", rt, &scmd)?;
let iterator = rt
.store()
- .all_contacts()
- .map_err_trace_exit_unwrap()
+ .all_contacts()?
.into_get_iter()
- .map(|el| {
- el.map_err_trace_exit_unwrap()
- .ok_or_else(|| {
- error!("Could not get StoreId from Store::all_contacts(). This is a BUG!");
- ::std::process::exit(1)
- })
- .unwrap() // safed above
- })
- .filter_map(|entry| {
- let card = entry.deser().map_err_trace_exit_unwrap();
+ .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
+ .and_then_ok(|entry| {
+ let card = entry.deser()?;
let str_contains_any = |s: &String, v: &Vec<String>| {
v.iter().any(|i| s.contains(i))
@@ -291,91 +268,94 @@ fn find(rt: &Runtime) {
|| card.fullname().iter().any(|a| str_contains_any(a, &grepstring));
if take {
- rt.report_touched(entry.get_location()).unwrap_or_exit();
+ rt.report_touched(entry.get_location())?;
// optimization so we don't have to parse again in the next step
- Some((entry, card))
+ Ok((true, entry, card))
} else {
- None
+ Ok((false, entry, card))
}
- })
- .enumerate();
+ });
+
+ let mut i = 0;
if !rt.output_is_pipe() || rt.ignore_ids() {
if scmd.is_present("json") {
- let v : Vec<DeserVcard> = iterator.map(|(_, tlp)| tlp.1).collect();
-
- match ::serde_json::to_string(&v) {
- Ok(s) => writeln!(rt.stdout(), "{}", s).to_exit_code().unwrap_or_exit(),
- Err(e) => {
- error!("Error generating JSON: {:?}", e);
- ::std::process::exit(1)
- }
- }
+ iterator
+ .filter_ok(|tpl| tpl.0)
+ .map_ok(|tpl| tpl.2)
+ .and_then_ok(|v| {
+ let s = ::serde_json::to_string(&v)?;
+ writeln!(rt.stdout(), "{}", s).map_err(Error::from)
+ })
+ .collect::<Result<Vec<_>>>()
+ .map(|_| ())
} else if scmd.is_present("find-id") {
iterator
- .for_each(|(_i, (entry, _))| {
- writeln!(rt.stdout(), "{}", entry.get_location())
- .to_exit_code()
- .unwrap_or_exit();
+ .and_then_ok(|(take, entry, _)| {
+ if take {
+ writeln!(rt.stdout(), "{}", entry.get_location()).map_err(Error::from)
+ } else {
+ Ok(())
+ }
})
+ .collect::<Result<Vec<_>>>()
+ .map(|_| ())
} else if scmd.is_present("find-full-id") {
let storepath = rt.store().path().display();
iterator
- .for_each(|(_i, (entry, _))| {
- writeln!(rt.stdout(), "{}/{}", storepath, entry.get_location())
- .to_exit_code()
- .unwrap_or_exit();
+ .and_then_ok(|(take, entry, _)| {
+ if take {
+ writeln!(rt.stdout(), "{}/{}", storepath, entry.get_location()).map_err(Error::from)
+ } else {
+ Ok(())
+ }
})
+ .collect::<Result<Vec<_>>>()
+ .map(|_| ())
} else {
iterator
- .for_each(|(i, (_, card))| {
- let fmt = if scmd.is_present("find-show") {
- &show_format
- } else { // default: find-list
- &list_format
- };
-
- let data = build_data_object_for_handlebars(i, &card);
- let s = fmt
- .render("format", &data)
- .map_err(Error::from)
- .map_err_trace_exit_unwrap();
-
- writeln!(rt.stdout(), "{}", s)
- .to_exit_code()
- .unwrap_or_exit();
- });
+ .and_then_ok(|(take, _, card)| {
+ if take {
+ i += 1;
+ let fmt = if scmd.is_present("find-show") {
+ &show_format
+ } else { // default: find-list
+ &list_format
+ };
+
+ let data = build_data_object_for_handlebars(i, &card);
+ let s = fmt.render("format", &data)?;
+
+ writeln!(rt.stdout(), "{}", s).map_err(Error::from)
+ } else {
+ Ok(())
+ }
+ })
+ .collect::<Result<Vec<_>>>()
+ .map(|_| ())
}
} else { // if not printing, we still have to consume the iterator to report the touched IDs
let _ = iterator.collect::<Vec<_>>();
+ Ok(())
}
}
-fn get_contact_print_format(config_value_path: &'static str, rt: &Runtime, scmd: &ArgMatches) -> Handlebars {
- let fmt = scmd
- .value_of("format")
- .map(String::from)
- .unwrap_or_else(|| {
- rt.config()
- .ok_or_else(|| err_msg("No configuration file"))
- .map_err_trace_exit_unwrap()
- .read_string(config_value_path)
- .map_err(Error::from)
- .map_err_trace_exit_unwrap()
- .ok_or_else(|| err_msg("Configuration 'contact.list_format' does not exist"))
- .map_err_trace_exit_unwrap()
- });
+fn get_contact_print_format(config_value_path: &'static str, rt: &Runtime, scmd: &ArgMatches) -> Result<Handlebars> {
+ let fmt = match scmd.value_of("format").map(String::from) {
+ Some(s) => Ok(s),
+ None => rt.config()
+ .ok_or_else(|| err_msg("No configuration file"))?
+ .read_string(config_value_path)?
+ .ok_or_else(|| err_msg("Configuration 'contact.list_format' does not exist")),
+ }?;
let mut hb = Handlebars::new();
- hb
- .register_template_string("format", fmt)
- .map_err(Error::from)
- .map_err_trace_exit_unwrap();
+ hb.register_template_string("format", fmt)?;
hb.register_escape_fn(::handlebars::no_escape);
::libimaginteraction::format::register_all_color_helpers(&mut hb);
::libimaginteraction::format::register_all_format_helpers(&mut hb);
- hb
+ Ok(hb)
}
diff --git a/bin/domain/imag-contact/src/util.rs b/bin/domain/imag-contact/src/util.rs
index 61d061cc..0910f644 100644
--- a/bin/domain/imag-contact/src/util.rs
+++ b/bin/domain/imag-contact/src/util.rs
@@ -18,14 +18,15 @@
//
use std::collections::BTreeMap;
-use std::process::exit;
+
+use failure::Fallible as Result;
+use failure::err_msg;
+use resiter::IterInnerOkOrElse;
+use resiter::AndThen;
use libimagcontact::deser::DeserVcard;
use libimagcontact::store::ContactStore;
use libimagcontact::contact::Contact;
-use libimagerror::exit::ExitUnwrap;
-use libimagerror::iter::TraceIterator;
-use libimagerror::trace::MapErrTrace;
use libimagrt::runtime::Runtime;
use libimagstore::store::FileLockEntry;
@@ -85,34 +86,24 @@ pub fn build_data_object_for_handlebars(i: usize, vcard: &DeserVcard) -> BTreeMa
}
pub fn find_contact_by_hash<'a, H: AsRef<str>>(rt: &'a Runtime, hash: H)
- -> impl Iterator<Item = FileLockEntry<'a>>
+ -> Result<impl Iterator<Item = Result<(bool, FileLockEntry<'a>)>>>
{
- rt.store()
- .all_contacts()
- .map_err_trace_exit_unwrap()
+ Ok(rt.store()
+ .all_contacts()?
.into_get_iter()
- .trace_unwrap_exit()
- .map(|o| o.unwrap_or_else(|| {
- error!("Failed to get entry");
- exit(1)
- }))
- .filter(move |entry| {
- let deser = entry.deser().map_err_trace_exit_unwrap();
+ .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
+ .and_then_ok(move |entry| {
+ let deser = entry.deser()?;
let id_starts_with_hash = deser.uid()
- .ok_or_else(|| {
- error!("Could not get StoreId from Store::all_contacts(). This is a BUG!");
- ::std::process::exit(1)
- })
- .unwrap() // exited above
+ .ok_or_else(|| err_msg("Could not get StoreId from Store::all_contacts(). This is a BUG!"))?
.starts_with(hash.as_ref());
if id_starts_with_hash {
- rt.report_touched(entry.get_location()).unwrap_or_exit();
- true
- } else {
- false
+ rt.report_touched(entry.get_location())?;
}
- })
+
+ Ok((id_starts_with_hash, entry))
+ }))
}
diff --git a/bin/domain/imag-mail/Cargo.toml b/bin/domain/imag-mail/Cargo.toml
index 0c1afed6..ed88d064 100644
--- a/bin/domain/imag-mail/Cargo.toml
+++ b/bin/domain/imag-mail/Cargo.toml
@@ -23,6 +23,7 @@ maintenance = { status = "actively-developed" }
log = "0.4.6"
failure = "0.1.5"
indoc = "0.3.3"
+resiter = "0.4"
libimagrt = { version = "0.10.0", path = "../../../lib/core/libimagrt" }
libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" }
diff --git a/bin/domain/imag-mail/src/lib.rs b/bin/domain/imag-mail/src/lib.rs
index d30062eb..85049a1f 100644
--- a/bin/domain/imag-mail/src/lib.rs
+++ b/bin/domain/imag-mail/src/lib.rs
@@ -39,6 +39,7 @@ extern crate clap;
#[macro_use] extern crate failure;
extern crate toml_query;
#[macro_use] extern crate indoc;
+extern crate resiter;
extern crate libimagrt;
extern crate libimagmail;
@@ -51,13 +52,13 @@ use std::io::Write;
use std::path::PathBuf;
use failure::Fallible as Result;
+use failure::err_msg;
+use failure::Error;
use toml_query::read::TomlValueReadTypeExt;
use clap::App;
+use resiter::AndThen;
+use resiter::IterInnerOkOrElse;
-use libimagerror::trace::{MapErrTrace, trace_error};
-use libimagerror::iter::TraceIterator;
-use libimagerror::exit::ExitUnwrap;
-use libimagerror::io::ToExitCode;
use libimagmail::mail::Mail;
use libimagmail::store::MailStore;
use libimagmail::util;
@@ -79,25 +80,19 @@ mod ui;
pub enum ImagMail {}
impl ImagApplication for ImagMail {
fn run(rt: Runtime) -> Result<()> {
-
- if let Some(name) = rt.cli().subcommand_name() {
-
- debug!("Call {}", name);
- match name {
- "import-mail" => import_mail(&rt),
- "list" => list(&rt),
- "mail-store" => mail_store(&rt),
- other => {
- debug!("Unknown command");
- let _ = rt.handle_unknown_subcommand("imag-mail", other, rt.cli())
- .map_err_trace_exit_unwrap()
- .code()
- .map(::std::process::exit);
- }
+ match rt.cli().subcommand_name().ok_or_else(|| err_msg("No subcommand called"))? {
+ "import-mail" => import_mail(&rt),
+ "list" => list(&rt),
+ "mail-store" => mail_store(&rt),
+ other => {
+ debug!("Unknown command");
+ if rt.handle_unknown_subcommand("imag-mail", other, rt.cli())?.success() {
+ Ok(())
+ } else {
+ Err(err_msg("Failed to handle unknown subcommand"))
}
- }
-
- Ok(())
+ },
+ }
}
fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
@@ -118,9 +113,9 @@ impl ImagApplication for ImagMail {
}
-fn import_mail(rt: &Runtime) {
- let collection_name = get_ref_collection_name(rt).map_err_trace_exit_unwrap();
- let refconfig = get_ref_config(rt, "imag-mail").map_err_trace_exit_unwrap();
+fn import_mail(rt: &Runtime) -> Result<()> {
+ let collection_name = get_ref_collection_name(rt)?;
+ let refconfig = get_ref_config(rt, "imag-mail")?;
let scmd = rt.cli().subcommand_matches("import-mail").unwrap();
let store = rt.store();
@@ -139,13 +134,14 @@ fn import_mail(rt: &Runtime) {
store.create_mail_from_path(path, &collection_name, &refconfig)
}
.map_info_str("Ok")
- .map_err_trace_exit_unwrap()
})
- .for_each(|entry| rt.report_touched(entry.get_location()).unwrap_or_exit());
+ .and_then_ok(|e| rt.report_touched(e.get_location()).map_err(Error::from))
+ .collect::<Result<Vec<()>>>()
+ .map(|_| ())
}
-fn list(rt: &Runtime) {
- let refconfig = get_ref_config(rt, "imag-mail").map_err_trace_exit_unwrap();
+fn list(rt: &Runtime) -> Result<()> {
+ let refconfig = get_ref_config(rt, "imag-mail")?;
let scmd = rt.cli().subcommand_matches("list").unwrap(); // safe via clap
let print_content = scmd.is_present("list-read");
@@ -166,42 +162,26 @@ fn list(rt: &Runtime) {
fn list_mail<'a>(rt: &Runtime,
refconfig: &::libimagentryref::reference::Config,
m: &FileLockEntry<'a>,
- print_content: bool) {
-
- let id = match m.get_message_id(&refconfig) {
- Ok(Some(f)) => f,
- Ok(None) => "<no id>".to_owned(),
- Err(e) => {
- trace_error(&e);
- "<error>".to_owned()
- },
+ print_content: bool) -> Result<()> {
+
+ let id = match m.get_message_id(&refconfig)? {
+ Some(f) => f,
+ None => "<no id>".to_owned(),
};
- let from = match m.get_from(&refconfig) {
- Ok(Some(f)) => f,
- Ok(None) => "<no from>".to_owned(),
- Err(e) => {
- trace_error(&e);
- "<error>".to_owned()
- },
+ let from = match m.get_from(&refconfig)? {
+ Some(f) => f,
+ None => "<no from>".to_owned(),
};
- let to = match m.get_to(&refconfig) {
- Ok(Some(f)) => f,
- Ok(None) => "<no to>".to_owned(),
- Err(e) => {
- trace_error(&e);
- "<error>".to_owned()
- },
+ let to = match m.get_to(&refconfig)? {
+ Some(f) => f,
+ None => "<no to>".to_owned(),
};
- let subject = match m.get_subject(&refconfig) {
- Ok(Some(f)) => f,
- Ok(None) => "<no subject>".to_owned(),
- Err(e) => {
- trace_error(&e);
- "<error>".to_owned()
- },
+ let subject = match m.get_subject(&refconfig)? {
+ Some(f) => f,
+ None => "<no subject>".to_owned(),
};
if print_content {
@@ -209,8 +189,7 @@ fn list(rt: &Runtime) {
let content = m.as_ref_with_hasher::<MailHasher>()
.get_path(&refconfig)
- .and_then(util::get_mail_text_content)
- .map_err_trace_exit_unwrap();
+ .and_then(util::get_mail_text_content)?;
writeln!(rt.stdout(),
"Mail: {id}\nFrom: {from}\nTo: {to}\n{subj}\n---\n{content}\n---\n",
@@ -219,7 +198,7 @@ fn list(rt: &Runtime) {
subj = subject,
to = to,
content = content
- ).to_exit_code().unwrap_or_exit();
+ )?;
} else {
writeln!(rt.stdout(),
"Mail: {id}\nFrom: {from}\nTo: {to}\n{subj}\n",
@@ -227,41 +206,37 @@ fn list(rt: &Runtime) {
id = id,
subj = subject,
to = to
- ).to_exit_code().unwrap_or_exit();
+ )?;
}
- rt.report_touched(m.get_location()).unwrap_or_exit();
+ rt.report_touched(m.get_location())?;
+ Ok(())
}
if rt.ids_from_stdin() {
let iter = rt
- .ids::<crate::ui::PathProvider>()
- .map_err_trace_exit_unwrap()
- .unwrap_or_else(|| {
- error!("No ids supplied");
- ::std::process::exit(1);
- })
+ .ids::<crate::ui::PathProvider>()?
+ .ok_or_else(|| err_msg("No ids supplied"))?
.into_iter()
.map(Ok);
StoreIdIterator::new(Box::new(iter))
} else {
rt.store()
- .all_mails()
- .map_err_trace_exit_unwrap()
+ .all_mails()?
.into_storeid_iter()
}
- .map(|id| { debug!("Found: {:?}", id); id })
+ .inspect(|id| debug!("Found: {:?}", id))
.into_get_iter(rt.store())
- .trace_unwrap_exit()
- .filter_map(|e| e)
- .for_each(|m| list_mail(&rt, &refconfig, &m, print_content));
+ .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
+ .and_then_ok(|m| list_mail(&rt, &refconfig, &m, print_content))
+ .collect::<Result<Vec<_>>>()
+ .map(|_| ())
}
-fn mail_store(rt: &Runtime) {
+fn mail_store(rt: &Runtime) -> Result<()> {
let _ = rt.cli().subcommand_matches("mail-store").unwrap();
- error!("This feature is currently not implemented.");
- unimplemented!()
+ Err(format_err!("This feature is currently not implemented."))
}
fn get_ref_collection_name(rt: &Runtime) -> Result<String> {
diff --git a/bin/domain/imag-notes/Cargo.toml b/bin/domain/imag-notes/Cargo.toml
index 90d15f90..438bc27a 100644
--- a/bin/domain/imag-notes/Cargo.toml
+++ b/bin/domain/imag-notes/Cargo.toml
@@ -23,6 +23,7 @@ maintenance = { status = "actively-developed" }
log = "0.4.6"
itertools = "0.8.0"
failure = "0.1.5"
+resiter = "0.4.0"
libimagrt = { version = "0.10.0", path = "../../../lib/core/libimagrt" }
libimagerror = { version = "0.10.0", path = "../../../lib/core/libimagerror" }
diff --git a/bin/domain/imag-notes/src/lib.rs b/bin/domain/imag-notes/src/lib.rs
index a62d299b..c2d7b372 100644
--- a/bin/domain/imag-notes/src/lib.rs
+++ b/bin/domain/imag-notes/src/lib.rs
@@ -37,7 +37,8 @@
extern crate clap;
#[macro_use] extern crate log;
extern crate itertools;
-extern crate failure;
+#[macro_use] extern crate failure;
+extern crate resiter;
extern crate libimagnotes;
extern crate libimagrt;
@@ -47,11 +48,13 @@ extern crate libimagutil;
extern crate libimagstore;
use std::io::Write;
-use std::process::exit;
use itertools::Itertools;
use clap::App;
+use failure::Error;
+use failure::err_msg;
use failure::Fallible as Result;
+use resiter::IterInnerOkOrElse;
use libimagentryedit::edit::Edit;
use libimagrt::runtime::Runtime;
@@ -59,11 +62,6 @@ use libimagrt::application::ImagApplication;
use libimagstore::iter::get::StoreIdGetIteratorExtension;
use libimagnotes::note::Note;
use libimagnotes::notestore::*;
-use libimagerror::trace::MapErrTrace;
-use libimagerror::exit::ExitUnwrap;
-use libimagerror::io::ToExitCode;
-use libimagerror::iter::TraceIterator;
-use libimagutil::info_result::*;
use libimagutil::warn_result::WarnResult;
@@ -76,25 +74,20 @@ mod ui;
pub enum ImagNotes {}
impl ImagApplication for ImagNotes {
fn run(rt: Runtime) -> Result<()> {
- if let Some(name) = rt.cli().subcommand_name() {
-
- debug!("Call: {}", name);
- match name {
- "create" => create(&rt),
- "delete" => delete(&rt),
- "edit" => edit(&rt),
- "list" => list(&rt),
- other => {
- debug!("Unknown command");
- let _ = rt.handle_unknown_subcommand("imag-notes", other, rt.cli())
- .map_err_trace_exit_unwrap()
- .code()
- .map(::std::process::exit);
- },
- };
+ match rt.cli().subcommand_name().ok_or_else(|| err_msg("No subcommand called"))? {
+ "create" => create(&rt),
+ "delete" => delete(&rt),
+ "edit" => edit(&rt),
+ "list" => list(&rt),
+ other => {
+ debug!("Unknown command");
+ if rt.handle_unknown_subcommand("imag-notes", other, rt.cli())?.success() {
+ Ok(())
+ } else {
+ Err(err_msg("Failed to handle unknown subcommand"))
+ }
+ },
}
-
- Ok(())
}
fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
@@ -118,74 +111,53 @@ fn name_from_cli(rt: &Runtime, subcmd: &str) -> String {
rt.cli().subcommand_matches(subcmd).unwrap().value_of("name").map(String::from).unwrap()
}
-fn create(rt: &Runtime) {
+fn create(rt: &Runtime) -> Result<()> {
let name = name_from_cli(rt, "create");
- let mut note = rt
- .store()
- .new_note(name.clone(), String::new())
- .map_err_trace_exit_unwrap();
+ let mut note = rt.store().new_note(name.clone(), String::new())?;
if rt.cli().subcommand_matches("create").unwrap().is_present("edit") {
- note
- .edit_content(rt)
- .map_warn_err_str("Editing failed")
- .map_err_trace_exit_unwrap();
+ note.edit_content(rt)?
}
- rt.report_touched(note.get_location()).unwrap_or_exit();
+ rt.report_touched(note.get_location()).map_err(Error::from)
}
-fn delete(rt: &Runtime) {
- rt.store()
- .delete_note(name_from_cli(rt, "delete"))
- .map_info_str("Ok")
- .map_err_trace_exit_unwrap();
+fn delete(rt: &Runtime) -> Result<()> {
+ rt.store().delete_note(name_from_cli(rt, "delete")).map(|_| ())
}
-fn edit(rt: &Runtime) {
+fn edit(rt: &Runtime) -> Result<()> {
let name = name_from_cli(rt, "edit");
rt
.store()
- .get_note(name.clone())
- .map_err_trace_exit_unwrap()
- .map(|mut note| {
- note
- .edit_content(rt)
- .map_warn_err_str("Editing failed")
- .map_err_trace_exit_unwrap();
-
- rt.report_touched(note.get_location()).unwrap_or_exit();
+ .get_note(name.clone())?
+ .ok_or_else(|| format_err!("Name '{}' not found", name))
+ .and_then(|mut note| {
+ note.edit_content(rt).map_warn_err_str("Editing failed")?;
+ rt.report_touched(note.get_location()).map_err(Error::from)
})
- .unwrap_or_else(|| {
- error!("Cannot find note with name '{}'", name);
- });
}
-fn list(rt: &Runtime) {
+fn list(rt: &Runtime) -> Result<()> {
use std::cmp::Ordering;
rt
.store()
- .all_notes()
- .map_err_trace_exit_unwrap()
+ .all_notes()?
.into_get_iter(rt.store())
- .trace_unwrap_exit()
- .map(|opt| opt.unwrap_or_else(|| {
- error!("Fatal: Nonexistent entry where entry should exist");
- exit(1)
- }))
- .sorted_by(|note_a, note_b| if let (Ok(a), Ok(b)) = (note_a.get_name(), note_b.get_name()) {
- a.cmp(&b)
- } else {
- Ordering::Greater
+ .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
+ .collect::<Result<Vec<_>>>()?
+ .into_iter()
+ .sorted_by(|a, b| match (a.get_name(), b.get_name()) {
+ (Ok(a), Ok(b)) => a.cmp(&b),
+ _ => Ordering::Greater,
+ })
+ .map(|note| {
+ let name = note.get_name().map_err(Error::from)?;
+ writeln!(rt.stdout(), "{}", name)?;
+ rt.report_touched(note.get_location()).map_err(Error::from)
})
- .for_each(|note| {
- let name = note.get_name().map_err_trace_exit_unwrap();
- writeln!(rt.stdout(), "{}", name)
- .to_exit_code()
- .unwrap_or_exit();
-
- rt.report_touched(note.get_location()).unwrap_or_exit();
- });
+ .collect::<Result<Vec<_>>>()
+ .map(|_| ())
}
diff --git a/bin/domain/imag-todo/Cargo.toml b/bin/domain/imag-todo/Cargo.toml
index bbb5667a..210f1bce 100644
--- a/bin/domain/imag-todo/Cargo.toml
+++ b/bin/domain/imag-todo/Cargo.toml
@@ -29,6 +29,8 @@ chrono = "0.4"
filters = "0.3"
kairos = "0.3"
resiter = "0.4.0"
+handlebars = "2"
+prettytable-rs = "0.8.0"
libimagrt = { version = "0.10.0", path = "../../../lib/core/libimagrt" }
libimagstore = { version = "0.10.0", path = "../../../lib/core/libimagstore" }
@@ -37,6 +39,7 @@ libimagentryedit = { version = "0.10.0", path = "../../../lib/entry/libimagentry
libimagtodo = { version = "0.10.0", path = "../../../lib/domain/libimagtodo" }
libimagutil = { version = "0.10.0", path = "../../../lib/etc/libimagutil" }
libimagentryview = { version = "0.10.0", path = "../../../lib/entry/libimagentryview" }
+libimaginteraction = { version = "0.10.0", path = "../../../lib/etc/libimaginteraction" }
[dependencies.clap]
version = "2.33.0"
diff --git a/bin/domain/imag-todo/src/lib.rs b/bin/domain/imag-todo/src/lib.rs
index 1ee53439..ff6ba777 100644
--- a/bin/domain/imag-todo/src/lib.rs
+++ b/bin/domain/imag-todo/src/lib.rs
@@ -43,6 +43,8 @@ extern crate kairos;
#[macro_use] extern crate log;
#[macro_use] extern crate failure;
extern crate resiter;
+extern crate handlebars;
+extern crate prettytable;
#[cfg(feature = "import-taskwarrior")]
extern crate task_hookrs;
@@ -63,7 +65,9 @@ extern crate libimagentryedit;
extern crate libimagtodo;
extern crate libimagutil;
extern crate libimagentryview;
+extern crate libimaginteraction;
+use std::ops::Deref;
use std::io::Write;
use std::result::Result as RResult;
@@ -75,9 +79,11 @@ use failure::err_msg;
use clap::App;
use resiter::AndThen;
use resiter::IterInnerOkOrElse;
+use prettytable::Table;
+use prettytable::Cell;
+use prettytable::Row;
use libimagentryedit::edit::Edit;
-use libimagentryview::viewer::ViewFromIter;
use libimagentryview::viewer::Viewer;
use libimagrt::application::ImagApplication;
use libimagrt::runtime::Runtime;
@@ -88,10 +94,10 @@ use libimagtodo::entry::Todo;
use libimagtodo::priority::Priority;
use libimagtodo::status::Status;
use libimagtodo::store::TodoStore;
-use libimagutil::date::datetime_to_string;
mod ui;
mod import;
+mod util;
/// Marker enum for implementing ImagApplication on
///
@@ -251,15 +257,6 @@ fn list_todos(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool) -> Resul
use filters::failable::filter::FailableFilter;
debug!("Listing todos with status filter {:?}", matcher);
- let now = {
- let now = chrono::offset::Local::now();
- NaiveDateTime::new(now.date().naive_local(), now.time())
- };
-
- let filter_hidden = |todo: &FileLockEntry<'_>| -> Result<bool> {
- Ok(todo.get_hidden()?.map(|hid| hid > now).unwrap_or(true))
- };
-
struct TodoViewer {
details: bool,
}
@@ -287,9 +284,9 @@ fn list_todos(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool) -> Resul
status = status,
first_line = first_line)
} else {
- let sched = get_dt_str(entry.get_scheduled(), "Not scheduled")?;
- let hidden = get_dt_str(entry.get_hidden(), "Not hidden")?;
- let due = get_dt_str(entry.get_due(), "No due")?;
+ let sched = util::get_dt_str(entry.get_scheduled(), "Not scheduled")?;
+ let hidden = util::get_dt_str(entry.get_hidden(), "Not hidden")?;
+ let due = util::get_dt_str(entry.get_due(), "No due")?;
let priority = entry.get_priority().map_err(E::from)?.map(|p| p.as_str().to_string())
.unwrap_or("No prio".to_string());
@@ -306,30 +303,65 @@ fn list_todos(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool) -> Resul
}
}
- let viewer = TodoViewer { details: false };
-
- rt.store()
- .get_todos()?
- .into_get_iter()
- .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
- .filter_map(|r| {
- match r.and_then(|e| e.get_status().map(|s| (s, e))) {
- Err(e) => Some(Err(e)),
- Ok((st, e)) => if matcher.matches(st) {
- Some(Ok(e))
- } else {
- None
+ fn process<'a, I>(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool, iter: I) -> Result<()>
+ where I: Iterator<Item = Result<FileLockEntry<'a>>> + Sized
+ {
+ let viewer = TodoViewer { details: false };
+
+ let now = {
+ let now = chrono::offset::Local::now();
+ NaiveDateTime::new(now.date().naive_local(), now.time())
+ };
+
+ let filter_hidden = |todo: &FileLockEntry<'_>| -> Result<bool> {
+ Ok(todo.get_hidden()?.map(|hid| hid > now).unwrap_or(true))
+ };
+
+ iter
+ .filter_map(|r| {
+ match r.and_then(|e| e.get_status().map(|s| (s, e))) {
+ Err(e) => Some(Err(e)),
+ Ok((st, e)) => if matcher.matches(st) {
+ Some(Ok(e))
+ } else {
+ None
+ }
+ }
+ })
+ .and_then_ok(|entry| {
+ if !rt.output_is_pipe() && (show_hidden || filter_hidden.filter(&entry)?) {
+ if let Err(e) = viewer.view_entry(&entry, &mut rt.stdout()) {
+ use libimagentryview::error::Error;
+ match e {
+ Error::Other(e) => return Err(e),
+ Error::Io(e) => if e.kind() != std::io::ErrorKind::BrokenPipe {
+ return Err(failure::Error::from(e))
+ },
+ }
+ }
}
- }
- })
- .and_then_ok(|entry| {
- if !rt.output_is_pipe() && (show_hidden || filter_hidden.filter(&entry)?) {
- viewer.view_entry(&entry, &mut rt.stdout())?;
- }
- rt.report_touched(entry.get_location()).map_err(Error::from)
- })
- .collect()
+ rt.report_touched(entry.get_location()).map_err(Error::from)
+ })
+ .collect()
+ };
+
+ if rt.ids_from_stdin() {
+ let iter = rt.ids::<crate::ui::PathProvider>()?
+ .ok_or_else(|| err_msg("No ids supplied"))?
+ .into_iter()
+ .map(Ok)
+ .into_get_iter(rt.store())
+ .map_inner_ok_or_else(|| err_msg("Did not find one entry"));
+
+ process(&rt, matcher, show_hidden, iter)
+ } else {
+ let iter = rt.store().get_todos()?
+ .into_get_iter()
+ .map_inner_ok_or_else(|| err_msg("Did not find one entry"));
+
+ process(&rt, matcher, show_hidden, iter)
+ }
}
/// Generic todo items list function
@@ -357,43 +389,67 @@ fn list(rt: &Runtime) -> Result<()> {
}
fn show(rt: &Runtime) -> Result<()> {
- #[derive(Default)]
- struct TodoShow;
- impl Viewer for TodoShow {
-
- fn view_entry<W>(&self, entry: &Entry, sink: &mut W) -> RResult<(), libimagentryview::error::Error>
- where W: Write
- {
+ let scmd = rt.cli().subcommand_matches("show").unwrap();
+ let show_format = util::get_todo_print_format("todo.show_format", rt, &scmd)?;
+ let out = rt.stdout();
+ let mut outlock = out.lock();
+
+ fn show_with_table<'a, I>(rt: &Runtime, iter: I) -> Result<()>
+ where I: Iterator<Item = FileLockEntry<'a>>
+ {
+ const HEADER: &'static [&'static str] = &[
+ "uuid",
+ "status",
+ "sched",
+ "hidden",
+ "due",
+ "priority",
+ "text",
+ ];
+
+ let mut table = {
+ let mut t = Table::new();
+ let header = HEADER.iter().map(|s| Cell::new(s)).collect::<Vec<Cell>>();
+ t.set_titles(Row::from(header));
+ t
+ };
+
+ iter.map(|entry| {
use libimagentryview::error::Error as E;
- if !entry.is_todo().map_err(E::from)? {
- return Err(format_err!("Not a Todo: {}", entry.get_location())).map_err(E::from);
- }
-
- let uuid = entry.get_uuid().map_err(E::from)?;
+ let uuid = entry.get_uuid().map_err(E::from)?.to_hyphenated().to_string();
let status = entry.get_status().map_err(E::from)?;
- let status = status.as_str();
- let text = entry.get_content();
- let sched = get_dt_str(entry.get_scheduled(), "Not scheduled")?;
- let hidden = get_dt_str(entry.get_hidden(), "Not hidden")?;
- let due = get_dt_str(entry.get_due(), "No due")?;
- let priority = entry.get_priority().map_err(E::from)?.map(|p| p.as_str().to_string())
- .unwrap_or("No prio".to_string());
-
- writeln!(sink, "{uuid}\nStatus: {status}\nPriority: {prio}\nScheduled: {sched}\nHidden: {hidden}\nDue: {due}\n\n{text}",
- uuid = uuid,
- status = status,
- sched = sched,
- hidden = hidden,
- due = due,
- prio = priority,
- text = text)
- .map_err(Error::from)
- .map_err(libimagentryview::error::Error::from)
- }
+ let status = status.as_str().to_string();
+ let sched = util::get_dt_str(entry.get_scheduled(), "Not scheduled")?.to_string();
+ let hidden = util::get_dt_str(entry.get_hidden(), "Not hidden")?.to_string();
+ let due = util::get_dt_str(entry.get_due(), "No due")?.to_string();
+ let priority = entry.get_priority().map_err(E::from)?.map(|p| p.as_str().to_string()).unwrap_or("No prio".to_string());
+
+ let text = entry.get_content().to_owned();
+
+ let v = [
+ uuid,
+ status,
+ sched,
+ hidden,
+ due,
+ priority,
+ text,
+ ];
+ table.add_row(v.iter().map(|s| Cell::new(s)).collect());
+
+ Ok(entry)
+ })
+ .and_then_ok(|e| rt.report_touched(e.get_location()).map_err(Error::from).map(|_| e))
+ .collect::<Result<Vec<_>>>()?;
+
+ table.print(&mut rt.stdout())
+ .map(|_| ())
+ .map_err(Error::from)
}
- rt.ids::<crate::ui::PathProvider>()?
+ let iter = rt
+ .ids::<crate::ui::PathProvider>()?
.ok_or_else(|| err_msg("No ids supplied"))?
.into_iter()
.map(Ok)
@@ -401,9 +457,19 @@ fn show(rt: &Runtime) -> Result<()> {
.map_inner_ok_or_else(|| err_msg("Did not find one entry"))
.and_then_ok(|e| rt.report_touched(e.get_location()).map_err(Error::from).map(|_| e))
.collect::<Result<Vec<_>>>()?
- .into_iter()
- .view::<TodoShow, _>(&mut rt.stdout())
- .map_err(Error::from)
+ .into_iter();
+
+ if scmd.is_present("show-no-table") {
+ iter.enumerate()
+ .map(|(i, elem)| {
+ let data = util::build_data_object_for_handlebars(i, elem.deref())?;
+ let s = show_format.render("format", &data)?;
+ writeln!(outlock, "{}", s).map_err(Error::from)
+ })
+ .collect()
+ } else {
+ show_with_table(rt, iter)
+ }
}
//
@@ -437,9 +503,3 @@ fn prio_from_str<S: AsRef<str>>(s: S) -> Result<Priority> {
}
}
-fn get_dt_str(d: Result<Option<NaiveDateTime>>, s: &str) -> RResult<String, libimagentryview::error::Error> {
- Ok(d.map_err(libimagentryview::error::Error::from)?
- .map(|v| datetime_to_string(&v))
- .unwrap_or(s.to_string()))
-}
-
diff --git a/bin/domain/imag-todo/src/ui.rs b/bin/domain/imag-todo/src/ui.rs
index 8ed92dba..b5646273 100644
--- a/bin/domain/imag-todo/src/ui.rs
+++ b/bin/domain/imag-todo/src/ui.rs
@@ -138,6 +138,23 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
)
.subcommand(SubCommand::with_name("show")
+ .arg(Arg::with_name("format")
+ .long("format")
+ .short("F")
+ .takes_value(true)
+ .required(false)
+ .help("Output format string")
+ )
+
+ .arg(Arg::with_name("show-no-table")
+ .long("no-table")
+ .short("T")
+ .takes_value(false)
+ .required(false)
+ .multiple(false)
+ .help("Show the entries raw, without the ascii-table")
+ )
+
.arg(Arg::with_name("todos")
.index(1)
.takes_value(true)
@@ -200,6 +217,11 @@ impl IdPathProvider for PathProvider {
}
.map(|v| v
.into_iter()
+ .map(|s| if s.starts_with("todo") {
+ s.to_string()
+ } else {
+ format!("todo/{}", s)
+ })
.map(PathBuf::from)
.map(|pb| pb.into_storeid())
.collect::<Result<Vec<_>>>()
diff --git a/bin/domain/imag-todo/src/util.rs b/bin/domain/imag-todo/src/util.rs
new file mode 100644
index 00000000..6e3fd669
--- /dev/null
+++ b/bin/domain/imag-todo/src/util.rs
@@ -0,0 +1,87 @@
+//
+// imag - the personal information management suite for the commandline
+// Copyright (C) 2015-2019 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 std::collections::BTreeMap;
+use std::result::Result as RResult;
+
+use failure::Fallible as Result;
+use failure::Error;
+use failure::err_msg;
+use handlebars::Handlebars;
+use clap::ArgMatches;
+use chrono::NaiveDateTime;
+use toml_query::read::TomlValueReadTypeExt;
+
+use libimagrt::runtime::Runtime;
+use libimagstore::store::Entry;
+use libimagtodo::entry::Todo;
+use libimagutil::date::datetime_to_string;
+
+pub fn get_dt_str(d: Result<Option<NaiveDateTime>>, s: &str) -> RResult<String, libimagentryview::error::Error> {
+ Ok(d.map_err(libimagentryview::error::Error::from)?
+ .map(|v| datetime_to_string(&v))
+ .unwrap_or(s.to_string()))
+}
+
+pub fn get_todo_print_format(config_value_path: &'static str, rt: &Runtime, scmd: &ArgMatches) -> Result<Handlebars> {
+ let fmt = match scmd.value_of("format").map(String::from) {
+ Some(s) => Ok(s),
+ None => {
+ rt.config()
+ .ok_or_else(|| err_msg("No configuration file"))?
+ .read_string(config_value_path)
+ .map_err(Error::from)?
+ .ok_or_else(|| format_err!("Configuration '{}' does not exist", config_value_path))
+ }
+ }?;
+
+ let mut hb = Handlebars::new();
+ hb.register_template_string("format", fmt)?;
+
+ hb.register_escape_fn(::handlebars::no_escape);
+ ::libimaginteraction::format::register_all_color_helpers(&mut hb);
+ ::libimaginteraction::format::register_all_format_helpers(&mut hb);
+ Ok(hb)
+}
+
+pub fn build_data_object_for_handlebars(i: usize, todo: &Entry) -> Result<BTreeMap<&'static str, String>> {
+ let mut data = BTreeMap::new();
+
+ data.insert("i", format!("{}", i));
+ let uuid = todo.get_uuid().map_err(Error::from)?.to_string();
+ let status = todo.get_status().map_err(Error::from)?;
+ let status = status.as_str().to_string();
+ let text = todo.get_content().to_string();
+ let sched = get_dt_str(todo.get_scheduled(), "Not scheduled")?;
+ let hidden = get_dt_str(todo.get_hidden(), "Not hidden")?;
+ let due = get_dt_str(todo.get_due(), "No due")?;
+ let priority = todo.get_priority().map_err(Error::from)?.map(|p| p.as_str().to_string())
+ .unwrap_or("No prio".to_string());
+
+ data.insert("uuid" , uuid);
+ data.insert("status" , status);
+ data.insert("text" , text);
+ data.insert("sched" , sched);
+ data.insert("hidden" , hidden);
+ data.insert("due" , due);
+ data.insert("priority" , priority);
+
+ Ok(data)
+}
+
diff --git a/doc/src/01000-intro.md b/doc/src/01000-intro.md
index ca645b53..205b3b54 100644
--- a/doc/src/01000-intro.md
+++ b/doc/src/01000-intro.md
@@ -1,86 +1,13 @@
# Introduction {#sec:introduction}
This document is the user documentation for imag, the personal
-information management suite for the commandline.
+information management suite for the commandline.
-**Basically: This is Hobby stuff. Expect incompleteness, false statements and
-generally read with grain of salt.**
+**Basically: This is Hobby stuff. Expect incompleteness, outdated documentation,
+false statements and generally read with grain of salt.**
If you have any objections, suggestions for improvements, bugs, etc, please file
them (See [@sec:contributing]).
A way to reach out to the imag project maintainer(s) is described in the
[@sec:contributing] section.
-
-## The Problem {#sec:intro:problem}
-
-The problem this project tries to solve is to provide a modular commandline
-application for personal information management.
-
-It targets "power users" or "commandline users", uses plain text as a storage
-format and tries to be as scriptable as possible.
-imag offers the ability to link data from different "PIM aspects" (such as
-"diary", "contacts" and "bookmark" for example).
-
-One major goal of imag is to make the PIM data traverseable and queryable.
-For example: a wiki article can be linked to an appointment which is linked to a
-todo which is linked to a note which is linked to a contact.
-
-imag wants to offer an all-in-one scriptable modular commandline personal
-information management suite for all PIM aspects one could possibly think of.
-Because imag uses plain text (TOML headers for structured data and plain text
-which can be rendered using markdown, for example, for continuous text)
-the user is always able to access their data without the imag tools at hand.
-
-
-## The Approach {#sec:intro:approach}
-
-The approach "imag" takes on solving this problem is to store content in a
-"store" and persisting content in a unified way.
-Meta-information is attached to the content which can be used to store
-structured data.
-This can be used to implement a variety of "domain modules" using the store.
-While content is stored in _one_ place, imag does not duplicate content.
-imag does not copy or move icalendar files, emails, vcard files, music or
-movies to the store, but tries to remember the actual files are and stores
-meta-information about them in the store.
-
-Detailed explanation on this approach follows in the chapters of this work.
-
-## Implementation {#sec:intro:implementation}
-
-The program is written in the Rust programming language.
-
-The program consists of libraries which can be re-used by other projects
-to implement and adapt imag functionality. An external program may use a
-library of the imag distribution to store content in the store of imag and
-make it visible to imag this way.
-
-This is a technical detail a user does not necessarily need to know, but as imag
-is intended for power-users anyways, we would say it fits here.
-
-## Alternative Projects {#sec:intro:alternatives}
-
-imag is not the only project which tries to solve that particular problem. For
-example there is
-[org mode](https://orgmode.org)
-for the [emacs](https://www.gnu.org/software/emacs/) text editor.
-There is also [zim](http://zim-wiki.org/), a desktop wiki editor which is
-intended to be used for a personal wiki.
-
-The difference between imag and the mentioned projects is:
-* emacs orgmode is (from what I know and see) for _orgabizing_ things. imag is
- intended not only for organizing, but also for recording, tracking and
- querying.
-* zim is a wiki, which could be used for PIM but is not specialized for it.
- Recording habits might be possible, but not that simple as with imag
-
-imag is not there
-yet, though. Some parts can be used, though it is far away from being feature-complete.
-
-In addition: imag is text-editor independent and other tools than imag might be
-used to access data stored in the imag store.
-For example, one could "grep", "awk" and "sed" entries without much hassle and
-even write bash scripts for automatically filling imag entries with data.
-
-
diff --git a/doc/src/01001-intro-problem.md b/doc/src/01001-intro-problem.md
new file mode 100644
index 00000000..7be11231
--- /dev/null
+++ b/doc/src/01001-intro-problem.md
@@ -0,0 +1,21 @@
+## The Problem {#sec:intro:problem}
+
+The problem this project tries to solve is to provide a modular commandline
+application for personal information management.
+
+It targets "power users" or "commandline users", uses plain text as a storage
+format and tries to be as scriptable as possible.
+imag offers the ability to link data from different "PIM aspects" (such as
+"diary", "contacts" and "bookmark" for example).
+
+One major goal of imag is to make the PIM data traverseable and queryable.
+For example: a wiki article can be linked to an appointment which is linked to a
+todo which is linked to a note which is linked to a contact.
+
+imag wants to offer an all-in-one scriptable modular commandline personal
+information management suite for all PIM aspects one could possibly think of.
+Because imag uses plain text (TOML headers for structured data and plain text
+which can be rendered using markdown, for example, for continuous text)
+the user is always able to access their data without the imag tools at hand.
+
+
diff --git a/doc/src/01002-intro-approach.md b/doc/src/01002-intro-approach.md
new file mode 100644
index 00000000..4c65d8fe
--- /dev/null
+++ b/doc/src/01002-intro-approach.md
@@ -0,0 +1,15 @@
+## The Approach {#sec:intro:approach}
+
+The approach "imag" takes on solving this problem is to store content in a
+"store" and persisting content in a unified way.
+Meta-information is attached to the content which can be used to store
+structured data.
+This can be used to implement a variety of "domain modules" using the store.
+While content is stored in _one_ place, imag does not duplicate content.
+imag does not copy or move icalendar files, emails, vcard files, music or
+movies to the store, but tries to remember the actual files are and stores
+meta-information about them in the store.
+
+Detailed explanation on this approach follows in the chapters of this work.
+
+
diff --git a/doc/src/01003-intro-implementation.md b/doc/src/01003-intro-implementation.md
new file mode 100644
index 00000000..3393eae1
--- /dev/null
+++ b/doc/src/01003-intro-implementation.md
@@ -0,0 +1,13 @@
+## Implementation {#sec:intro:implementation}
+
+The program is written in the Rust programming language.
+
+The program consists of libraries which can be re-used by other projects
+to implement and adapt imag functionality. An external program may use a
+library of the imag distribution to store content in the store of imag and
+make it visible to imag this way.
+
+This is a technical detail a user does not necessarily need to know, but as imag
+is intended for power-users anyways, we would say it fits here.
+
+
diff --git a/doc/src/01004-intro-alternatives.md b/doc/src/01004-intro-alternatives.md
new file mode 100644
index 00000000..e50a8fb1
--- /dev/null
+++ b/doc/src/01004-intro-alternatives.md
@@ -0,0 +1,26 @@
+## Alternative Projects {#sec:intro:alternatives}
+
+imag is not the only project which tries to solve that particular problem. For
+example there is
+[org mode](https://orgmode.org)
+for the [emacs](https://www.gnu.org/software/emacs/) text editor.
+There is also [zim](http://zim-wiki.org/), a desktop wiki editor which is
+intended to be used for a personal wiki.
+
+The difference between imag and the mentioned projects is:
+* emacs orgmode is (from what I know and see) for _orgabizing_ things. imag is
+ intended not only for organizing, but also for recording, tracking and
+ querying.
+* zim is a wiki, which could be used for PIM but is not specialized for it.
+ Recording habits might be possible, but not that simple as with imag
+
+imag is not there
+yet, though. Some parts can be used, though it is far away from being feature-complete.
+
+In addition: imag is text-editor independent and other tools than imag might be
+used to access data stored in the imag store.
+For example, one could "grep", "awk" and "sed" entries without much hassle and
+even write bash scripts for automatically filling imag entries with data.
+
+
+
diff --git a/doc/src/01010-architecture.md b/doc/src/01010-architecture.md
index f3d98a9b..5ea8ddff 100644
--- a/doc/src/01010-architecture.md
+++ b/doc/src/01010-architecture.md
@@ -4,47 +4,48 @@ The imag codebase has a rather simple overall architecture.
In this chapter the types of crates, architecture of an imag module
and the type structure are described.
-## Crate types
-
-There are different types of crates in the imag world. A crate is a rust
-project.
-
-First of all, there are core crates. These crates provide the very core of imag
-and almost all other crates use them:
-* libimagstore - The imag store is the abstraction over the filesystem. It
- provides primitives to get, write and manipulate store entries and their
- header information.
-* libimagrt - The runtime library, which provides functionality to create a
- store object from libimagstore, helps with configurarion loading and
- commandline argument handling (through the external "clap" crate).
-* libimagerror - Error handling library for handling errors the imag way. Used
- in all other crates, even the store itself. It also offers functionality to
- log and trace errors as well as exiting the application, if necessary.
-* libimagutil - Utilities.
-
-The next type of imag crates are entry extension libraries. Those provide
-extensional functionality for the types from libimagstore. For example, there is
-"libimagentrylink" which provides functionality to link two entries in the
-store.
-
-The third kind of crate is the one that offers end-user functionality for a imag
-domain, for example "libimagtodo" provides functionality to track todos.
+## Crate types
-And last, but not least, the commandline frontend crates provide the user
-interface. These are the kind of crates that are not library crates, but
-binaries.
+There are different types of crates in the imag world:
+
+* "core" crates:
+ * libimagstore - The imag store is the abstraction over the filesystem. It
+ provides primitives to get, write and manipulate store entries and their
+ header information.
+ * libimagrt - The runtime library, which provides default argument parser
+ setup, interfacing with STDIN/STDOUT, configuration loading and parsing.
+ Simply put: It provides the runtime for a imag commandline application.
+ * libimagerror - Error handling library for handling errors the imag way. Used
+ in all other crates, even the store itself. It also offers functionality to
+ log and trace errors as well as exiting the application, if necessary.
+ (Note: This library might be removed in the future.)
+* "entry" crates:
+ "Entry" crates provide extensional functionality for the types from
+ libimagstore. For example, there is "libimagentrylink" which provides
+ functionality to link two entries in the store.
+* "domain" crates offer end-user functionality for a imag
+ domain, for example "libimagtodo" provides functionality to track todos.
+* "etc"/"util" crates for simple utilities.
+
+These are all libraries. There are also binary crates in the imag project
+(though they are technically _also_ library crates):
+
+* "core" binary crates, which implement core functionality of imag, such as
+ commandline interfaces for tagging, linking, ... entries as well as querying
+ them from the store and altering their data.
+* "domain" binary crates, which implement a domain such as "todo" handling or
+ "calendar" handling.
-Besides these, there are some other utility crates.
## Architecture of an imag module
With the things from above, a module could have the following architecture:
```
-+---------------------------------------------+
-| imag-foo |
+-----------------------------------+---------+
+| imag-foo | |
++-----------------------------------+ |
| libimagfoo | |
+-----------------+-----------------+ |
| | | |
@@ -52,35 +53,35 @@ With the things from above, a module could have the following architecture:
| | | lib |
+-----------------+-----------------+ |
| | |
-| ... | |
+| <more library crates> | |
| | imag |
+-----------------------------------+ |
| | |
-| libimagrt | |
-| | error |
-+-----------------------------------+ |
-| | |
-| libimagstore | |
+| libimagstore | rt |
| | |
+-----------------------------------+---------+
```
+External dependencies are not listed in this graphic.
+
The foundation of all imag modules is the store, as one can see in the
visualization from above.
-Above the store library there is the libimagrt, which provides the basic runtime
-and access to the `Store` object.
-Cross-cutting, there is the error library (and possibly
-the util library, but we do not care about this one here), which is used through
-all levels. The highest level of all imag modules is the commandline interface
-on top of the domain library. In between can be any number of entry extension
+Above the store level, entry libraries and domain libraries are used to
+implement functionality.
+The highest level of all imag modules is the commandline interface
+on top of the domain library. In between can be any number of entry extension
libraries, or none if not needed.
+libimagrt is used by the binary to construct the runtime, which itself
+constructs and initializes the Store, so this library is used in the full stack
+more or less.
Theoretically, the commandline interface crate could be replaced to build a
terminal user interface, graphical user interface or web interface.
+
## Types
-The imag core, hence the libimagstore, libimagrt and libimagerror, provide a set
+The imag core, hence the libimagstore and libimagrt, provide a set
of types that a user (as in a library writer) should be aware of.
First of all, there is the `Runtime` type which is provided by the libimagrt. It
@@ -103,10 +104,6 @@ not on the `FileLockEntry`.
The `Entry` provides access to its header, which is a `toml::Value`, where toml
is the toml-rs crate (external project). Convenience functionality is provided
-via the `toml-query` crate, which is an external project which was initiated and
+via the `toml-query` crate, an external project which was initiated and
extracted from the imag project.
-Error types are also important.
-All errors in imag projects should be created with `error-chain`.
-libimagerror provides functionality to enhance the experience with `Result`
-types and general tracing of errors.
diff --git a/doc/src/02000-store.md b/doc/src/02000-store.md
index 07334cea..cd22d688 100644
--- a/doc/src/02000-store.md
+++ b/doc/src/02000-store.md
@@ -42,33 +42,34 @@ The following section describe their purpose.
The header format is where imag stores its data. The header is an area at the
top of every file which is seperated from the content part by three dashes
(`---`). Between these three dashes there is structured data. imag uses `TOML`
-as data format for this structured data, because it fits best and the available
-`TOML` parser for the rust programming language is really good.
+as data format for this structured data.
The header can contain any amount of data, but modules (see @sec:modules) are
-restricted in their way of altering the data.
+restricted (by convention) in their way of altering the data.
-So normally there are several sections in the header. One section (`[imag]`) is
-always present. It contains a `version` field, which tells imag which version
-this file was created with.
+Normally there are several sections in the header. One section (`[imag]`) is
+always present, it is automatically created by the store and contains a
+`version` field, which tells imag which version this file was created with.
+The store automatically verifies that it is compatible (satisfying semver) with
+the version of imag an entry was created with, and if it is not, it fails
+loading the entry.
Other sections are named like the modules which created them. Every module is
allowed to store arbitrary data under its own section and a module may never
-read other sections than its own.
+read or write other sections than its own.
-These conventions are not enforced by imag itself, though.
### Content Format {#sec:thestore:fileformat:content}
The content is the part of the file where the user is free to enter any textual
content. The content may be rendered as Markdown or other markup format for the
-users convenience. The store does never expect and specific markup and actually
-the markup implementation is not inside the very core of imag.
+users convenience. The store does never expect any specific markup.
Technically it would be possible that the content part of a file is used to
store binary data.
We don't want this, though, as it is contrary to the goals of imag.
+
### Example {#sec:thestore:fileformat:example}
An example for a file in the store follows.
@@ -96,15 +97,13 @@ The "Entries" are stored as files in the "Store", which is a directory the
user has access to. The store may exist in the users Home-directory or any
other directory the user has read-write-access to.
-Each module stores its data in an own subdirectory in the store. This is because
-we like to keep things ordered and clean, not because it is technically
-necessary.
+Each module stores its data in an own subdirectory in the store, by convention.
-We name the path to a file in the store "Store id" or "Storepath" and we often
+The path to a file in the store is named "Store id" (or short "ID") and we
refer to it by using the store location as root.
-So if the store exists in `/home/user/store/`, a file with the storepath
+So if the store exists in `/home/user/.imag/store/`, a file with the storepath
`example.file` is (on the filesystem) located at
-`/home/user/store/example.file`.
+`/home/user/.imag/store/example.file`.
By convention, each `libimagentry<name>` and `libimag<name>` module stores its
entries in in `<name>/`.
diff --git a/doc/src/03010-conventions.md b/doc/src/03010-conventions.md
index 5e9ffe7e..77856353 100644
--- a/doc/src/03010-conventions.md
+++ b/doc/src/03010-conventions.md
@@ -47,7 +47,7 @@ Libraries which provide functionality for entries or the store but no
domain-functionality should be named "libimagentrything" whereas "thing" stands for
what the library provides.
-All domain libraries should be prefixed with "libimag".
+All domain libraries should be prefixed with "libimag".
### Library scope
@@ -93,7 +93,7 @@ possible without a lot of effort, but still: more tests = better!
The commandline tools are the CLI-frontends for their respective libraries.
So `libimagdiary` has a CLI frontend `imag-diary`.
-Those CLI frontends use functionality from `libimagrt` to build a
+Those CLI frontends use functionality from `libimagrt` to build a
commandline interface which is consistent with the rest of the ecosystem.
Commandline applications use the runtime interfaces for receiving IDs from the
diff --git a/doc/src/09010-contributing.md b/doc/src/09010-contributing.md
index 1978cccc..a86ae2e9 100644
--- a/doc/src/09010-contributing.md
+++ b/doc/src/09010-contributing.md
@@ -51,7 +51,7 @@ All dependencies are installable with the nix package manager by using a
## Commit guidelines
Make sure your patchset does not contain "Fixup" commits when publishing it, but feel
-free to send "Fixup" commits in the review process.
+free to send "Fixup" commits in the review process.
If squashing fails I will come back to you.
We do not follow some official Rust styleguide for our codebase, but we try to
diff --git a/doc/src/09020-changelog.md b/doc/src/09020-changelog.md
index 023086c4..c3299649 100644
--- a/doc/src/09020-changelog.md
+++ b/doc/src/09020-changelog.md
@@ -2,13 +2,6 @@
This section contains the changelog.
-We try to include a changelog line in each pull request, to make sure the
-changelog is up to date when releasing a new version of the codebase.
-Make sure to append the new change to the list, do not prepend it.
-
-The "Major" section of each section includes huge changes in functionality and
-interfaces (but not necessarily user-facing ones), whereas the "Minor" section
-contains only small stuff.
Some things, like typo fixes, version string updates and such are not stated in
the changelog (though updating of dependencies is).
Please note that we do not have a "Breaking changes" section as we are in
diff --git a/imagrc.toml b/imagrc.toml
index 0bd1c291..7c8947ad 100644
--- a/imagrc.toml
+++ b/imagrc.toml
@@ -374,3 +374,16 @@ calendars = "/home/user/calendars"
# The name of the mail reference collection
ref_collection_name = "mail"
+[todo]
+show_format = """
+{{i}} {{uuid}}
+
+Status: {{status}}
+Priority: {{prio}}
+Scheduled: {{sched}}
+Hidden: {{hidden}}
+Due: {{due}}
+
+{{text}}
+"""
+
diff --git a/tests/ui/src/imag_view.rs b/tests/ui/src/imag_view.rs
index 987eadc5..e7bfe5e4 100644
--- a/tests/ui/src/imag_view.rs
+++ b/tests/ui/src/imag_view.rs
@@ -17,3 +17,42 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
+use std::process::Command;
+
+use assert_cmd::prelude::*;
+use assert_fs::fixture::TempDir;
+use predicates::prelude::*;
+
+/// Helper to call imag-init
+pub fn call(tempdir: &TempDir, targets: &[&str]) -> Vec<String> {
+ let mut binary = binary(tempdir);
+
+ // ensure that stdin is not used by the child process
+ binary.stdin(std::process::Stdio::inherit());
+
+ binary.arg("--ignore-ids");
+
+ for target in targets.iter() {
+ binary.arg(target);
+ }
+
+ debug!("Command = {:?}", binary);
+ crate::imag::stdout_of_command(binary)
+}
+
+pub fn binary(tempdir: &TempDir) -> Command {
+ crate::imag::binary(tempdir, "imag-view")
+}
+
+#[test]
+fn test_view_empty_entry_shows_nothing() {
+ crate::setup_logging();
+ let imag_home = crate::imag::make_temphome();
+ crate::imag_init::call(&imag_home);
+ crate::imag_create::call(&imag_home, &["test"]);
+
+ let out = call(&imag_home, &["test"]);
+ debug!("out = '{:?}'", out);
+ assert!(out.iter().all(|s| s.is_empty()));
+}
+
diff --git a/tests/version-sync/Cargo.toml b/tests/version-sync/Cargo.toml
new file mode 100644
index 00000000..64f4a95a
--- /dev/null
+++ b/tests/version-sync/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "version-sync"
+version = "0.10.0"
+authors = ["Matthias Beyer <mail@beyermatthias.de>"]
+edition = "2018"
+
+# We do not publish this crate because it contains tests which should only be
+# available during development and CI runs, but there's nothing that a user of
+# any imag command might need, ever.
+publish = false
+
+[dev-dependencies]
+env_logger = "0.7"
+log = "0.4.6"
+version-sync = "0.8"
+walkdir = "2"
+toml-query = "0.9"
+toml = "0.5"
diff --git a/tests/version-sync/src/lib.rs b/tests/version-sync/src/lib.rs
new file mode 100644
index 00000000..b167c74a
--- /dev/null
+++ b/tests/version-sync/src/lib.rs
@@ -0,0 +1,116 @@
+//
+// imag - the personal information management suite for the commandline
+// Copyright (C) 2015-2019 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
+//
+
+#[cfg(test)] extern crate version_sync;
+#[cfg(test)] extern crate walkdir;
+#[cfg(test)] extern crate env_logger;
+#[cfg(test)] #[macro_use] extern crate log;
+#[cfg(test)] extern crate toml_query;
+#[cfg(test)] extern crate toml;
+
+#[cfg(test)] use std::path::PathBuf;
+
+#[cfg(test)]
+fn setup_logging() {
+ let _ = env_logger::try_init();
+}
+
+#[test]
+fn test_readme() {
+ version_sync::assert_markdown_deps_updated!("../../README.md");
+}
+
+#[test]
+fn test_doc() {
+ version_sync::assert_contains_regex!("../../doc/src/00000.md", "^version: {version}$");
+ version_sync::assert_contains_regex!("../../doc/src/02000-store.md", "^version = \"{version}\"$");
+ version_sync::assert_contains_regex!("../../doc/src/03020-writing-modules.md", "version = \"{version}\"");
+
+ version_sync::assert_contains_regex!("../../doc/user/src/approach.md", "^version = \"{version}\"$");
+}
+
+#[test]
+fn test_all_cargotoml_files() {
+ use toml::Value;
+
+ setup_logging();
+
+ let current_version = env!("CARGO_PKG_VERSION");
+ let imag_root = PathBuf::from(format!("{}/../../", env!("CARGO_MANIFEST_DIR")));
+ println!("imag_root = {}", imag_root.display());
+
+ walkdir::WalkDir::new(&imag_root)
+ .follow_links(false)
+ .into_iter()
+ .collect::<Result<Vec<_>, _>>()
+ .expect("Failed collecting files")
+ .into_iter()
+ .filter(|e| !e.path().to_str().unwrap().contains("target"))
+ .filter_map(|element| if element.file_type().is_file() && element.path().ends_with("Cargo.toml") {
+ debug!("Using = {:?}", element);
+ Some(element.into_path())
+ } else {
+ debug!("Ignoring = {:?}", element);
+ None
+ })
+ .for_each(|cargotoml| {
+ let filecontent = std::fs::read_to_string(&cargotoml).expect(&format!("Failed to read {}", cargotoml.display()));
+ let toml = filecontent.parse::<Value>().expect(&format!("Failed to parse toml: {}", cargotoml.display()));
+
+ match toml.get("dependencies") {
+ Some(Value::Table(ref tab)) => {
+ for (k, v) in tab.iter() {
+ if k.contains("libimag") {
+ match v {
+ Value::String(s) => assert!(s.contains(current_version)),
+ Value::Table(ref dep) => {
+ let version = dep.get("version").expect(&format!("No 'version' key for dependencies at {}", cargotoml.display()));
+ let version_str = version.as_str().unwrap();
+ assert!(version_str.contains(current_version), "failed for: {} ('{}')", cargotoml.display(), version_str)
+ },
+ _ => unimplemented!(),
+ }
+ }
+ }
+ },
+ Some(_) => panic!("Dependencies is not a table?"),
+ None => /* ignore if there is no "dependencies" */ {},
+ }
+
+ match toml.get("dev-dependencies") {
+ Some(Value::Table(ref tab)) => {
+ for (k, v) in tab.iter() {
+ if k.contains("libimag") {
+ match v {
+ Value::String(s) => assert!(s.contains(current_version)),
+ Value::Table(ref dep) => {
+ let version = dep.get("version").expect(&format!("No 'version' key for dev-dependencies at {}", cargotoml.display()));
+ let version_str = version.as_str().unwrap();
+ assert!(version_str.contains(current_version), "failed for: {} ('{}')", cargotoml.display(), version_str)
+ },
+ _ => unimplemented!(),
+ }
+ }
+ }
+ },
+ Some(_) => panic!("dev-dependencies is not a table?"),
+ None => /* ignore if there is no "dependencies" */ {},
+ }
+ });
+}