diff --git a/bin/domain/imag-contact/Cargo.toml b/bin/domain/imag-contact/Cargo.toml index 1dac6415..e4251fed 100644 --- a/bin/domain/imag-contact/Cargo.toml +++ b/bin/domain/imag-contact/Cargo.toml @@ -19,8 +19,15 @@ log = "0.3" version = "2.0.1" toml = "0.4" toml-query = "^0.3.1" +handlebars = "0.29" +vobject = { git = 'https://github.com/matthiasbeyer/rust-vobject', branch = "next" } +walkdir = "1" 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" } +libimaginteraction = { version = "0.5.0", path = "../../../lib/etc/libimaginteraction" } + diff --git a/bin/domain/imag-contact/src/main.rs b/bin/domain/imag-contact/src/main.rs index cb320502..014c5e48 100644 --- a/bin/domain/imag-contact/src/main.rs +++ b/bin/domain/imag-contact/src/main.rs @@ -37,19 +37,38 @@ extern crate clap; #[macro_use] extern crate version; extern crate toml; extern crate toml_query; +extern crate handlebars; +extern crate vobject; +extern crate walkdir; extern crate libimagcontact; +extern crate libimagstore; extern crate libimagrt; extern crate libimagerror; extern crate libimagutil; +extern crate libimaginteraction; +extern crate libimagentryref; use std::process::exit; +use std::collections::BTreeMap; +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, trace_error, trace_error_exit}; -use libimagutil::info_result::*; +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; @@ -61,5 +80,265 @@ fn main() { "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), + _ => { + 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 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 +} + +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 index 00a222b6..e2f9c367 100644 --- a/bin/domain/imag-contact/src/ui.rs +++ b/bin/domain/imag-contact/src/ui.rs @@ -19,8 +19,6 @@ use clap::{Arg, App, SubCommand}; -use libimagutil::cli_validators::*; - pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { app .subcommand(SubCommand::with_name("list") @@ -33,6 +31,13 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { .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") @@ -50,12 +55,19 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> { .subcommand(SubCommand::with_name("show") .about("Show contact") .version("0.1") - .arg(Arg::with_name("ref") + .arg(Arg::with_name("hash") .index(1) .takes_value(true) .required(true) .multiple(false) - .value_name("REF") + .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")) ) } 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}} +""" +