libimaglogfrontend/
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 is_match;
39#[macro_use] extern crate log;
40extern crate toml;
41extern crate toml_query;
42extern crate itertools;
43extern crate failure;
44extern crate textwrap;
45extern crate resiter;
46
47extern crate libimaglog;
48extern crate libimagrt;
49extern crate libimagstore;
50extern crate libimagerror;
51extern crate libimagdiary;
52
53use std::io::Write;
54use std::io::Cursor;
55use std::str::FromStr;
56
57use failure::Error;
58use failure::err_msg;
59use failure::Fallible as Result;
60use resiter::Map;
61use resiter::AndThen;
62use resiter::IterInnerOkOrElse;
63use resiter::Filter;
64
65use libimagrt::application::ImagApplication;
66use libimagrt::runtime::Runtime;
67use libimagdiary::diary::Diary;
68use libimagdiary::diaryid::DiaryId;
69use libimaglog::log::Log;
70use libimagstore::iter::get::StoreIdGetIteratorExtension;
71use libimagstore::store::FileLockEntry;
72
73use clap::App;
74
75mod ui;
76
77use toml::Value;
78use itertools::Itertools;
79
80/// Marker enum for implementing ImagApplication on
81///
82/// This is used by binaries crates to execute business logic
83/// or to build a CLI completion.
84pub enum ImagLog {}
85impl ImagApplication for ImagLog {
86    fn run(rt: Runtime) -> Result<()> {
87        if let Some(scmd) = rt.cli().subcommand_name() {
88            match scmd {
89                "show" => show(&rt),
90                other  => {
91                    debug!("Unknown command");
92                    if rt.handle_unknown_subcommand("imag-bookmark", other, rt.cli())?.success() {
93                        Ok(())
94                    } else {
95                        Err(err_msg("Failed to handle unknown subcommand"))
96                    }
97                },
98            }
99        } else {
100            let text       = get_log_text(&rt);
101            let diary_name = match rt.cli().value_of("diaryname").map(String::from) {
102                Some(s) => s,
103                None => get_diary_name(&rt)?,
104            };
105
106            debug!("Writing to '{}': {}", diary_name, text);
107
108            rt.store()
109                .new_entry_now(&diary_name)
110                .and_then(|mut fle| {
111                    fle.make_log_entry()?;
112                    *fle.get_content_mut() = text;
113                    Ok(fle)
114                })
115                .and_then(|fle| rt.report_touched(fle.get_location()).map_err(Error::from))
116        }
117    }
118
119    fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
120        ui::build_ui(app)
121    }
122
123    fn name() -> &'static str {
124        env!("CARGO_PKG_NAME")
125    }
126
127    fn description() -> &'static str {
128        "Overlay to imag-diary to 'log' single lines of text"
129    }
130
131    fn version() -> &'static str {
132        env!("CARGO_PKG_VERSION")
133    }
134}
135
136fn show(rt: &Runtime) -> Result<()> {
137    use std::borrow::Cow;
138
139    use libimagdiary::iter::DiaryEntryIterator;
140    use libimagdiary::entry::DiaryEntry;
141
142    let scmd = rt.cli().subcommand_matches("show").unwrap(); // safed by main()
143    let iters : Vec<DiaryEntryIterator> = match scmd.values_of("show-name") {
144        Some(values) => values
145            .map(|diary_name| Diary::entries(rt.store(), diary_name))
146            .collect::<Result<Vec<DiaryEntryIterator>>>(),
147
148        None => if scmd.is_present("show-all") {
149            debug!("Showing for all diaries");
150            let iter = rt.store()
151                .diary_names()?
152                .map(|diary_name| {
153                    let diary_name = diary_name?;
154                    debug!("Getting entries for Diary: {}", diary_name);
155                    let entries = Diary::entries(rt.store(), &diary_name)?;
156                    let diary_name = Cow::from(diary_name);
157                    Ok((entries, diary_name))
158                })
159                .collect::<Result<Vec<(DiaryEntryIterator, Cow<str>)>>>()?;
160
161            let iter = iter.into_iter()
162                .unique_by(|tpl| tpl.1.clone())
163                .map(|tpl| tpl.0)
164                .collect::<Vec<DiaryEntryIterator>>();
165
166            Ok(iter)
167        } else {
168            // showing default logs
169            get_diary_name(rt).and_then(|dname| Diary::entries(rt.store(), &dname)).map(|e| vec![e])
170        }
171    }?;
172
173    let mut do_wrap = if scmd.is_present("show-wrap") {
174        Some(80)
175    } else {
176        None
177    };
178    let do_remove_newlines = scmd.is_present("show-skipnewlines");
179
180    if let Some(wrap_value) = scmd.value_of("show-wrap") {
181        do_wrap = Some(usize::from_str(wrap_value).map_err(Error::from)?);
182    }
183
184    let mut output = rt.stdout();
185
186    let v = iters.into_iter()
187        .flatten()
188        .into_get_iter(rt.store())
189        .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
190        .and_then_ok(|e| e.is_log().map(|b| (b, e)))
191        .filter_ok(|tpl| tpl.0)
192        .map_ok(|tpl| tpl.1)
193        .and_then_ok(|entry| entry.diary_id().map(|did| (did.get_date_representation(), did, entry)))
194        .collect::<Result<Vec<_>>>()?;
195
196    v.into_iter()
197        .sorted_by_key(|tpl| tpl.0)
198        .map(|tpl| (tpl.1, tpl.2))
199        .inspect(|tpl| debug!("Found entry: {:?}", tpl.1))
200        .map(|(id, entry)| {
201            if let Some(wrap_limit) = do_wrap {
202                // assume a capacity here:
203                // diaryname + year + month + day + hour + minute + delimiters + whitespace
204                // 10 + 4 + 2 + 2 + 2 + 2 + 6 + 4 = 32
205                // plus text, which we assume to be 120 characters... lets allocate 256 bytes.
206                let mut buffer = Cursor::new(Vec::with_capacity(256));
207                do_write_to(&mut buffer, id, &entry, do_remove_newlines)?;
208                let buffer = String::from_utf8(buffer.into_inner())?;
209
210                // now lets wrap
211                ::textwrap::wrap(&buffer, wrap_limit)
212                    .iter()
213                    .map(|line| writeln!(&mut output, "{}", line).map_err(Error::from))
214                    .collect::<Result<Vec<_>>>()?;
215            } else {
216                do_write_to(&mut output, id, &entry, do_remove_newlines)?;
217            }
218
219            rt.report_touched(entry.get_location()).map_err(Error::from)
220        })
221        .collect::<Result<Vec<_>>>()
222        .map(|_| ())
223}
224
225fn get_diary_name(rt: &Runtime) -> Result<String> {
226    use toml_query::read::TomlValueReadExt;
227    use toml_query::read::TomlValueReadTypeExt;
228
229    let cfg = rt
230        .config()
231        .ok_or_else(|| err_msg("Configuration not present, cannot continue"))?;
232
233    let current_log = cfg
234        .read_string("log.default")?
235        .ok_or_else(|| err_msg("Configuration missing: 'log.default'"))?;
236
237    if cfg
238        .read("log.logs")?
239        .ok_or_else(|| err_msg("Configuration missing: 'log.logs'"))?
240        .as_array()
241        .ok_or_else(|| err_msg("Configuration 'log.logs' is not an Array"))?
242        .iter()
243        .map(|e| if !is_match!(e, &Value::String(_)) {
244            Err(err_msg("Configuration 'log.logs' is not an Array<String>!"))
245        } else {
246            Ok(e)
247        })
248        .map_ok(|value| value.as_str().unwrap())
249        .map_ok(String::from)
250        .collect::<Result<Vec<_>>>()?
251        .iter()
252        .find(|log| *log == &current_log)
253        .is_none()
254    {
255        Err(err_msg("'log.logs' does not contain 'log.default'"))
256    } else {
257        Ok(current_log)
258    }
259}
260
261fn get_log_text(rt: &Runtime) -> String {
262    rt.cli()
263        .values_of("text")
264        .unwrap() // safe by clap
265        .enumerate()
266        .fold(String::with_capacity(500), |mut acc, (n, e)| {
267            if n != 0 {
268                acc.push_str(" ");
269            }
270            acc.push_str(e);
271            acc
272        })
273}
274
275fn do_write_to<'a>(sink: &mut dyn Write, id: DiaryId, entry: &FileLockEntry<'a>, do_remove_newlines: bool) -> Result<()> {
276    let text = if do_remove_newlines {
277        entry.get_content().trim_end().replace("\n", "")
278    } else {
279        entry.get_content().trim_end().to_string()
280    };
281
282    writeln!(sink,
283            "{dname: >10} - {y: >4}-{m:0>2}-{d:0>2}T{H:0>2}:{M:0>2} - {text}",
284             dname = id.diary_name(),
285             y = id.year(),
286             m = id.month(),
287             d = id.day(),
288             H = id.hour(),
289             M = id.minute(),
290             text = text)
291        .map_err(Error::from)
292}
293