summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2017-11-09 12:15:46 +0100
committerGitHub <noreply@github.com>2017-11-09 12:15:46 +0100
commitb2377c3bc87bef681a85c1c0be1c286b065c389a (patch)
tree02e300bf91e7beba25c96ad3d6f10d0db00fbfd6
parent6efd0a9450d5d52d6f8e75a5c30ae48c0c8ec00e (diff)
parent5661dd8e35a0866615a17f4c20110426fb1f4430 (diff)
downloadimag-b2377c3bc87bef681a85c1c0be1c286b065c389a.zip
imag-b2377c3bc87bef681a85c1c0be1c286b065c389a.tar.gz
Merge pull request #1132 from matthiasbeyer/imag-contact/init
Imag contact/init
-rw-r--r--Cargo.toml1
-rw-r--r--bin/domain/imag-contact/Cargo.toml35
-rw-r--r--bin/domain/imag-contact/src/create.rs570
-rw-r--r--bin/domain/imag-contact/src/main.rs248
-rw-r--r--bin/domain/imag-contact/src/ui.rs93
-rw-r--r--bin/domain/imag-contact/src/util.rs124
-rw-r--r--bin/domain/imag-contact/static/new-contact-template-test.toml90
-rw-r--r--bin/domain/imag-contact/static/new-contact-template.toml90
-rw-r--r--doc/src/09020-changelog.md1
-rw-r--r--imagrc.toml61
-rw-r--r--lib/domain/libimagcontact/Cargo.toml2
11 files changed, 1314 insertions, 1 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 21f3a85..7a0e8b3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,7 @@ members = [
"bin/core/imag-tag",
"bin/core/imag-view",
"bin/domain/imag-bookmark",
+ "bin/domain/imag-contact",
"bin/domain/imag-diary",
"bin/domain/imag-mail",
"bin/domain/imag-notes",
diff --git a/bin/domain/imag-contact/Cargo.toml b/bin/domain/imag-contact/Cargo.toml
new file mode 100644
index 0000000..dbc4a64
--- /dev/null
+++ b/bin/domain/imag-contact/Cargo.toml
@@ -0,0 +1,35 @@
+[package]
+name = "imag-contact"
+version = "0.5.0"
+authors = ["Matthias Beyer <mail@beyermatthias.de>"]
+
+description = "Part of the imag core distribution: imag-contact command"
+
+keywords = ["imag", "PIM", "personal", "information", "management"]
+readme = "../../../README.md"
+license = "LGPL-2.1"
+
+documentation = "https://matthiasbeyer.github.io/imag/imag_documentation/index.html"
+repository = "https://github.com/matthiasbeyer/imag"
+homepage = "http://imag-pim.org"
+
+[dependencies]
+clap = ">=2.17"
+log = "0.3"
+version = "2.0.1"
+toml = "0.4"
+toml-query = "^0.3.1"
+handlebars = "0.29"
+vobject = "0.4"
+walkdir = "1"
+uuid = { version = "0.5", features = ["v4"] }
+
+libimagrt = { version = "0.5.0", path = "../../../lib/core/libimagrt" }
+libimagstore = { version = "0.5.0", path = "../../../lib/core/libimagstore" }
+libimagerror = { version = "0.5.0", path = "../../../lib/core/libimagerror" }
+libimagcontact = { version = "0.5.0", path = "../../../lib/domain/libimagcontact" }
+libimagutil = { version = "0.5.0", path = "../../../lib/etc/libimagutil" }
+libimagentryref = { version = "0.5.0", path = "../../../lib/entry/libimagentryref" }
+libimagentryedit = { version = "0.5.0", path = "../../../lib/entry/libimagentryedit" }
+libimaginteraction = { version = "0.5.0", path = "../../../lib/etc/libimaginteraction" }
+
diff --git a/bin/domain/imag-contact/src/create.rs b/bin/domain/imag-contact/src/create.rs
new file mode 100644
index 0000000..0614015
--- /dev/null
+++ b/bin/domain/imag-contact/src/create.rs
@@ -0,0 +1,570 @@
+//
+// imag - the personal information management suite for the commandline
+// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; version
+// 2.1 of the License.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+
+use std::collections::BTreeMap;
+use std::process::exit;
+use std::io::Write;
+use std::io::stdout;
+use std::path::PathBuf;
+use std::fs::OpenOptions;
+
+use vobject::vcard::Vcard;
+use vobject::write_component;
+use toml_query::read::TomlValueReadExt;
+use toml::Value;
+use uuid::Uuid;
+
+use libimagrt::runtime::Runtime;
+use libimagerror::trace::MapErrTrace;
+use libimagerror::trace::trace_error;
+use libimagerror::trace::trace_error_exit;
+use libimagutil::warn_result::WarnResult;
+use libimagentryref::refstore::RefStore;
+use libimagentryref::flags::RefFlags;
+
+const TEMPLATE : &'static str = include_str!("../static/new-contact-template.toml");
+
+#[cfg(test)]
+mod test {
+ use toml::Value;
+ use super::TEMPLATE;
+
+ const TEMPLATE_WITH_DATA : &'static str = include_str!("../static/new-contact-template-test.toml");
+
+ #[test]
+ fn test_validity_template_toml() {
+ let _ : Value = ::toml::de::from_str(TEMPLATE).unwrap();
+ }
+
+ #[test]
+ fn test_validity_template_toml_without_comments() {
+ let _ : Value = ::toml::de::from_str(TEMPLATE_WITH_DATA).unwrap();
+ }
+}
+
+macro_rules! ask_continue {
+ { yes => $yes:expr; no => $no:expr } => {
+ if ::libimaginteraction::ask::ask_bool("Edit tempfile", Some(true)) {
+ $yes
+ } else {
+ $no
+ }
+ };
+}
+
+pub fn create(rt: &Runtime) {
+ let scmd = rt.cli().subcommand_matches("create").unwrap();
+ let mut template = String::from(TEMPLATE);
+
+ let (mut dest, location) : (Box<Write>, Option<PathBuf>) = {
+ if let Some(mut fl) = scmd.value_of("file-location").map(PathBuf::from) {
+ if fl.is_file() {
+ error!("File does exist, cannot create/override");
+ exit(1);
+ } else if fl.is_dir() {
+ fl.push(Uuid::new_v4().hyphenated().to_string());
+ info!("Creating file: {:?}", fl);
+ }
+
+ debug!("Destination = {:?}", fl);
+
+ let file = OpenOptions::new()
+ .write(true)
+ .create_new(true)
+ .open(fl.clone())
+ .map_warn_err_str("Cannot create/open destination File. Stopping.")
+ .map_err_trace_exit_unwrap(1);
+
+ (Box::new(file), Some(fl))
+ } else {
+ (Box::new(stdout()), None)
+ }
+ };
+
+ loop {
+ ::libimagentryedit::edit::edit_in_tmpfile(&rt, &mut template)
+ .map_warn_err_str("Editing failed.")
+ .map_err_trace_exit_unwrap(1);
+
+ if template == TEMPLATE || template.is_empty() {
+ error!("No (changed) content in tempfile. Not doing anything.");
+ exit(2);
+ }
+
+ match ::toml::de::from_str(&template).map(parse_toml_into_vcard) {
+ Err(e) => {
+ error!("Error parsing template");
+ trace_error(&e);
+ ask_continue! { yes => continue; no => exit(1) };
+ },
+
+ Ok(None) => continue,
+ Ok(Some(vcard)) => {
+ if template == TEMPLATE || template.is_empty() {
+ if ::libimaginteraction::ask::ask_bool("Abort contact creating", Some(false)) {
+ exit(1);
+ } else {
+ continue;
+ }
+ }
+
+ let vcard_string = write_component(&vcard);
+ if let Err(e) = dest.write_all(&vcard_string.as_bytes()) {
+ warn!("Error while writing out vcard content");
+ trace_error_exit(&e, 1);
+ }
+
+ break;
+ }
+ }
+ }
+
+ if let Some(location) = location {
+ if !scmd.is_present("dont-track") {
+ let flags = RefFlags::default()
+ .with_content_hashing(true)
+ .with_permission_tracking(false);
+
+ RefStore::create(rt.store(), location, flags)
+ .map_err_trace_exit_unwrap(1);
+
+ info!("Created entry in store");
+ } else {
+ info!("Not creating entry in store");
+ }
+ } else {
+ info!("Cannot track stdout-created contact information");
+ }
+
+ info!("Ready");
+}
+
+fn parse_toml_into_vcard(toml: Value) -> Option<Vcard> {
+ let mut vcard = Vcard::default();
+
+ { // parse name
+ debug!("Parsing name");
+ let firstname = read_str_from_toml(&toml, "name.first");
+ trace!("firstname = {:?}", firstname);
+
+ let lastname = read_str_from_toml(&toml, "name.last");
+ trace!("lastname = {:?}", lastname);
+
+ vcard = vcard.with_name(parameters!(),
+ read_str_from_toml(&toml, "name.prefix"),
+ firstname.clone(),
+ read_str_from_toml(&toml, "name.additional"),
+ lastname.clone(),
+ read_str_from_toml(&toml, "name.suffix"));
+
+ if let (Some(first), Some(last)) = (firstname, lastname) {
+ trace!("Building fullname: '{} {}'", first, last);
+ vcard = vcard.with_fullname(format!("{} {}", first, last));
+ }
+ }
+
+ { // parse personal
+ debug!("Parsing person information");
+ let birthday = read_str_from_toml(&toml, "person.birthday");
+ trace!("birthday = {:?}", birthday);
+
+ if let Some(bday) = birthday {
+ vcard = vcard.with_bday(parameters!(), bday);
+ }
+ }
+
+ { // parse nicknames
+ debug!("Parsing nicknames");
+ match toml.read("nickname").map_err_trace_exit_unwrap(1) {
+ Some(&Value::Array(ref ary)) => {
+ for (i, element) in ary.iter().enumerate() {
+ let nicktype = match read_str_from_toml(element, "type") {
+ None => BTreeMap::new(),
+ Some(p) => {
+ let mut m = BTreeMap::new();
+ m.insert("TYPE".into(), p);
+ m
+ },
+ };
+
+ let name = match read_str_from_toml(element, "name") {
+ Some(p) => p,
+ None => {
+ error!("Key 'nickname.[{}].name' missing", i);
+ ask_continue! { yes => return None; no => exit(1) };
+ },
+ };
+
+ trace!("nick type = {:?}", nicktype);
+ trace!("name = {:?}", name);
+
+ vcard = vcard.with_nickname(nicktype, name);
+ }
+ },
+
+ Some(&Value::String(ref name)) => {
+ vcard = vcard.with_nickname(parameters!(), name.clone());
+ }
+
+ Some(_) => {
+ error!("Type Error: Expected Array or String at 'nickname'");
+ ask_continue! { yes => return None; no => exit(1) };
+ },
+ None => {
+ // nothing
+ },
+ }
+ }
+
+ { // parse organisation
+ debug!("Parsing organisation");
+
+ if let Some(orgs) = read_strary_from_toml(&toml, "organisation.name") {
+ trace!("orgs = {:?}", orgs);
+ vcard = vcard.with_org(orgs);
+ } else {
+ error!("Key 'organisation.name' missing");
+ ask_continue! { yes => return None; no => exit(1) };
+ }
+
+ if let Some(title) = read_str_from_toml(&toml, "organisation.title") {
+ trace!("title = {:?}", title);
+ vcard = vcard.with_title(title);
+ }
+
+ if let Some(role) = read_str_from_toml(&toml, "organisation.role") {
+ trace!("role = {:?}", role);
+ vcard = vcard.with_role(role);
+ }
+ }
+
+ { // parse phone
+ debug!("Parse phone");
+ match toml.read("person.phone").map_err_trace_exit_unwrap(1) {
+ Some(&Value::Array(ref ary)) => {
+ for (i, element) in ary.iter().enumerate() {
+ let phonetype = match read_str_from_toml(element, "type") {
+ Some(p) => p,
+ None => {
+ error!("Key 'phones.[{}].type' missing", i);
+ ask_continue! { yes => return None; no => exit(1) };
+ }
+ };
+
+ let number = match read_str_from_toml(element, "number") {
+ Some(p) => p,
+ None => {
+ error!("Key 'phones.[{}].number' missing", i);
+ ask_continue! { yes => return None; no => exit(1) };
+ }
+ };
+
+ trace!("phonetype = {:?}", phonetype);
+ trace!("number = {:?}", number);
+
+ vcard = vcard.with_tel(parameters!("TYPE" => phonetype), number);
+ }
+ },
+
+ Some(_) => {
+ error!("Expected Array at 'phones'.");
+ ask_continue! { yes => return None; no => exit(1) };
+ },
+ None => {
+ // nothing
+ },
+ }
+ }
+
+ { // parse address
+ debug!("Parsing address");
+ match toml.read("addresses").map_err_trace_exit_unwrap(1) {
+ Some(&Value::Array(ref ary)) => {
+ for (i, element) in ary.iter().enumerate() {
+ let adrtype = match read_str_from_toml(element, "type") {
+ None => {
+ error!("Key 'adresses.[{}].type' missing", i);
+ ask_continue! { yes => return None; no => exit(1) };
+ },
+ Some(p) => p,
+ };
+ trace!("adrtype = {:?}", adrtype);
+
+ let bx = read_str_from_toml(element, "box");
+ let extended = read_str_from_toml(element, "extended");
+ let street = read_str_from_toml(element, "street");
+ let code = read_str_from_toml(element, "code");
+ let city = read_str_from_toml(element, "city");
+ let region = read_str_from_toml(element, "region");
+ let country = read_str_from_toml(element, "country");
+
+ trace!("bx = {:?}", bx);
+ trace!("extended = {:?}", extended);
+ trace!("street = {:?}", street);
+ trace!("code = {:?}", code);
+ trace!("city = {:?}", city);
+ trace!("region = {:?}", region);
+ trace!("country = {:?}", country);
+
+ vcard = vcard.with_adr(
+ parameters!("TYPE" => adrtype),
+ bx, extended, street, code, city, region, country
+ );
+ }
+ },
+
+ Some(_) => {
+ error!("Type Error: Expected Array at 'addresses'");
+ ask_continue! { yes => return None; no => exit(1) };
+ },
+ None => {
+ // nothing
+ },
+ }
+ }
+
+ { // parse email
+ debug!("Parsing email");
+ match toml.read("person.email").map_err_trace_exit_unwrap(1) {
+ Some(&Value::Array(ref ary)) => {
+ for (i, element) in ary.iter().enumerate() {
+ let mailtype = match read_str_from_toml(element, "type") {
+ None => {
+ error!("Error: 'email.[{}].type' missing", i);
+ ask_continue! { yes => return None; no => exit(1) };
+ },
+ Some(p) => p,
+ }; // TODO: Unused, because unsupported by vobject
+
+ let mail = match read_str_from_toml(element, "addr") {
+ None => {
+ error!("Error: 'email.[{}].addr' missing", i);
+ ask_continue! { yes => return None; no => exit(1) };
+ },
+ Some(p) => p,
+ };
+
+ trace!("mailtype = {:?} (UNUSED)", mailtype);
+ trace!("mail = {:?}", mail);
+
+ vcard = vcard.with_email(mail);
+ }
+ },
+
+ Some(_) => {
+ error!("Type Error: Expected Array at 'email'");
+ ask_continue! { yes => return None; no => exit(1) };
+ },
+ None => {
+ // nothing
+ },
+ }
+ }
+
+ { // parse others
+ debug!("Parsing others");
+ 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") {
+ vcard = vcard.with_url(webpage);
+ } else {
+ debug!("No webpage");
+ }
+
+ if let Some(note) = read_str_from_toml(&toml, "other.note") {
+ vcard = vcard.with_note(note);
+ } else {
+ debug!("No note");
+ }
+
+ }
+
+ Some(vcard)
+}
+
+fn read_strary_from_toml(toml: &Value, path: &'static str) -> Option<Vec<String>> {
+ match toml.read(path).map_warn_err_str(&format!("Failed to read value at '{}'", path)) {
+ Ok(Some(&Value::Array(ref vec))) => {
+ let mut v = Vec::new();
+ for elem in vec {
+ match *elem {
+ Value::String(ref s) => v.push(s.clone()),
+ _ => {
+ error!("Type Error: '{}' must be Array<String>", path);
+ return None
+ },
+ }
+ }
+
+ Some(v)
+ }
+ Ok(Some(&Value::String(ref s))) => {
+ warn!("Having String, wanting Array<String> ... going to auto-fix");
+ Some(vec![s.clone()])
+ },
+ Ok(Some(_)) => {
+ error!("Type Error: '{}' must be Array<String>", path);
+ None
+ },
+ Ok(None) => None,
+ Err(_) => None,
+ }
+}
+
+fn read_str_from_toml(toml: &Value, path: &'static str) -> Option<String> {
+ let v = toml.read(path)
+ .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
+ },
+ Ok(None) => {
+ error!("Expected '{}' to be present, but is not.", path);
+ None
+ },
+ Err(e) => {
+ trace_error(&e);
+ None
+ }
+ }
+}
+
+#[cfg(test)]
+mod test_parsing {
+ use super::parse_toml_into_vcard;
+
+ // TODO
+ const TEMPLATE : &'static str = include_str!("../static/new-contact-template-test.toml");
+
+ #[test]
+ fn test_template_names() {
+ let vcard = parse_toml_into_vcard(::toml::de::from_str(TEMPLATE).unwrap());
+ assert!(vcard.is_some(), "Failed to parse test template.");
+ let vcard = vcard.unwrap();
+
+ assert!(vcard.name().is_some());
+
+ assert_eq!(vcard.name().unwrap().surname().unwrap(), "test");
+ assert_eq!(vcard.name().unwrap().given_name().unwrap(), "test");
+ assert_eq!(vcard.name().unwrap().additional_names().unwrap(), "test");
+ assert_eq!(vcard.name().unwrap().honorific_prefixes().unwrap(), "test");
+ assert_eq!(vcard.name().unwrap().honorific_suffixes().unwrap(), "test");
+ }
+
+ #[test]
+ fn test_template_person() {
+ let vcard = parse_toml_into_vcard(::toml::de::from_str(TEMPLATE).unwrap());
+ assert!(vcard.is_some(), "Failed to parse test template.");
+ let vcard = vcard.unwrap();
+
+ assert!(vcard.bday().is_some());
+
+ assert_eq!(vcard.bday().unwrap().raw(), "2017-01-01");
+
+ assert_eq!(vcard.nickname().len(), 1);
+ assert_eq!(vcard.nickname()[0].raw(), "boss");
+
+ // TODO: parameters() not yet implemented in underlying API
+ // assert!(vcard.nickname()[0].parameters().contains_key("work"));
+ }
+
+ #[test]
+ fn test_template_organization() {
+ let vcard = parse_toml_into_vcard(::toml::de::from_str(TEMPLATE).unwrap());
+ assert!(vcard.is_some(), "Failed to parse test template.");
+ let vcard = vcard.unwrap();
+
+ assert_eq!(vcard.org().len(), 1);
+ assert_eq!(vcard.org()[0].raw(), "test");
+
+ assert_eq!(vcard.title().len(), 1);
+ assert_eq!(vcard.title()[0].raw(), "test");
+
+ assert_eq!(vcard.role().len(), 1);
+ assert_eq!(vcard.role()[0].raw(), "test");
+ }
+
+ #[test]
+ fn test_template_phone() {
+ let vcard = parse_toml_into_vcard(::toml::de::from_str(TEMPLATE).unwrap());
+ assert!(vcard.is_some(), "Failed to parse test template.");
+ let vcard = vcard.unwrap();
+
+ assert_eq!(vcard.tel().len(), 1);
+ assert_eq!(vcard.tel()[0].raw(), "0123 123456789");
+
+ // TODO: parameters() not yet implemented in underlying API
+ // assert!(vcard.tel()[0].parameters().contains_key("type"));
+ // assert_eq!(vcard.tel()[0].parameters().get("type").unwrap(), "home");
+ }
+
+ #[test]
+ fn test_template_email() {
+ let vcard = parse_toml_into_vcard(::toml::de::from_str(TEMPLATE).unwrap());
+ assert!(vcard.is_some(), "Failed to parse test template.");
+ let vcard = vcard.unwrap();
+
+ assert_eq!(vcard.email().len(), 1);
+ assert_eq!(vcard.email()[0].raw(), "examle@examplemail.org");
+
+ // TODO: parameters() not yet implemented in underlying API
+ // assert!(vcard.email()[0].parameters().contains_key("type"));
+ // assert_eq!(vcard.email()[0].parameters().get("type").unwrap(), "home");
+ }
+
+ #[test]
+ fn test_template_addresses() {
+ let vcard = parse_toml_into_vcard(::toml::de::from_str(TEMPLATE).unwrap());
+ assert!(vcard.is_some(), "Failed to parse test template.");
+ let vcard = vcard.unwrap();
+
+ assert_eq!(vcard.adr().len(), 1);
+ assert_eq!(vcard.adr()[0].raw(), "testbox;testextended;teststreet;testcode;testcity;testregion;testcountry");
+
+ // TODO: parameters() not yet implemented in underlying API
+ //for e in &["box", "extended", "street", "code", "city", "region", "country"] {
+ // assert!(vcard.adr()[0].parameters().contains_key(e));
+ // assert_eq!(vcard.adr()[0].parameters().get(e).unwrap(), "test");
+ //}
+ }
+
+ #[test]
+ fn test_template_other() {
+ let vcard = parse_toml_into_vcard(::toml::de::from_str(TEMPLATE).unwrap());
+ assert!(vcard.is_some(), "Failed to parse test template.");
+ let vcard = vcard.unwrap();
+
+ assert_eq!(vcard.categories().len(), 1);
+ assert_eq!(vcard.categories()[0].raw(), "test");
+
+ assert_eq!(vcard.url().len(), 1);
+ assert_eq!(vcard.url()[0].raw(), "test");
+
+ assert_eq!(vcard.note().len(), 1);
+ assert_eq!(vcard.note()[0].raw(), "test");
+ }
+}
+
diff --git a/bin/domain/imag-contact/src/main.rs b/bin/domain/imag-contact/src/main.rs
new file mode 100644
index 0000000..505ef91
--- /dev/null
+++ b/bin/domain/imag-contact/src/main.rs
@@ -0,0 +1,248 @@
+//
+// imag - the personal information management suite for the commandline
+// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; version
+// 2.1 of the License.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+
+#![deny(
+ non_camel_case_types,
+ non_snake_case,
+ path_statements,
+ trivial_numeric_casts,
+ unstable_features,
+ unused_allocation,
+ unused_import_braces,
+ unused_imports,
+ unused_must_use,
+ unused_mut,
+ unused_qualifications,
+ while_true,
+)]
+
+extern crate clap;
+#[macro_use] extern crate log;
+#[macro_use] extern crate version;
+#[macro_use] extern crate vobject;
+extern crate toml;
+extern crate toml_query;
+extern crate handlebars;
+extern crate walkdir;
+extern crate uuid;
+
+extern crate libimagcontact;
+extern crate libimagstore;
+extern crate libimagrt;
+extern crate libimagerror;
+extern crate libimagutil;
+extern crate libimaginteraction;
+extern crate libimagentryref;
+extern crate libimagentryedit;
+
+use std::process::exit;
+use std::path::PathBuf;
+
+use handlebars::Handlebars;
+use clap::ArgMatches;
+use vobject::vcard::Vcard;
+use toml_query::read::TomlValueReadExt;
+use toml::Value;
+use walkdir::WalkDir;
+
+use libimagrt::runtime::Runtime;
+use libimagrt::setup::generate_runtime_setup;
+use libimagerror::trace::MapErrTrace;
+use libimagcontact::store::ContactStore;
+use libimagcontact::error::ContactError as CE;
+use libimagcontact::contact::Contact;
+use libimagstore::iter::get::StoreIdGetIteratorExtension;
+use libimagentryref::reference::Ref;
+use libimagentryref::refstore::RefStore;
+
+mod ui;
+mod util;
+mod create;
+
+use ui::build_ui;
+use util::build_data_object_for_handlebars;
+use create::create;
+
+fn main() {
+ let rt = generate_runtime_setup("imag-contact",
+ &version!()[..],
+ "Contact management tool",
+ build_ui);
+
+
+ rt.cli()
+ .subcommand_name()
+ .map(|name| {
+ debug!("Call {}", name);
+ match name {
+ "list" => list(&rt),
+ "import" => import(&rt),
+ "show" => show(&rt),
+ "create" => create(&rt),
+ _ => {
+ error!("Unknown command"); // More error handling
+ },
+ }
+ });
+}
+
+fn list(rt: &Runtime) {
+ let scmd = rt.cli().subcommand_matches("list").unwrap();
+ let list_format = get_contact_print_format("contact.list_format", rt, &scmd);
+
+ let _ = rt
+ .store()
+ .all_contacts()
+ .map_err_trace_exit(1)
+ .unwrap() // safed by above call
+ .into_get_iter(rt.store())
+ .map(|fle| {
+ let fle = fle
+ .map_err_trace_exit(1)
+ .unwrap()
+ .ok_or_else(|| CE::from("StoreId not found".to_owned()))
+ .map_err_trace_exit(1)
+ .unwrap();
+
+ fle
+ .get_contact_data()
+ .map(|cd| (fle, cd))
+ .map(|(fle, cd)| (fle, cd.into_inner()))
+ .map(|(fle, cd)| (fle, Vcard::from_component(cd)))
+ .map_err_trace_exit(1)
+ .unwrap()
+ })
+ .enumerate()
+ .map(|(i, (fle, vcard))| {
+ let hash = fle.get_path_hash().map_err_trace_exit(1).unwrap();
+ let vcard = vcard.unwrap_or_else(|e| {
+ error!("Element is not a VCARD object: {:?}", e);
+ exit(1)
+ });
+
+ let data = build_data_object_for_handlebars(i, hash, &vcard);
+
+ let s = list_format.render("format", &data)
+ .map_err_trace_exit(1)
+ .unwrap();
+ println!("{}", s);
+ })
+ .collect::<Vec<_>>();
+}
+
+fn import(rt: &Runtime) {
+ let scmd = rt.cli().subcommand_matches("import").unwrap(); // secured by main
+ let path = scmd.value_of("path").map(PathBuf::from).unwrap(); // secured by clap
+
+ if !path.exists() {
+ error!("Path does not exist");
+ exit(1)
+ }
+
+ if path.is_file() {
+ let _ = rt
+ .store()
+ .create_from_path(&path)
+ .map_err_trace_exit(1)
+ .unwrap();
+ } else if path.is_dir() {
+ for entry in WalkDir::new(path).min_depth(1).into_iter() {
+ let entry = entry.map_err_trace_exit(1).unwrap();
+ if entry.file_type().is_file() {
+ let pb = PathBuf::from(entry.path());
+ let _ = rt
+ .store()
+ .create_from_path(&pb)
+ .map_err_trace_exit(1)
+ .unwrap();
+ 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>"));
+ }
+ }
+ } else {
+ error!("Path is neither directory nor file");
+ exit(1)
+ }
+}
+
+fn show(rt: &Runtime) {
+ let scmd = rt.cli().subcommand_matches("show").unwrap();
+ let hash = scmd.value_of("hash").map(String::from).unwrap(); // safed by clap
+
+ let contact_data = rt.store()
+ .get_by_hash(hash.clone())
+ .map_err_trace_exit(1)
+ .unwrap()
+ .ok_or(CE::from(format!("No entry for hash {}", hash)))
+ .map_err_trace_exit(1)
+ .unwrap()
+ .get_contact_data()
+ .map_err_trace_exit(1)
+ .unwrap()
+ .into_inner();
+ let vcard = Vcard::from_component(contact_data)
+ .unwrap_or_else(|e| {
+ error!("Element is not a VCARD object: {:?}", e);
+ exit(1)
+ });
+
+ let show_format = get_contact_print_format("contact.show_format", rt, &scmd);
+ let data = build_data_object_for_handlebars(0, hash, &vcard);
+
+ let s = show_format.render("format", &data)
+ .map_err_trace_exit(1)
+ .unwrap();
+ println!("{}", s);
+ info!("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(|| CE::from("No configuration file".to_owned()))
+ .map_err_trace_exit(1)
+ .unwrap()
+ .read(config_value_path)
+ .map_err_trace_exit(1)
+ .unwrap()
+ .ok_or_else(|| CE::from("Configuration 'contact.list_format' does not exist".to_owned()))
+ .and_then(|value| match *value {
+ Value::String(ref s) => Ok(s.clone()),
+ _ => Err(CE::from("Type error: Expected String at 'contact.list_format'. Have non-String".to_owned()))
+ })
+ .map_err_trace_exit(1)
+ .unwrap()
+ });
+
+ let mut hb = Handlebars::new();
+ let _ = hb
+ .register_template_string("format", fmt)
+ .map_err_trace_exit(1)
+ .unwrap();
+
+ hb.register_escape_fn(::handlebars::no_escape);
+ ::libimaginteraction::format::register_all_color_helpers(&mut hb);
+ ::libimaginteraction::format::register_all_format_helpers(&mut hb);
+ hb
+}
+
diff --git a/bin/domain/imag-contact/src/ui.rs b/bin/domain/imag-contact/src/ui.rs
new file mode 100644
index 0000000..f3442a0
--- /dev/null
+++ b/bin/domain/imag-contact/src/ui.rs
@@ -0,0 +1,93 @@
+//
+// imag - the personal information management suite for the commandline
+// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; version
+// 2.1 of the License.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+
+use clap::{Arg, App, SubCommand};
+
+pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
+ app
+ .subcommand(SubCommand::with_name("list")
+ .about("List contacts")
+ .version("0.1")
+ .arg(Arg::with_name("filter")
+ .index(1)
+ .takes_value(true)
+ .required(false)
+ .multiple(true)
+ .value_name("FILTER")
+ .help("Filter by these properties (not implemented yet)"))
+ .arg(Arg::with_name("format")
+ .long("format")
+ .takes_value(true)
+ .required(false)
+ .multiple(false)
+ .value_name("FORMAT")
+ .help("Format to format the listing"))
+ )
+
+ .subcommand(SubCommand::with_name("import")
+ .about("Import contacts")
+ .version("0.1")
+ .arg(Arg::with_name("path")
+ .index(1)
+ .takes_value(true)
+ .required(true)
+ .multiple(false)
+ .value_name("PATH")
+ .help("Import from this file/directory"))
+ )
+
+ .subcommand(SubCommand::with_name("show")
+ .about("Show contact")
+ .version("0.1")
+ .arg(Arg::with_name("hash")
+ .index(1)
+ .takes_value(true)
+ .required(true)
+ .multiple(false)
+ .value_name("HASH")
+ .help("Show the contact pointed to by this reference hash"))
+ .arg(Arg::with_name("format")
+ .long("format")
+ .takes_value(true)
+ .required(false)
+ .multiple(false)
+ .value_name("FORMAT")
+ .help("Format to format the contact when printing it"))
+ )
+
+ .subcommand(SubCommand::with_name("create")
+ .about("Create a contact file (.vcf) and track it in imag.")
+ .version("0.1")
+ .arg(Arg::with_name("file-location")
+ .short("F")
+ .long("file")
+ .takes_value(true)
+ .required(false)
+ .multiple(false)
+ .value_name("PATH")
+ .help("Create this file. If a directory is passed, a file with a uuid as name will be created. vcf contents are dumped to stdout if this is not passed."))
+ .arg(Arg::with_name("dont-track")
+ .short("T")
+ .long("no-track")
+ .takes_value(false)
+ .required(false)
+ .multiple(false)
+ .help("Don't track the new vcf file if one is created."))
+ )
+}
diff --git a/bin/domain/imag-contact/src/util.rs b/bin/domain/imag-contact/src/util.rs
new file mode 100644
index 0000000..e85a6a8
--- /dev/null
+++ b/bin/domain/imag-contact/src/util.rs
@@ -0,0 +1,124 @@
+//
+// imag - the personal information management suite for the commandline
+// Copyright (C) 2015, 2016 Matthias Beyer <mail@beyermatthias.de> and contributors
+//
+// This library is free software; you can redistribute it and/or
+// modify it under the terms of the GNU Lesser General Public
+// License as published by the Free Software Foundation; version
+// 2.1 of the License.
+//
+// This library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+// Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public
+// License along with this library; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+//
+
+use std::collections::BTreeMap;
+use vobject::vcard::Vcard;
+
+pub fn build_data_object_for_handlebars<'a>(i: usize, hash: String, vcard: &Vcard) -> BTreeMap<&'static str, String> {
+ let mut data = BTreeMap::new();
+ {
+ data.insert("i" , format!("{}", i));
+
+ /// The hash (as in libimagentryref) of the contact
+ data.insert("id" , hash);
+
+ data.insert("ADR" , vcard.adr()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("ANNIVERSARY" , vcard.anniversary()
+ .map(|c| c.raw().clone()).unwrap_or(String::new()));
+
+ data.insert("BDAY" , vcard.bday()
+ .map(|c| c.raw().clone()).unwrap_or(String::new()));
+
+ data.insert("CATEGORIES" , vcard.categories()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("CLIENTPIDMAP" , vcard.clientpidmap()
+ .map(|c| c.raw().clone()).unwrap_or(String::new()));
+
+ data.insert("EMAIL" , vcard.email()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("FN" , vcard.fullname()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("GENDER" , vcard.gender()
+ .map(|c| c.raw().clone()).unwrap_or(String::new()));
+
+ data.insert("GEO" , vcard.geo()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("IMPP" , vcard.impp()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("KEY" , vcard.key()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("LANG" , vcard.lang()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("LOGO" , vcard.logo()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("MEMBER" , vcard.member()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("N" , vcard.name()
+ .map(|c| c.raw().clone()).unwrap_or(String::new()));
+
+ data.insert("NICKNAME" , vcard.nickname()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("NOTE" , vcard.note()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("ORG" , vcard.org()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("PHOTO" , vcard.photo()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("PRIOD" , vcard.proid()
+ .map(|c| c.raw().clone()).unwrap_or(String::new()));
+
+ data.insert("RELATED" , vcard.related()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("REV" , vcard.rev()
+ .map(|c| c.raw().clone()).unwrap_or(String::new()));
+
+ data.insert("ROLE" , vcard.role()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("SOUND" , vcard.sound()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("TEL" , vcard.tel()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("TITLE" , vcard.title()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("TZ" , vcard.tz()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("UID" , vcard.uid()
+ .map(|c| c.raw().clone()).unwrap_or(String::new()));
+
+ data.insert("URL" , vcard.url()
+ .into_iter().map(|c| c.raw().clone()).collect());
+
+ data.insert("VERSION" , vcard.version()
+ .map(|c| c.raw().clone()).unwrap_or(String::new()));
+ }
+
+ data
+}
+
diff --git a/bin/domain/imag-contact/static/new-contact-template-test.toml b/bin/domain/imag-contact/static/new-contact-template-test.toml
new file mode 100644
index 0000000..a7791e0
--- /dev/null
+++ b/bin/domain/imag-contact/static/new-contact-template-test.toml
@@ -0,0 +1,90 @@
+# Contact template for imag-contact version 0.5.0
+#
+# This file is explicitely _not_ distributed under the terms of the original imag license, but
+# public domain.
+#
+# Use this TOML formatted template to create a new contact.
+
+[name]
+
+# every entry may contain a string or a list of strings
+# E.G.:
+# first = "Foo"
+# last = [ "bar", "bar", "a" ]
+prefix = "test"
+first = "test"
+additional = "test"
+last = "test"
+suffix = "test"
+
+[person]
+
+# Birthday
+# Format: YYYY-MM-DD
+birthday = "2017-01-01"
+
+# allowed types:
+# vcard 3.0: At least one of bbs, car, cell, fax, home, isdn, msg, modem,
+# pager, pcs, pref, video, voice, work
+# vcard 4.0: At least one of home, work, pref, text, voice, fax, cell, video,
+# pager, textphone
+phone = [
+ { "type" = "home", "number" = "0123 123456789" },
+]
+
+#
+# Email addresses
+#
+email = [
+ { "type" = "home", "addr" = "examle@examplemail.org" },
+]
+
+# post addresses
+#
+# allowed types:
+# vcard 3.0: At least one of dom, intl, home, parcel, postal, pref, work
+# vcard 4.0: At least one of home, pref, work
+[[addresses]]
+type = "home"
+box = "testbox"
+extended = "testextended"
+street = "teststreet"
+code = "testcode"
+city = "testcity"
+region = "testregion"
+country = "testcountry"
+
+# Nickname
+# "type" is optional
+[[nickname]]
+type = "work"
+name = "boss"
+
+[organisation]
+
+# Organisation name
+# May contain a string or a list of strings
+name = "test"
+
+# Organisation title and role
+# May contain a string or a list of strings
+title = "test"
+
+# Role at organisation
+# May contain a string or a list of strings
+role = "test"
+
+[other]
+
+# categories or tags
+# May contain a string or a list of strings
+categories = "test"
+
+# Web pages
+# May contain a string or a list of strings
+webpage = "test"
+
+# Notes
+# May contain a string or a list of strings
+note = "test"
+
diff --git a/bin/domain/imag-contact/static/new-contact-template.toml b/bin/domain/imag-contact/static/new-contact-template.toml
new file mode 100644
index 0000000..04f13af
--- /dev/null
+++ b/bin/domain/imag-contact/static/new-contact-template.toml
@@ -0,0 +1,90 @@
+# Contact template for imag-contact version 0.5.0
+#
+# This file is explicitely _not_ distributed under the terms of the original imag license, but
+# public domain.
+#
+# Use this TOML formatted template to create a new contact.
+
+[name]
+
+# every entry may contain a string or a list of strings
+# E.G.:
+# first = "Foo"
+# last = [ "bar", "bar", "a" ]
+#prefix = ""
+first = ""
+#additional = ""
+last = ""
+#suffix = ""
+
+[person]
+
+# Birthday
+# Format: YYYY-MM-DD
+#birthday = ""
+
+# allowed types:
+# vcard 3.0: At least one of bbs, car, cell, fax, home, isdn, msg, modem,
+# pager, pcs, pref, video, voice, work
+# vcard 4.0: At least one of home, work, pref, text, voice, fax, cell, video,
+# pager, textphone
+#phone = [
+# { "type" = "home", "number" = "0123 123456789" },
+#]
+
+#
+# Email addresses
+#
+#email = [
+# { "type" = "home", "addr" = "examle@examplemail.org" },
+#]
+
+# post addresses
+#
+# allowed types:
+# vcard 3.0: At least one of dom, intl, home, parcel, postal, pref, work
+# vcard 4.0: At least one of home, pref, work
+#[[addresses]]
+#type = "home"
+#box = ""
+#extended = ""
+#street = ""
+#code = ""
+#city = ""
+#region = ""
+#country = ""
+
+# Nickname
+# "type" is optional
+#[[nickname]]
+#type = "work"
+#name = "boss"
+
+[organisation]
+
+# Organisation name
+# May contain a string or a list of strings
+#name = ""
+
+# Organisation title and role
+# May contain a string or a list of strings
+#title = ""
+
+# Role at organisation
+# May contain a string or a list of strings
+#role = ""
+
+[other]
+
+# categories or tags
+# May contain a string or a list of strings
+#categories = ""
+
+# Web pages
+# May contain a string or a list of strings
+#webpage = ""
+
+# Notes
+# May contain a string or a list of strings
+#note = ""
+
diff --git a/doc/src/09020-changelog.md b/doc/src/09020-changelog.md
index d6e60eb..a0004df 100644
--- a/doc/src/09020-changelog.md
+++ b/doc/src/09020-changelog.md
@@ -33,6 +33,7 @@ This section contains the changelog from the last release to the next release.
* The runtime does not read the config file for editor settings anymore.
Specifying an editor either via CLI or via the `$EDITOR` environment
variable still possible.
+ * `imag-contact` was added (with basic contact support so far).
* Minor changes
* `libimagentryannotation` got a rewrite, is not based on `libimagnotes`
diff --git a/imagrc.toml b/imagrc.toml
index 9318446..6700607 100644
--- a/imagrc.toml
+++ b/imagrc.toml
@@ -255,3 +255,64 @@ default_collection = "default"
editor = "vim -R {{entry}}"
web = "chromium {{entry}}"
+[contact]
+
+# Format for listing contacts
+#
+# Available variables:
+# * "i" : Integer, counts the output lines
+# * "id" : The hash which can be used to print the entry itself.
+# * "ADR" : Array
+# * "ANNIVERSARY" : String
+# * "BDAY" : String
+# * "CATEGORIES" : Array<String>
+# * "CLIENTPIDMAP" : String
+# * "EMAIL" : Array<String>
+# * "FN" : Array<String>
+# * "GENDER" : String
+# * "GEO" : Array<String>
+# * "IMPP" : Array<String>
+# * "KEY" : Array<String>
+# * "LANG" : Array<String>
+# * "LOGO" : Array<String>
+# * "MEMBER" : Array<String>
+# * "N" : String
+# * "NICKNAME" : Array<String>
+# * "NOTE" : Array<String>
+# * "ORG" : Array<String>
+# * "PHOTO" : Array<String>
+# * "PRIOD" : String
+# * "RELATED" : Array<String>
+# * "REV" : String
+# * "ROLE" : Array<String>
+# * "SOUND" : Array<String>
+# * "TEL" : Array<String>
+# * "TITLE" : Array<String>
+# * "TZ" : Array<String>
+# * "UID" : String
+# * "URL" : Array<String>
+# * "VERSION" : String
+#
+# Multiple lines shouldn't be used, as this is for listing multiple entries.
+#
+# Note: Abbreviating the hash ("id") is not yet supported in the "show" command,
+# thus we print the id here without abbreviating it. To abbreviate it to 5
+# characters, use:
+#
+# {{abbrev 5 id}}
+#
+list_format = "{{lpad 5 i}} | {{id}} | {{FN}} | {{mail}} | {{adr}}"
+
+# The format when printing a single contact
+#
+# Here, the same rules like for the list format apply.
+# Multiple lines should work fine.
+# The "i" variable defaults to zero (0)
+show_format = """
+{{id}} - {{UID}}
+
+Full name: {{FN}}
+Email : {{EMAIL}}
+Address : {{ADR}}
+"""
+
diff --git a/lib/domain/libimagcontact/Cargo.toml b/lib/domain/libimagcontact/Cargo.toml
index acc420a..17a2d43 100644
--- a/lib/domain/libimagcontact/Cargo.toml
+++ b/lib/domain/libimagcontact/Cargo.toml
@@ -18,7 +18,7 @@ error-chain = "0.11"
log = "0.3"
toml = "0.4"
toml-query = "0.4"
-vobject = { git = 'https://github.com/matthiasbeyer/rust-vobject', branch = "next" }
+vobject = "0.4"
libimagstore = { version = "0.5.0", path = "../../../lib/core/libimagstore" }
libimagerror = { version = "0.5.0", path = "../../../lib/core/libimagerror" }