// // imag - the personal information management suite for the commandline // Copyright (C) 2015-2020 Matthias Beyer and contributors // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; version // 2.1 of the License. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // #![forbid(unsafe_code)] #![deny( non_camel_case_types, non_snake_case, path_statements, trivial_numeric_casts, unstable_features, unused_allocation, unused_import_braces, unused_imports, unused_must_use, unused_mut, unused_qualifications, while_true, )] extern crate clap; extern crate toml; extern crate toml_query; extern crate chrono; extern crate filters; extern crate kairos; #[macro_use] extern crate log; #[macro_use] extern crate failure; extern crate resiter; extern crate handlebars; extern crate prettytable; #[cfg(feature = "import-taskwarrior")] extern crate task_hookrs; #[cfg(feature = "import-taskwarrior")] extern crate uuid; #[cfg(feature = "import-taskwarrior")] extern crate libimagentrytag; #[cfg(feature = "import-taskwarrior")] extern crate libimagentrylink; extern crate libimagrt; extern crate libimagstore; extern crate libimagerror; extern crate libimagentryedit; extern crate libimagtodo; extern crate libimagutil; extern crate libimagentryview; extern crate libimaginteraction; use std::ops::Deref; use std::io::Write; use std::result::Result as RResult; use std::str::FromStr; use clap::ArgMatches; use chrono::NaiveDateTime; use failure::Error; use failure::Fallible as Result; use failure::err_msg; use clap::App; use resiter::AndThen; use resiter::IterInnerOkOrElse; use prettytable::Table; use prettytable::Cell; use prettytable::Row; use libimagentryedit::edit::Edit; use libimagentryview::viewer::Viewer; use libimagentryview::viewer::IntoViewIter; use libimagrt::application::ImagApplication; use libimagrt::runtime::Runtime; use libimagstore::iter::get::*; use libimagstore::store::Entry; use libimagstore::store::FileLockEntry; use libimagtodo::entry::Todo; use libimagtodo::priority::Priority; use libimagtodo::status::Status; use libimagtodo::store::TodoStore; use libimagrt::runtime::IntoTouchIterator; mod ui; mod import; mod util; /// Marker enum for implementing ImagApplication on /// /// This is used by binaries crates to execute business logic /// or to build a CLI completion. pub enum ImagTodo {} impl ImagApplication for ImagTodo { fn run(rt: Runtime) -> Result<()> { match rt.cli().subcommand_name() { Some("create") => create(&rt), Some("show") => show(&rt), Some("mark") => mark(&rt), Some("pending") | None => list_todos(&rt, &StatusMatcher::new().is(Status::Pending), false), Some("list") => list(&rt), Some("import") => import::import(&rt), Some(other) => { debug!("Unknown command"); if rt.handle_unknown_subcommand("imag-todo", other, rt.cli())?.success() { Ok(()) } else { Err(err_msg("Failed to handle unknown subcommand")) } } } // end match scmd } fn build_cli<'a>(app: App<'a, 'a>) -> App<'a, 'a> { ui::build_ui(app) } fn name() -> &'static str { env!("CARGO_PKG_NAME") } fn description() -> &'static str { "Interface with taskwarrior" } fn version() -> &'static str { env!("CARGO_PKG_VERSION") } } /// A black- and whitelist for matching statuses of todo entries /// /// The blacklist is checked first, followed by the whitelist. /// In case the whitelist is empty, the StatusMatcher works with a /// blacklist-only approach. #[derive(Debug, Default)] pub struct StatusMatcher { is: Vec, is_not: Vec, } impl StatusMatcher { pub fn new() -> Self { StatusMatcher { ..Default::default() } } pub fn is(mut self, s: Status) -> Self { self.is.push(s); self } #[allow(clippy::wrong_self_convention)] pub fn is_not(mut self, s: Status) -> Self { self.is_not.push(s); self } pub fn matches(&self, todo: Status) -> bool { if self.is_not.iter().any(|t| *t == todo) { // On blacklist false } else { self.is.iter().any(|t| *t == todo) } } } fn create(rt: &Runtime) -> Result<()> { debug!("Creating todo"); let scmd = rt.cli().subcommand().1.unwrap(); // safe by clap let scheduled: Option = get_datetime_arg(&scmd, "create-scheduled")?; let hidden: Option = get_datetime_arg(&scmd, "create-hidden")?; let due: Option = get_datetime_arg(&scmd, "create-due")?; let prio: Option = scmd.value_of("create-prio").map(prio_from_str).transpose()?; let status: Status = scmd.value_of("create-status").map(Status::from_str).unwrap()?; let edit = scmd.is_present("create-edit"); let text = scmd.value_of("text").unwrap(); trace!("Creating todo with these variables:"); trace!("scheduled = {:?}", scheduled); trace!("hidden = {:?}", hidden); trace!("due = {:?}", due); trace!("prio = {:?}", prio); trace!("status = {:?}", status); trace!("edit = {}", edit); trace!("text = {:?}", text); let mut entry = rt.store().create_todo(status, scheduled, hidden, due, prio, true)?; debug!("Created: todo {}", entry.get_uuid()?); debug!("Setting content"); *entry.get_content_mut() = text.to_string(); if edit { debug!("Editing content"); entry.edit_content(&rt)?; } rt.report_touched(entry.get_location()) } fn mark(rt: &Runtime) -> Result<()> { fn mark_todos_as(rt: &Runtime, status: Status) -> Result<()> { rt.ids::()? .ok_or_else(|| err_msg("No ids supplied"))? .into_iter() .map(Ok) .into_get_iter(rt.store()) .map_inner_ok_or_else(|| err_msg("Did not find one entry")) .and_then_ok(|e| rt.report_touched(e.get_location()).map(|_| e)) .and_then_ok(|mut e| e.set_status(status.clone())) .collect() } let scmd = rt.cli().subcommand().1.unwrap(); match scmd.subcommand_name() { Some("done") => mark_todos_as(rt, Status::Done), Some("deleted") => mark_todos_as(rt, Status::Deleted), Some("pending") => mark_todos_as(rt, Status::Pending), Some(other) => Err(format_err!("Unknown mark type selected: {}", other)), None => Err(format_err!("No mark type selected, doing nothing!")), } } /// Generic todo listing function /// /// Supports filtering of todos by status using the passed in StatusMatcher fn list_todos(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool) -> Result<()> { debug!("Listing todos with status filter {:?}", matcher); struct TodoViewer { details: bool, } impl Viewer for TodoViewer { fn view_entry(&self, entry: &Entry, sink: &mut W) -> RResult<(), libimagentryview::error::Error> where W: Write { use libimagentryview::error::Error as E; if !entry.is_todo().map_err(E::from)? { return Err(format_err!("Not a Todo: {}", entry.get_location())).map_err(E::from); } let uuid = entry.get_uuid().map_err(E::from)?; let status = entry.get_status().map_err(E::from)?; let status = status.as_str(); let first_line = entry.get_content() .lines() .next() .unwrap_or(""); if !self.details { writeln!(sink, "{uuid} - {status} : {first_line}", uuid = uuid, status = status, first_line = first_line) } else { let sched = util::get_dt_str(entry.get_scheduled(), "Not scheduled")?; let hidden = util::get_dt_str(entry.get_hidden(), "Not hidden")?; let due = util::get_dt_str(entry.get_due(), "No due")?; let priority = entry.get_priority().map_err(E::from)?.map(|p| p.as_str().to_string()) .unwrap_or_else(|| "No prio".to_string()); writeln!(sink, "{uuid} - {status} - {sched} - {hidden} - {due} - {prio}: {first_line}", uuid = uuid, status = status, sched = sched, hidden = hidden, due = due, prio = priority, first_line = first_line) } .map_err(libimagentryview::error::Error::from) } } fn process<'a, I>(rt: &Runtime, matcher: &StatusMatcher, show_hidden: bool, iter: I) -> Result<()> where I: Iterator>> + Sized { let viewer = TodoViewer { details: false }; let now = { let now = chrono::offset::Local::now(); NaiveDateTime::new(now.date().naive_local(), now.time()) }; let filter_hidden = |todo: &FileLockEntry<'_>| -> Result { Ok(todo.get_hidden()?.map(|hid| hid > now).unwrap_or(true)) }; iter .filter_map(|r| { match r.and_then(|e| e.get_status().map(|s| (s, e))) { Err(e) => Some(Err(e)), Ok((st, e)) => if matcher.matches(st) { Some(Ok(e)) } else { None } } }) .inspect(|e| trace!("Processing: {:?}", e)) .filter_map(|r| match r { Err(err) => Some(Err(err)), Ok(entry) => match filter_hidden(&entry).map(|b| b || show_hidden) { Ok(true) => Some(Ok(entry)), Ok(false) => None, Err(err) => Some(Err(err)), }, }) .view_all_if(viewer, &mut rt.stdout(), |r| r.as_ref().ok()) .and_then_ok(|e| e) .report_entries_touched(rt, |r| r.as_ref().ok().map(|e| e.get_location().clone())) .and_then_ok(|e| e) .collect::>>() .map(|_| ()) }; if rt.ids_from_stdin() { trace!("Getting IDs from stdin"); let iter = rt.ids::()? .ok_or_else(|| err_msg("No ids supplied"))? .into_iter() .map(Ok) .into_get_iter(rt.store()) .map_inner_ok_or_else(|| err_msg("Did not find one entry")); process(&rt, matcher, show_hidden, iter) } else { trace!("Getting IDs from store"); let iter = rt.store().get_todos()? .into_get_iter() .map_inner_ok_or_else(|| err_msg("Did not find one entry")); process(&rt, matcher, show_hidden, iter) } } /// Generic todo items list function /// /// This sets up filtes based on the command line and prints out a list of todos fn list(rt: &Runtime) -> Result<()> { debug!("Listing todo"); let scmd = rt.cli().subcommand().1; let table = scmd.map(|s| s.is_present("list-table")).unwrap_or(true); let hidden = scmd.map(|s| s.is_present("list-hidden")).unwrap_or(false); let done = scmd.map(|s| s.is_present("list-done")).unwrap_or(false); let nopending = scmd.map(|s| s.is_present("list-nopending")).unwrap_or(true); let deleted = scmd.map(|s| s.is_present("list-deleted")).unwrap_or(true); trace!("table = {}", table); trace!("hidden = {}", hidden); trace!("done = {}", done); trace!("nopending = {}", nopending); let matcher = { let mut matcher = if nopending { StatusMatcher::new().is_not(Status::Pending) } else { StatusMatcher::new().is(Status::Pending) }; if done { matcher = matcher.is(Status::Done) } if deleted { matcher = matcher.is(Status::Deleted) } matcher }; // TODO: Support printing as ASCII table list_todos(rt, &matcher, hidden) } fn show(rt: &Runtime) -> Result<()> { let scmd = rt.cli().subcommand_matches("show").unwrap(); let show_format = util::get_todo_print_format("todo.show_format", rt, &scmd)?; let out = rt.stdout(); let mut outlock = out.lock(); fn show_with_table<'a, I>(rt: &Runtime, iter: I) -> Result<()> where I: Iterator> { const HEADER: &[&str] = &[ "uuid", "status", "sched", "hidden", "due", "priority", "text", ]; let mut table = { let mut t = Table::new(); let header = HEADER.iter().map(|s| Cell::new(s)).collect::>(); t.set_titles(Row::from(header)); t }; iter.map(|entry| { use libimagentryview::error::Error as E; let uuid = entry.get_uuid().map_err(E::from)?.to_hyphenated().to_string(); let status = entry.get_status().map_err(E::from)?; let status = status.as_str().to_string(); let sched = util::get_dt_str(entry.get_scheduled(), "Not scheduled")?; let hidden = util::get_dt_str(entry.get_hidden(), "Not hidden")?; let due = util::get_dt_str(entry.get_due(), "No due")?; let priority = entry.get_priority().map_err(E::from)?.map(|p| p.as_str().to_string()).unwrap_or_else(|| "No prio".to_string()); let text = entry.get_content().to_owned(); let v = [ uuid, status, sched, hidden, due, priority, text, ]; table.add_row(v.iter().map(|s| Cell::new(s)).collect()); Ok(entry) }) .and_then_ok(|e| rt.report_touched(e.get_location()).map_err(Error::from).map(|_| e)) .collect::>>()?; table.print(&mut rt.stdout()) .map(|_| ()) .map_err(Error::from) } let iter = rt .ids::()? .ok_or_else(|| err_msg("No ids supplied"))? .into_iter() .map(Ok) .into_get_iter(rt.store()) .map_inner_ok_or_else(|| err_msg("Did not find one entry")) .and_then_ok(|e| rt.report_touched(e.get_location()).map(|_| e)) .collect::>>()? .into_iter(); if scmd.is_present("show-no-table") { iter.enumerate() .map(|(i, elem)| { let data = util::build_data_object_for_handlebars(i, elem.deref())?; let s = show_format.render("format", &data)?; writeln!(outlock, "{}", s).map_err(Error::from) }) .collect() } else { show_with_table(rt, iter) } } // // utility functions // fn get_datetime_arg(scmd: &ArgMatches, argname: &'static str) -> Result> { use kairos::timetype::TimeType; use kairos::parser; match scmd.value_of(argname) { None => Ok(None), Some(v) => match parser::parse(v)? { parser::Parsed::TimeType(TimeType::Moment(moment)) => Ok(Some(moment)), parser::Parsed::TimeType(other) => { Err(format_err!("You did not pass a date, but a {}", other.name())) }, parser::Parsed::Iterator(_) => { Err(format_err!("Argument {} results in a list of dates, but we need a single date.", v)) } } } } fn prio_from_str>(s: S) -> Result { match s.as_ref() { "h" => Ok(Priority::High), "m" => Ok(Priority::Medium), "l" => Ok(Priority::Low), other => Err(format_err!("Unsupported Priority: '{}'", other)), } }