diff --git a/Cargo.toml b/Cargo.toml index 21f3a85e..7a0e8b3a 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 00000000..dbc4a64b --- /dev/null +++ b/bin/domain/imag-contact/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "imag-contact" +version = "0.5.0" +authors = ["Matthias Beyer "] + +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 00000000..06140159 --- /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 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, Option) = { + 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 { + 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> { + 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", path); + return None + }, + } + } + + Some(v) + } + Ok(Some(&Value::String(ref s))) => { + warn!("Having String, wanting Array ... going to auto-fix"); + Some(vec![s.clone()]) + }, + Ok(Some(_)) => { + error!("Type Error: '{}' must be Array", path); + None + }, + Ok(None) => None, + Err(_) => None, + } +} + +fn read_str_from_toml(toml: &Value, path: &'static str) -> Option { + 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 00000000..505ef918 --- /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 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::>(); +} + +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("")); + } else { + warn!("Ignoring non-file: {}", entry.path().to_str().unwrap_or("")); + } + } + } 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 00000000..f3442a09 --- /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 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 00000000..e85a6a8a --- /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 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 00000000..a7791e05 --- /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 00000000..04f13af2 --- /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 d6e60ebe..a0004df1 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 93184460..67006073 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 +# * "CLIENTPIDMAP" : String +# * "EMAIL" : Array +# * "FN" : Array +# * "GENDER" : String +# * "GEO" : Array +# * "IMPP" : Array +# * "KEY" : Array +# * "LANG" : Array +# * "LOGO" : Array +# * "MEMBER" : Array +# * "N" : String +# * "NICKNAME" : Array +# * "NOTE" : Array +# * "ORG" : Array +# * "PHOTO" : Array +# * "PRIOD" : String +# * "RELATED" : Array +# * "REV" : String +# * "ROLE" : Array +# * "SOUND" : Array +# * "TEL" : Array +# * "TITLE" : Array +# * "TZ" : Array +# * "UID" : String +# * "URL" : Array +# * "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 acc420aa..17a2d433 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" }