1#![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
103pub 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 } }
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#[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 false
184 } else {
185 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(); 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
251fn 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
365fn 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 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
473fn 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