Add implementation for imag-contact
* Implement contact listing with formatting via commandline or config * Implement importer functionality for contact * Implement "show" subcommand * imagrc.toml: Add explanation which fns are supported in templates
This commit is contained in:
parent
0540ae9392
commit
d0ec7e26dc
4 changed files with 365 additions and 6 deletions
|
@ -19,8 +19,15 @@ log = "0.3"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
toml = "0.4"
|
toml = "0.4"
|
||||||
toml-query = "^0.3.1"
|
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" }
|
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" }
|
libimagerror = { version = "0.5.0", path = "../../../lib/core/libimagerror" }
|
||||||
libimagcontact = { version = "0.5.0", path = "../../../lib/domain/libimagcontact" }
|
libimagcontact = { version = "0.5.0", path = "../../../lib/domain/libimagcontact" }
|
||||||
libimagutil = { version = "0.5.0", path = "../../../lib/etc/libimagutil" }
|
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" }
|
||||||
|
|
||||||
|
|
|
@ -37,19 +37,38 @@ extern crate clap;
|
||||||
#[macro_use] extern crate version;
|
#[macro_use] extern crate version;
|
||||||
extern crate toml;
|
extern crate toml;
|
||||||
extern crate toml_query;
|
extern crate toml_query;
|
||||||
|
extern crate handlebars;
|
||||||
|
extern crate vobject;
|
||||||
|
extern crate walkdir;
|
||||||
|
|
||||||
extern crate libimagcontact;
|
extern crate libimagcontact;
|
||||||
|
extern crate libimagstore;
|
||||||
extern crate libimagrt;
|
extern crate libimagrt;
|
||||||
extern crate libimagerror;
|
extern crate libimagerror;
|
||||||
extern crate libimagutil;
|
extern crate libimagutil;
|
||||||
|
extern crate libimaginteraction;
|
||||||
|
extern crate libimagentryref;
|
||||||
|
|
||||||
use std::process::exit;
|
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::runtime::Runtime;
|
||||||
use libimagrt::setup::generate_runtime_setup;
|
use libimagrt::setup::generate_runtime_setup;
|
||||||
use libimagerror::trace::{MapErrTrace, trace_error, trace_error_exit};
|
use libimagerror::trace::MapErrTrace;
|
||||||
use libimagutil::info_result::*;
|
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 ui;
|
||||||
|
|
||||||
|
@ -61,5 +80,265 @@ fn main() {
|
||||||
"Contact management tool",
|
"Contact management tool",
|
||||||
build_ui);
|
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::<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 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,6 @@
|
||||||
|
|
||||||
use clap::{Arg, App, SubCommand};
|
use clap::{Arg, App, SubCommand};
|
||||||
|
|
||||||
use libimagutil::cli_validators::*;
|
|
||||||
|
|
||||||
pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
|
pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
|
||||||
app
|
app
|
||||||
.subcommand(SubCommand::with_name("list")
|
.subcommand(SubCommand::with_name("list")
|
||||||
|
@ -33,6 +31,13 @@ pub fn build_ui<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.value_name("FILTER")
|
.value_name("FILTER")
|
||||||
.help("Filter by these properties (not implemented yet)"))
|
.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")
|
.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")
|
.subcommand(SubCommand::with_name("show")
|
||||||
.about("Show contact")
|
.about("Show contact")
|
||||||
.version("0.1")
|
.version("0.1")
|
||||||
.arg(Arg::with_name("ref")
|
.arg(Arg::with_name("hash")
|
||||||
.index(1)
|
.index(1)
|
||||||
.takes_value(true)
|
.takes_value(true)
|
||||||
.required(true)
|
.required(true)
|
||||||
.multiple(false)
|
.multiple(false)
|
||||||
.value_name("REF")
|
.value_name("HASH")
|
||||||
.help("Show the contact pointed to by this reference 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"))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
61
imagrc.toml
61
imagrc.toml
|
@ -255,3 +255,64 @@ default_collection = "default"
|
||||||
editor = "vim -R {{entry}}"
|
editor = "vim -R {{entry}}"
|
||||||
web = "chromium {{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}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue