libimagcontactfrontend/
lib.rs

1//
2// imag - the personal information management suite for the commandline
3// Copyright (C) 2015-2020 Matthias Beyer <mail@beyermatthias.de> and contributors
4//
5// This library is free software; you can redistribute it and/or
6// modify it under the terms of the GNU Lesser General Public
7// License as published by the Free Software Foundation; version
8// 2.1 of the License.
9//
10// This library is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13// Lesser General Public License for more details.
14//
15// You should have received a copy of the GNU Lesser General Public
16// License along with this library; if not, write to the Free Software
17// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18//
19
20#![forbid(unsafe_code)]
21
22#![deny(
23    non_camel_case_types,
24    non_snake_case,
25    path_statements,
26    trivial_numeric_casts,
27    unstable_features,
28    unused_allocation,
29    unused_import_braces,
30    unused_imports,
31    unused_must_use,
32    unused_mut,
33    unused_qualifications,
34    while_true,
35)]
36
37extern crate clap;
38#[macro_use] extern crate log;
39#[macro_use] extern crate vobject;
40extern crate toml;
41extern crate toml_query;
42extern crate handlebars;
43extern crate walkdir;
44extern crate uuid;
45extern crate serde_json;
46#[macro_use] extern crate failure;
47extern crate resiter;
48
49extern crate libimagcontact;
50extern crate libimagstore;
51extern crate libimagrt;
52extern crate libimagerror;
53extern crate libimagutil;
54extern crate libimaginteraction;
55extern crate libimagentryedit;
56extern crate libimagentryref;
57
58use std::path::PathBuf;
59use std::io::Write;
60
61use handlebars::Handlebars;
62use clap::{App, ArgMatches};
63use toml_query::read::TomlValueReadExt;
64use toml_query::read::TomlValueReadTypeExt;
65use toml_query::read::Partial;
66use walkdir::WalkDir;
67use failure::Error;
68use failure::err_msg;
69use failure::Fallible as Result;
70use resiter::AndThen;
71use resiter::IterInnerOkOrElse;
72use resiter::Map;
73use resiter::Filter;
74
75use libimagrt::runtime::Runtime;
76use libimagrt::application::ImagApplication;
77use libimagcontact::store::ContactStore;
78use libimagcontact::contact::Contact;
79use libimagcontact::deser::DeserVcard;
80
81mod ui;
82mod util;
83mod create;
84mod edit;
85
86use crate::util::build_data_object_for_handlebars;
87use crate::create::create;
88use crate::edit::edit;
89
90/// Marker enum for implementing ImagApplication on
91///
92/// This is used by binaries crates to execute business logic
93/// or to build a CLI completion.
94pub enum ImagContact {}
95impl ImagApplication for ImagContact {
96    fn run(rt: Runtime) -> Result<()> {
97        match rt.cli().subcommand_name().ok_or_else(|| err_msg("No subcommand called"))? {
98            "list"   => list(&rt),
99            "import" => import(&rt),
100            "show"   => show(&rt),
101            "edit"   => edit(&rt),
102            "find"   => find(&rt),
103            "create" => create(&rt),
104            other    => {
105                debug!("Unknown command");
106                if rt.handle_unknown_subcommand("imag-contact", other, rt.cli())?.success() {
107                    Ok(())
108                } else {
109                    Err(err_msg("Failed to handle unknown subcommand"))
110                }
111            },
112        }
113    }
114
115    fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
116        ui::build_ui(app)
117    }
118
119    fn name() -> &'static str {
120        env!("CARGO_PKG_NAME")
121    }
122
123    fn description() -> &'static str {
124        "Contact management tool"
125    }
126
127    fn version() -> &'static str {
128        env!("CARGO_PKG_VERSION")
129    }
130}
131
132fn list(rt: &Runtime) -> Result<()> {
133    let scmd        = rt.cli().subcommand_matches("list").unwrap();
134    let list_format = get_contact_print_format("contact.list_format", rt, &scmd)?;
135    debug!("List format: {:?}", list_format);
136
137    let iterator = rt
138        .store()
139        .all_contacts()?
140        .into_get_iter()
141        .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
142        .and_then_ok(|fle| {
143            rt.report_touched(fle.get_location())?;
144            Ok(fle)
145        })
146        .and_then_ok(|e| e.deser());
147
148    if scmd.is_present("json") {
149        debug!("Listing as JSON");
150        let v = iterator.collect::<Result<Vec<DeserVcard>>>()?;
151        let s = ::serde_json::to_string(&v)?;
152        writeln!(rt.stdout(), "{}", s).map_err(Error::from)
153    } else {
154        debug!("Not listing as JSON");
155        let output     = rt.stdout();
156        let mut output = output.lock();
157        let mut i = 0;
158        iterator
159            .map_ok(|dvcard| {
160                i += 1;
161                build_data_object_for_handlebars(i, &dvcard)
162            })
163            .and_then_ok(|data| list_format.render("format", &data).map_err(Error::from))
164            .and_then_ok(|s| writeln!(output, "{}", s).map_err(Error::from))
165            .collect::<Result<Vec<_>>>()
166            .map(|_| ())
167    }
168}
169
170fn import(rt: &Runtime) -> Result<()> {
171    let scmd           = rt.cli().subcommand_matches("import").unwrap(); // secured by main
172    let force_override = scmd.is_present("force-override");
173    let path           = scmd.value_of("path").map(PathBuf::from).unwrap(); // secured by clap
174
175    let collection_name = rt.cli().value_of("contact-ref-collection-name").unwrap(); // default by clap
176    let ref_config = rt.config()
177        .ok_or_else(|| format_err!("No configuration, cannot continue!"))?
178        .read_partial::<libimagentryref::reference::Config>()?
179        .ok_or_else(|| format_err!("Configuration missing: {}", libimagentryref::reference::Config::LOCATION))?;
180
181    // TODO: Refactor the above to libimagutil or libimagrt?
182
183    if !path.exists() {
184        return Err(format_err!("Path does not exist: {}", path.display()))
185    }
186
187    if path.is_file() {
188        let entry = rt
189            .store()
190            .retrieve_from_path(&path, &ref_config, &collection_name, force_override)?;
191
192        rt.report_touched(entry.get_location()).map_err(Error::from)
193    } else if path.is_dir() {
194        WalkDir::new(path)
195            .min_depth(1)
196            .into_iter()
197            .map(|r| r.map_err(Error::from))
198            .and_then_ok(|entry| {
199                if entry.file_type().is_file() {
200                    let pb = PathBuf::from(entry.path());
201                    let fle = rt
202                        .store()
203                        .retrieve_from_path(&pb, &ref_config, &collection_name, force_override)?;
204
205                    rt.report_touched(fle.get_location())?;
206                    info!("Imported: {}", entry.path().to_str().unwrap_or("<non UTF-8 path>"));
207                    Ok(())
208                } else {
209                    warn!("Ignoring non-file: {}", entry.path().to_str().unwrap_or("<non UTF-8 path>"));
210                    Ok(())
211                }
212            })
213            .collect::<Result<Vec<_>>>()
214            .map(|_| ())
215    } else {
216        Err(err_msg("Path is neither directory nor file"))
217    }
218}
219
220fn show(rt: &Runtime) -> Result<()> {
221    let scmd        = rt.cli().subcommand_matches("show").unwrap();
222    let hash        = scmd.value_of("hash").map(String::from).unwrap(); // safed by clap
223    let show_format = get_contact_print_format("contact.show_format", rt, &scmd)?;
224    let out         = rt.stdout();
225    let mut outlock = out.lock();
226
227    util::find_contact_by_hash(rt, hash)?
228        .filter_ok(|tpl| tpl.0)
229        .map_ok(|tpl| tpl.1)
230        .enumerate()
231        .map(|(i, elem)| {
232            let elem = elem?.deser()?;
233            let data = build_data_object_for_handlebars(i, &elem);
234
235            let s = show_format.render("format", &data)?;
236            writeln!(outlock, "{}", s).map_err(Error::from)
237        })
238        .collect::<Result<Vec<_>>>()
239        .map(|_| ())
240}
241
242fn find(rt: &Runtime) -> Result<()> {
243    let scmd       = rt.cli().subcommand_matches("find").unwrap();
244    let grepstring = scmd
245        .values_of("string")
246        .unwrap() // safed by clap
247        .map(String::from)
248        .collect::<Vec<String>>();
249
250    // We don't know yet which we need, but we pay that price for simplicity of the codebase
251    let show_format = get_contact_print_format("contact.show_format", rt, &scmd)?;
252    let list_format = get_contact_print_format("contact.list_format", rt, &scmd)?;
253
254    let iterator = rt
255        .store()
256        .all_contacts()?
257        .into_get_iter()
258        .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
259        .and_then_ok(|entry| {
260            let card = entry.deser()?;
261
262            let str_contains_any = |s: &String, v: &Vec<String>| {
263                v.iter().any(|i| s.contains(i))
264            };
265
266            let take = card.adr().iter().any(|a| str_contains_any(a, &grepstring))
267                || card.email().iter().any(|a| str_contains_any(&a.address, &grepstring))
268                || card.fullname().iter().any(|a| str_contains_any(a, &grepstring));
269
270            if take {
271                rt.report_touched(entry.get_location())?;
272
273                // optimization so we don't have to parse again in the next step
274                Ok((true, entry, card))
275            } else {
276                Ok((false, entry, card))
277            }
278        });
279
280    let mut i = 0;
281
282    if !rt.output_is_pipe() || rt.ignore_ids() {
283        if scmd.is_present("json") {
284            iterator
285                .filter_ok(|tpl| tpl.0)
286                .map_ok(|tpl| tpl.2)
287                .and_then_ok(|v| {
288                    let s = ::serde_json::to_string(&v)?;
289                    writeln!(rt.stdout(), "{}", s).map_err(Error::from)
290                })
291                .collect::<Result<Vec<_>>>()
292                .map(|_| ())
293        } else if scmd.is_present("find-id") {
294            iterator
295            .and_then_ok(|(take, entry, _)| {
296                if take {
297                    writeln!(rt.stdout(), "{}", entry.get_location()).map_err(Error::from)
298                } else {
299                    Ok(())
300                }
301            })
302            .collect::<Result<Vec<_>>>()
303            .map(|_| ())
304        } else if scmd.is_present("find-full-id") {
305            let storepath = rt.store().path().display();
306            iterator
307            .and_then_ok(|(take, entry, _)| {
308                if take {
309                    writeln!(rt.stdout(), "{}/{}", storepath, entry.get_location()).map_err(Error::from)
310                } else {
311                    Ok(())
312                }
313            })
314            .collect::<Result<Vec<_>>>()
315            .map(|_| ())
316        } else {
317            iterator
318            .and_then_ok(|(take, _, card)| {
319                if take {
320                    i += 1;
321                    let fmt = if scmd.is_present("find-show") {
322                        &show_format
323                    } else { // default: find-list
324                        &list_format
325                    };
326
327                    let data = build_data_object_for_handlebars(i, &card);
328                    let s = fmt.render("format", &data)?;
329
330                    writeln!(rt.stdout(), "{}", s).map_err(Error::from)
331                } else {
332                    Ok(())
333                }
334            })
335            .collect::<Result<Vec<_>>>()
336            .map(|_| ())
337        }
338    } else { // if not printing, we still have to consume the iterator to report the touched IDs
339        let _ = iterator.collect::<Vec<_>>();
340        Ok(())
341    }
342}
343
344fn get_contact_print_format(config_value_path: &'static str, rt: &Runtime, scmd: &ArgMatches) -> Result<Handlebars> {
345    let fmt = match scmd.value_of("format").map(String::from) {
346        Some(s) => Ok(s),
347        None => rt.config()
348            .ok_or_else(|| err_msg("No configuration file"))?
349            .read_string(config_value_path)?
350            .ok_or_else(|| err_msg("Configuration 'contact.list_format' does not exist")),
351    }?;
352
353    let mut hb = Handlebars::new();
354    hb.register_template_string("format", fmt)?;
355
356    hb.register_escape_fn(::handlebars::no_escape);
357    ::libimaginteraction::format::register_all_color_helpers(&mut hb);
358    ::libimaginteraction::format::register_all_format_helpers(&mut hb);
359    Ok(hb)
360}
361