libimagannotatecmd/
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]
39extern crate log;
40#[macro_use]
41extern crate failure;
42extern crate toml_query;
43extern crate resiter;
44
45extern crate libimagentryannotation;
46extern crate libimagentryedit;
47extern crate libimagerror;
48extern crate libimagrt;
49extern crate libimagstore;
50extern crate libimagutil;
51extern crate libimagentrylink;
52
53use std::io::Write;
54
55use failure::Error;
56use failure::Fallible as Result;
57use failure::ResultExt;
58use failure::err_msg;
59use resiter::IterInnerOkOrElse;
60use resiter::AndThen;
61use resiter::Map;
62use toml_query::read::TomlValueReadTypeExt;
63use clap::App;
64
65use libimagentryannotation::annotateable::*;
66use libimagentryannotation::annotation_fetcher::*;
67use libimagentryedit::edit::*;
68use libimagerror::errors::ErrorMsg as EM;
69use libimagrt::runtime::Runtime;
70use libimagrt::application::ImagApplication;
71use libimagstore::store::FileLockEntry;
72use libimagstore::iter::get::StoreIdGetIteratorExtension;
73use libimagentrylink::linkable::Linkable;
74use libimagrt::iter::ReportTouchedResultEntry;
75
76mod ui;
77
78pub enum ImagAnnotate {}
79impl ImagApplication for ImagAnnotate {
80    fn run(rt: Runtime) -> Result<()> {
81        match rt.cli().subcommand_name().ok_or_else(|| err_msg("No command called"))? {
82            "add"    => add(&rt),
83            "remove" => remove(&rt),
84            "list"   => list(&rt),
85            other    => {
86                debug!("Unknown command");
87                if rt.handle_unknown_subcommand("imag-annotation", other, rt.cli())?.success() {
88                    Ok(())
89                } else {
90                    Err(err_msg("Failed to handle unknown subcommand"))
91                }
92            },
93        }
94    }
95
96    fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
97        ui::build_ui(app)
98    }
99
100    fn name() -> &'static str {
101        env!("CARGO_PKG_NAME")
102    }
103
104    fn description() -> &'static str {
105        "Add annotations to entries"
106    }
107
108    fn version() -> &'static str {
109        env!("CARGO_PKG_VERSION")
110    }
111}
112
113fn add(rt: &Runtime) -> Result<()> {
114    let scmd = rt.cli().subcommand_matches("add").unwrap(); // safed by main()
115    let mut ids = rt
116        .ids::<crate::ui::PathProvider>()
117        .context("No StoreId supplied")?
118        .ok_or_else(|| err_msg("No ids supplied"))?
119        .into_iter();
120
121    if let Some(first) = ids.next() {
122        let mut annotation = rt.store()
123            .get(first.clone())?
124            .ok_or_else(|| EM::EntryNotFound(first.local_display_string()))?
125            .annotate(rt.store())?;
126
127        annotation.edit_content(&rt)?;
128
129        rt.report_touched(&first)?; // report first one first
130        ids.map(Ok).into_get_iter(rt.store())
131            .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
132            .and_then_ok(|mut entry| entry.add_link(&mut annotation).map(|_| entry))
133            .map_report_touched(&rt)
134            .map_ok(|_| ())
135            .collect::<Result<Vec<_>>>()?;
136
137        if !scmd.is_present("dont-print-name") {
138            if let Some(annotation_id) = annotation
139                .get_header()
140                .read_string("annotation.name")?
141            {
142                writeln!(rt.stdout(), "Name of the annotation: {}", annotation_id)?;
143            } else {
144                Err(format_err!("Unnamed annotation: {:?}", annotation.get_location()))
145                    .context("This is most likely a BUG, please report!")?;
146            }
147        }
148
149        rt.report_touched(annotation.get_location())?;
150    } else {
151        debug!("No entries to annotate");
152    }
153
154    Ok(())
155}
156
157fn remove(rt: &Runtime) -> Result<()> {
158    let scmd            = rt.cli().subcommand_matches("remove").unwrap(); // safed by main()
159    let annotation_name = scmd.value_of("annotation_name").unwrap(); // safed by clap
160    let delete          = scmd.is_present("delete-annotation");
161
162    rt.ids::<crate::ui::PathProvider>()
163        .context("No ids supplied")?
164        .ok_or_else(|| err_msg("No ids supplied"))?
165        .into_iter()
166        .map(|id| {
167            let mut entry = rt.store()
168                .get(id.clone())?
169                .ok_or_else(|| EM::EntryNotFound(id.local_display_string()))?;
170
171            let annotation = entry.denotate(rt.store(), annotation_name)?;
172
173            if delete {
174                debug!("Deleting annotation object");
175                if let Some(an) = annotation {
176                    let loc = an.get_location().clone();
177                    drop(an);
178
179                    rt.store().delete(loc)?;
180                } else {
181                    warn!("Not having annotation object, cannot delete!");
182                }
183            } else {
184                debug!("Not deleting annotation object");
185            }
186
187            rt.report_touched(entry.get_location()).map_err(Error::from)
188        })
189        .collect()
190}
191
192fn list(rt: &Runtime) -> Result<()> {
193    let scmd      = rt.cli().subcommand_matches("list").unwrap(); // safed by clap
194    let with_text = scmd.is_present("list-with-text");
195    let ids = rt
196        .ids::<crate::ui::PathProvider>()
197        .context("No ids supplied")?
198        .ok_or_else(|| err_msg("No ids supplied"))?;
199
200    if ids.is_empty() {
201        ids.into_iter()
202            .map(|id| -> Result<_> {
203                let lds = id.local_display_string();
204                Ok(rt.store()
205                    .get(id)?
206                    .ok_or_else(|| EM::EntryNotFound(lds))?
207                    .annotations()?
208                    .into_get_iter(rt.store())
209                    .map(|el| el.and_then(|o| o.ok_or_else(|| format_err!("Cannot find entry"))))
210                    .enumerate()
211                    .map(|(i, entry)| entry.and_then(|e| list_annotation(&rt, i, &e, with_text).map(|_| e)))
212                    .map_report_touched(&rt)
213                    .map_ok(|_| ())
214                    .collect())
215            })
216            .flatten()
217            .collect()
218    } else { // ids.len() == 0
219        // show them all
220        rt.store()
221            .all_annotations()?
222            .into_get_iter()
223            .map(|el| el.and_then(|opt| opt.ok_or_else(|| format_err!("Cannot find entry"))))
224            .enumerate()
225            .map(|(i, entry)| entry.and_then(|e| list_annotation(&rt, i, &e, with_text).map(|_| e)))
226            .map_report_touched(&rt)
227            .map_ok(|_| ())
228            .collect()
229    }
230}
231
232fn list_annotation<'a>(rt: &Runtime, i: usize, a: &FileLockEntry<'a>, with_text: bool) -> Result<()> {
233    if with_text {
234        writeln!(rt.stdout(),
235                 "--- {i: >5} | {id}\n{text}\n\n",
236                 i = i,
237                 id = a.get_location(),
238                 text = a.get_content())
239    } else {
240        writeln!(rt.stdout(), "{: >5} | {}", i, a.get_location())
241    }.map_err(Error::from)
242}
243