libimagbookmarkfrontend/
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;
39extern crate toml;
40extern crate url;
41extern crate uuid;
42extern crate toml_query;
43#[macro_use] extern crate failure;
44extern crate resiter;
45extern crate handlebars;
46extern crate rayon;
47
48extern crate libimagbookmark;
49extern crate libimagrt;
50extern crate libimagerror;
51extern crate libimagstore;
52extern crate libimagutil;
53extern crate libimagentryurl;
54
55use std::io::Write;
56use std::collections::BTreeMap;
57use std::process::Command;
58
59use failure::Error;
60use failure::err_msg;
61use failure::Fallible as Result;
62use resiter::AndThen;
63use resiter::IterInnerOkOrElse;
64use clap::App;
65use url::Url;
66use handlebars::Handlebars;
67use rayon::iter::ParallelIterator;
68use rayon::iter::IntoParallelIterator;
69use toml_query::read::TomlValueReadExt;
70
71use libimagrt::runtime::Runtime;
72use libimagrt::application::ImagApplication;
73use libimagstore::iter::get::StoreIdGetIteratorExtension;
74use libimagstore::store::FileLockEntry;
75use libimagbookmark::store::BookmarkStore;
76use libimagbookmark::bookmark::Bookmark;
77use libimagentryurl::link::Link;
78
79
80mod ui;
81
82/// Marker enum for implementing ImagApplication on
83///
84/// This is used by binaries crates to execute business logic
85/// or to build a CLI completion.
86pub enum ImagBookmark {}
87impl ImagApplication for ImagBookmark {
88    fn run(rt: Runtime) -> Result<()> {
89        match rt.cli().subcommand_name().ok_or_else(|| err_msg("No subcommand called"))? {
90            "add"        => add(&rt),
91            "open"       => open(&rt),
92            "list"       => list(&rt),
93            "remove"     => remove(&rt),
94            "find"       => find(&rt),
95            other        => {
96                debug!("Unknown command");
97                if rt.handle_unknown_subcommand("imag-bookmark", other, rt.cli())?.success() {
98                    Ok(())
99                } else {
100                    Err(err_msg("Failed to handle unknown subcommand"))
101                }
102            },
103        }
104    }
105
106    fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
107        ui::build_ui(app)
108    }
109
110    fn name() -> &'static str {
111        env!("CARGO_PKG_NAME")
112    }
113
114    fn description() -> &'static str {
115        "Bookmark collection tool"
116    }
117
118    fn version() -> &'static str {
119        env!("CARGO_PKG_VERSION")
120    }
121}
122
123fn add(rt: &Runtime) -> Result<()> {
124    let scmd = rt.cli().subcommand_matches("add").unwrap();
125    scmd.values_of("urls")
126        .unwrap()
127        .map(|s| Url::parse(s).map_err(Error::from))
128        .and_then_ok(|url| {
129            let (uuid, fle) = rt.store().add_bookmark(url.clone())?;
130            debug!("Created entry for url '{}' with uuid '{}'", url, uuid);
131            info!("{} = {}", url, uuid);
132            rt.report_touched(fle.get_location()).map_err(Error::from)
133        })
134        .collect()
135}
136
137fn open(rt: &Runtime) -> Result<()> {
138    let scmd = rt.cli().subcommand_matches("open").unwrap();
139    let open_command = rt.config()
140        .map(|value| {
141            value.read("bookmark.open")?
142                .ok_or_else(|| err_msg("Configuration missing: 'bookmark.open'"))?
143                .as_str()
144                .ok_or_else(|| err_msg("Open command should be a string"))
145        })
146        .or_else(|| Ok(scmd.value_of("opencmd")).transpose())
147        .unwrap_or_else(|| Err(err_msg("No open command available in config or on commandline")))?;
148
149    let hb = {
150        let mut hb = Handlebars::new();
151        hb.register_template_string("format", open_command)?;
152        hb
153    };
154
155    let iter = rt.ids::<crate::ui::PathProvider>()?
156        .ok_or_else(|| err_msg("No ids supplied"))?
157        .into_iter()
158        .map(Ok)
159        .into_get_iter(rt.store())
160        .map_inner_ok_or_else(|| err_msg("Did not find one entry"));
161
162    if scmd.is_present("openparallel") {
163        let links = iter
164            .and_then_ok(|link| rt.report_touched(link.get_location()).map_err(Error::from).map(|_| link))
165            .and_then_ok(|link| calculate_command_data(&hb, &link, open_command))
166            .collect::<Result<Vec<_>>>()?;
167
168        links
169            .into_par_iter()
170            .map(|command_rendered| {
171                    Command::new(&command_rendered[0]) // indexing save with check above
172                        .args(&command_rendered[1..])
173                        .output()
174                        .map_err(Error::from)
175            })
176            .collect::<Result<Vec<_>>>()
177            .map(|_| ())
178    } else {
179        iter.and_then_ok(|link| {
180                rt.report_touched(link.get_location()).map_err(Error::from).map(|_| link)
181            })
182            .and_then_ok(|link| {
183                let command_rendered = calculate_command_data(&hb, &link, open_command)?;
184                Command::new(&command_rendered[0]) // indexing save with check above
185                    .args(&command_rendered[1..])
186                    .output()
187                    .map_err(Error::from)
188            })
189            .collect::<Result<Vec<_>>>()
190            .map(|_| ())
191    }
192}
193
194fn list(rt: &Runtime) -> Result<()> {
195    rt.store()
196        .all_bookmarks()?
197        .into_get_iter()
198        .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
199        .and_then_ok(|entry| {
200            if entry.is_bookmark()? {
201                let url = entry.get_url()?
202                    .ok_or_else(|| format_err!("Failed to retrieve URL for {}", entry.get_location()))?;
203                if !rt.output_is_pipe() {
204                    writeln!(rt.stdout(), "{}", url)?;
205                }
206
207                rt.report_touched(entry.get_location()).map_err(Error::from)
208            } else {
209                Ok(())
210            }
211        })
212        .collect()
213}
214
215fn remove(rt: &Runtime) -> Result<()> {
216    rt.ids::<crate::ui::PathProvider>()?
217        .ok_or_else(|| err_msg("No ids supplied"))?
218        .into_iter()
219        .map(Ok)
220        .into_get_iter(rt.store())
221        .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
222        .and_then_ok(|fle| {
223            rt.report_touched(fle.get_location())
224                .map_err(Error::from)
225                .and_then(|_| rt.store().remove_bookmark(fle))
226        })
227        .collect()
228}
229
230fn find(rt: &Runtime) -> Result<()> {
231    let substr = rt.cli().subcommand_matches("find").unwrap().value_of("substr").unwrap();
232
233    if let Some(ids) = rt.ids::<crate::ui::PathProvider>()? {
234        ids.into_iter()
235            .map(Ok)
236            .into_get_iter(rt.store())
237    } else {
238        rt.store()
239            .all_bookmarks()?
240            .into_get_iter()
241    }
242    .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
243    .and_then_ok(|fle| {
244        if fle.is_bookmark()? {
245            let url = fle
246                .get_url()?
247                .ok_or_else(|| format_err!("Failed to retrieve URL for {}", fle.get_location()))?;
248            if url.as_str().contains(substr) {
249                if !rt.output_is_pipe() {
250                    writeln!(rt.stdout(), "{}", url)?;
251                }
252                rt.report_touched(fle.get_location()).map_err(Error::from)
253            } else {
254                Ok(())
255            }
256        } else {
257            Ok(())
258        }
259    })
260    .collect()
261}
262
263fn calculate_command_data<'a>(hb: &Handlebars, entry: &FileLockEntry<'a>, open_command: &str) -> Result<Vec<String>> {
264    let url = entry.get_url()?
265        .ok_or_else(|| format_err!("Failed to retrieve URL for {}", entry.get_location()))?
266        .into_string();
267
268    let data = {
269        let mut data = BTreeMap::new();
270        data.insert("url", url);
271        data
272    };
273
274    let command_rendered = hb.render("format", &data)?
275        .split_whitespace()
276        .map(String::from)
277        .collect::<Vec<String>>();
278
279    if command_rendered.len() > 2 {
280        return Err(format_err!("Command seems not to include URL: '{}'", open_command));
281    }
282
283    Ok(command_rendered.into_iter().map(String::from).collect())
284}