libimagtodofrontend/
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;
38extern crate toml;
39extern crate toml_query;
40extern crate chrono;
41extern crate filters;
42extern crate kairos;
43#[macro_use] extern crate log;
44#[macro_use] extern crate failure;
45extern crate resiter;
46extern crate handlebars;
47extern crate prettytable;
48
49#[cfg(feature = "import-taskwarrior")]
50extern crate task_hookrs;
51
52#[cfg(feature = "import-taskwarrior")]
53extern crate uuid;
54
55#[cfg(feature = "import-taskwarrior")]
56extern crate libimagentrytag;
57
58#[cfg(feature = "import-taskwarrior")]
59extern crate libimagentrylink;
60
61extern crate libimagrt;
62extern crate libimagstore;
63extern crate libimagerror;
64extern crate libimagentryedit;
65extern crate libimagtodo;
66extern crate libimagutil;
67extern crate libimagentryview;
68extern crate libimaginteraction;
69
70use std::ops::Deref;
71use std::io::Write;
72use std::result::Result as RResult;
73use std::str::FromStr;
74
75use clap::ArgMatches;
76use chrono::NaiveDateTime;
77use failure::Error;
78use failure::Fallible as Result;
79use failure::err_msg;
80use clap::App;
81use resiter::AndThen;
82use resiter::IterInnerOkOrElse;
83use prettytable::Table;
84use prettytable::Cell;
85use prettytable::Row;
86
87use libimagentryedit::edit::Edit;
88use libimagentryview::viewer::Viewer;
89use libimagrt::application::ImagApplication;
90use libimagrt::runtime::Runtime;
91use libimagstore::iter::get::*;
92use libimagstore::store::Entry;
93use libimagstore::store::FileLockEntry;
94use libimagtodo::entry::Todo;
95use libimagtodo::priority::Priority;
96use libimagtodo::status::Status;
97use libimagtodo::store::TodoStore;
98
99mod ui;
100mod import;
101mod util;
102
103/// Marker enum for implementing ImagApplication on
104///
105/// This is used by binaries crates to execute business logic
106/// or to build a CLI completion.
107pub enum ImagTodo {}
108impl ImagApplication for ImagTodo {
109    fn run(rt: Runtime) -> Result<()> {
110        match rt.cli().subcommand_name() {
111            Some("create")      => create(&rt),
112            Some("show")        => show(&rt),
113            Some("mark")           => mark(&rt),
114            Some("pending") | None => list_todos(&rt, &StatusMatcher::new().is(Status::Pending), false),
115            Some("list")           => list(&rt),
116            Some("import")         => import::import(&rt),
117            Some(other)         => {
118                debug!("Unknown command");
119                if rt.handle_unknown_subcommand("imag-todo", other, rt.cli())?.success() {
120                    Ok(())
121                } else {
122                    Err(err_msg("Failed to handle unknown subcommand"))
123                }
124            }
125        } // end match scmd
126    }
127
128    fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> {
129        ui::build_ui(app)
130    }
131
132    fn name() -> &'static str {
133        env!("CARGO_PKG_NAME")
134    }
135
136    fn description() -> &'static str {
137        "Interface with taskwarrior"
138    }
139
140    fn version() -> &'static str {
141        env!("CARGO_PKG_VERSION")
142    }
143}
144
145/// A black- and whitelist for matching statuses of todo entries
146///
147/// The blacklist is checked first, followed by the whitelist.
148/// In case the whitelist is empty, the StatusMatcher works with a
149/// blacklist-only approach.
150#[derive(Debug, Default)]
151pub struct StatusMatcher {
152    is: Vec<Status>,
153    is_not: Vec<Status>,
154}
155
156impl StatusMatcher {
157    pub fn new() -> Self {
158        StatusMatcher { ..Default::default() }
159    }
160
161    pub fn is(mut self, s: Status) -> Self {
162        self.add_is(s);
163        self
164    }
165
166    pub fn add_is(&mut self, s: Status) {
167        self.is.push(s);
168    }
169
170    #[allow(clippy::wrong_self_convention)]
171    pub fn is_not(mut self, s: Status) -> Self {
172        self.add_is_not(s);
173        self
174    }
175
176    pub fn add_is_not(&mut self, s: Status) {
177        self.is_not.push(s);
178    }
179
180    pub fn matches(&self, todo: Status) -> bool {
181        if self.is_not.iter().any(|t| *t == todo) {
182            // On blacklist
183            false
184        } else {
185            // No whitelist or on whitelist
186            // or
187            // Not on blacklist, but whitelist exists and not on it either
188            self.is.is_empty() || self.is.iter().any(|t| *t == todo)
189        }
190    }
191}
192
193fn create(rt: &Runtime) -> Result<()> {
194    debug!("Creating todo");
195    let scmd = rt.cli().subcommand().1.unwrap(); // safe by clap
196
197    let scheduled: Option<NaiveDateTime> = get_datetime_arg(&scmd, "create-scheduled")?;
198    let hidden: Option<NaiveDateTime>    = get_datetime_arg(&scmd, "create-hidden")?;
199    let due: Option<NaiveDateTime>       = get_datetime_arg(&scmd, "create-due")?;
200    let prio: Option<Priority>           = scmd.value_of("create-prio").map(prio_from_str).transpose()?;
201    let status: Status                   = scmd.value_of("create-status").map(Status::from_str).unwrap()?;
202    let edit                             = scmd.is_present("create-edit");
203    let text                             = scmd.value_of("text").unwrap();
204
205    trace!("Creating todo with these variables:");
206    trace!("scheduled = {:?}", scheduled);
207    trace!("hidden    = {:?}", hidden);
208    trace!("due       = {:?}", due);
209    trace!("prio      = {:?}", prio);
210    trace!("status    = {:?}", status);
211    trace!("edit      = {}", edit);
212    trace!("text      = {:?}", text);
213
214    let mut entry = rt.store().create_todo(status, scheduled, hidden, due, prio, true)?;
215    debug!("Created: todo {}", entry.get_uuid()?);
216
217    debug!("Setting content");
218    *entry.get_content_mut() = text.to_string();
219
220    if edit {
221        debug!("Editing content");
222        entry.edit_content(&rt)?;
223    }
224
225    rt.report_touched(entry.get_location())
226}
227
228fn mark(rt: &Runtime) -> Result<()> {
229    fn mark_todos_as(rt: &Runtime, status: Status) -> Result<()> {
230        rt.ids::<crate::ui::PathProvider>()?
231            .ok_or_else(|| err_msg("No ids supplied"))?
232            .into_iter()
233            .map(Ok)
234            .into_get_iter(rt.store())
235            .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
236            .and_then_ok(|e| rt.report_touched(e.get_location()).map(|_| e))
237            .and_then_ok(|mut e| e.set_status(status.clone()))
238            .collect()
239    }
240
241    let scmd = rt.cli().subcommand().1.unwrap();
242    match scmd.subcommand_name() {
243        Some("done")    => mark_todos_as(rt, Status::Done),
244        Some("deleted") => mark_todos_as(rt, Status::Deleted),
245        Some("pending") => mark_todos_as(rt, Status::Pending),
246        Some(other)     => Err(format_err!("Unknown mark type selected: {}", other)),
247        None            => Err(format_err!("No mark type selected, doing nothing!")),
248    }
249}
250
251/// Generic todo listing function
252///
253/// Supports filtering of todos by status using the passed in StatusMatcher
254fn list_todos(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool) -> Result<()> {
255    use filters::failable::filter::FailableFilter;
256    debug!("Listing todos with status filter {:?}", matcher);
257
258    struct TodoViewer {
259        details: bool,
260    }
261    impl Viewer for TodoViewer {
262        fn view_entry<W>(&self, entry: &Entry, sink: &mut W) -> RResult<(), libimagentryview::error::Error>
263            where W: Write
264        {
265            use libimagentryview::error::Error as E;
266
267            if !entry.is_todo().map_err(E::from)? {
268                return Err(format_err!("Not a Todo: {}", entry.get_location())).map_err(E::from);
269            }
270
271            let uuid     = entry.get_uuid().map_err(E::from)?;
272            let status   = entry.get_status().map_err(E::from)?;
273            let status   = status.as_str();
274            let first_line = entry.get_content()
275                .lines()
276                .next()
277                .unwrap_or("<empty description>");
278
279            if !self.details {
280                writeln!(sink, "{uuid} - {status} : {first_line}",
281                         uuid = uuid,
282                         status = status,
283                         first_line = first_line)
284            } else {
285                let sched    = util::get_dt_str(entry.get_scheduled(), "Not scheduled")?;
286                let hidden   = util::get_dt_str(entry.get_hidden(), "Not hidden")?;
287                let due      = util::get_dt_str(entry.get_due(), "No due")?;
288                let priority = entry.get_priority().map_err(E::from)?.map(|p| p.as_str().to_string())
289                    .unwrap_or_else(|| "No prio".to_string());
290
291                writeln!(sink, "{uuid} - {status} - {sched} - {hidden} - {due} - {prio}: {first_line}",
292                         uuid = uuid,
293                         status = status,
294                         sched = sched,
295                         hidden = hidden,
296                         due = due,
297                         prio = priority,
298                         first_line = first_line)
299            }
300            .map_err(libimagentryview::error::Error::from)
301        }
302    }
303
304    fn process<'a, I>(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool, iter: I) -> Result<()>
305        where I: Iterator<Item = Result<FileLockEntry<'a>>> + Sized
306    {
307        let viewer = TodoViewer { details: false };
308
309        let now = {
310            let now = chrono::offset::Local::now();
311            NaiveDateTime::new(now.date().naive_local(), now.time())
312        };
313
314        let filter_hidden = |todo: &FileLockEntry<'_>| -> Result<bool> {
315            Ok(todo.get_hidden()?.map(|hid| hid > now).unwrap_or(true))
316        };
317
318        iter
319            .filter_map(|r| {
320                match r.and_then(|e| e.get_status().map(|s| (s, e))) {
321                    Err(e) => Some(Err(e)),
322                    Ok((st, e)) => if matcher.matches(st) {
323                        Some(Ok(e))
324                    } else {
325                        None
326                    }
327                }
328            })
329            .and_then_ok(|entry| {
330                if !rt.output_is_pipe() && (show_hidden || filter_hidden.filter(&entry)?) {
331                    if let Err(e) = viewer.view_entry(&entry, &mut rt.stdout()) {
332                        use libimagentryview::error::Error;
333                        match e {
334                            Error::Other(e) => return Err(e),
335                            Error::Io(e)    => if e.kind() != std::io::ErrorKind::BrokenPipe {
336                                return Err(failure::Error::from(e))
337                            },
338                        }
339                    }
340                }
341
342                rt.report_touched(entry.get_location())
343            })
344            .collect()
345    };
346
347    if rt.ids_from_stdin() {
348        let iter = rt.ids::<crate::ui::PathProvider>()?
349            .ok_or_else(|| err_msg("No ids supplied"))?
350            .into_iter()
351            .map(Ok)
352            .into_get_iter(rt.store())
353            .map_inner_ok_or_else(|| err_msg("Did not find one entry"));
354
355        process(&rt, matcher, show_hidden, iter)
356    } else {
357        let iter = rt.store().get_todos()?
358            .into_get_iter()
359            .map_inner_ok_or_else(|| err_msg("Did not find one entry"));
360
361        process(&rt, matcher, show_hidden, iter)
362    }
363}
364
365/// Generic todo items list function
366///
367/// This sets up filtes based on the command line and prints out a list of todos
368fn list(rt: &Runtime) -> Result<()> {
369    debug!("Listing todo");
370    let scmd      = rt.cli().subcommand().1;
371    let table     = scmd.map(|s| s.is_present("list-table")).unwrap_or(true);
372    let hidden    = scmd.map(|s| s.is_present("list-hidden")).unwrap_or(false);
373    let done      = scmd.map(|s| s.is_present("list-done")).unwrap_or(false);
374    let nopending = scmd.map(|s| s.is_present("list-nopending")).unwrap_or(true);
375
376    trace!("table     = {}", table);
377    trace!("hidden    = {}", hidden);
378    trace!("done      = {}", done);
379    trace!("nopending = {}", nopending);
380
381    let mut matcher = StatusMatcher::new();
382    if !done { matcher.add_is_not(Status::Done); }
383    if nopending { matcher.add_is_not(Status::Pending); }
384
385    // TODO: Support printing as ASCII table
386    list_todos(rt, &matcher, hidden)
387}
388
389fn show(rt: &Runtime) -> Result<()> {
390    let scmd        = rt.cli().subcommand_matches("show").unwrap();
391    let show_format = util::get_todo_print_format("todo.show_format", rt, &scmd)?;
392    let out         = rt.stdout();
393    let mut outlock = out.lock();
394
395    fn show_with_table<'a, I>(rt: &Runtime, iter: I) -> Result<()>
396        where I: Iterator<Item = FileLockEntry<'a>>
397    {
398        const HEADER: &[&str] = &[
399            "uuid",
400            "status",
401            "sched",
402            "hidden",
403            "due",
404            "priority",
405            "text",
406        ];
407
408        let mut table = {
409            let mut t = Table::new();
410            let header = HEADER.iter().map(|s| Cell::new(s)).collect::<Vec<Cell>>();
411            t.set_titles(Row::from(header));
412            t
413        };
414
415        iter.map(|entry| {
416            use libimagentryview::error::Error as E;
417
418            let uuid     = entry.get_uuid().map_err(E::from)?.to_hyphenated().to_string();
419            let status   = entry.get_status().map_err(E::from)?;
420            let status   = status.as_str().to_string();
421            let sched    = util::get_dt_str(entry.get_scheduled(), "Not scheduled")?;
422            let hidden   = util::get_dt_str(entry.get_hidden(), "Not hidden")?;
423            let due      = util::get_dt_str(entry.get_due(), "No due")?;
424            let priority = entry.get_priority().map_err(E::from)?.map(|p| p.as_str().to_string()).unwrap_or_else(|| "No prio".to_string());
425
426            let text     = entry.get_content().to_owned();
427
428            let v = [
429                uuid,
430                status,
431                sched,
432                hidden,
433                due,
434                priority,
435                text,
436            ];
437            table.add_row(v.iter().map(|s| Cell::new(s)).collect());
438
439            Ok(entry)
440        })
441        .and_then_ok(|e| rt.report_touched(e.get_location()).map_err(Error::from).map(|_| e))
442        .collect::<Result<Vec<_>>>()?;
443
444        table.print(&mut rt.stdout())
445            .map(|_| ())
446            .map_err(Error::from)
447    }
448
449    let iter = rt
450        .ids::<crate::ui::PathProvider>()?
451        .ok_or_else(|| err_msg("No ids supplied"))?
452        .into_iter()
453        .map(Ok)
454        .into_get_iter(rt.store())
455        .map_inner_ok_or_else(|| err_msg("Did not find one entry"))
456        .and_then_ok(|e| rt.report_touched(e.get_location()).map(|_| e))
457        .collect::<Result<Vec<_>>>()?
458        .into_iter();
459
460    if scmd.is_present("show-no-table") {
461        iter.enumerate()
462            .map(|(i, elem)| {
463                let data = util::build_data_object_for_handlebars(i, elem.deref())?;
464                let s = show_format.render("format", &data)?;
465                writeln!(outlock, "{}", s).map_err(Error::from)
466            })
467            .collect()
468    } else {
469        show_with_table(rt, iter)
470    }
471}
472
473//
474// utility functions
475//
476
477fn get_datetime_arg(scmd: &ArgMatches, argname: &'static str) -> Result<Option<NaiveDateTime>> {
478    use kairos::timetype::TimeType;
479    use kairos::parser;
480
481    match scmd.value_of(argname) {
482        None => Ok(None),
483        Some(v) => match parser::parse(v)? {
484            parser::Parsed::TimeType(TimeType::Moment(moment)) => Ok(Some(moment)),
485            parser::Parsed::TimeType(other) => {
486                Err(format_err!("You did not pass a date, but a {}", other.name()))
487            },
488            parser::Parsed::Iterator(_) => {
489                Err(format_err!("Argument {} results in a list of dates, but we need a single date.", v))
490            }
491        }
492    }
493}
494
495fn prio_from_str<S: AsRef<str>>(s: S) -> Result<Priority> {
496    match s.as_ref() {
497        "h" => Ok(Priority::High),
498        "m" => Ok(Priority::Medium),
499        "l" => Ok(Priority::Low),
500        other => Err(format_err!("Unsupported Priority: '{}'", other)),
501    }
502}
503