diff --git a/src/cli.rs b/src/cli.rs index 25d064d..027bc60 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -13,6 +13,7 @@ use crate::files::{self, Files}; use self::error::Result; use self::layout::line::LineLayout; +mod cancel; mod done; mod error; mod layout; @@ -49,6 +50,12 @@ pub enum Command { #[structopt(required = true)] entries: Vec, }, + /// Marks one or more entries as canceled + Cancel { + /// Entries to mark as done + #[structopt(required = true)] + entries: Vec, + }, /// Reformat all loaded files Fmt, } @@ -107,6 +114,14 @@ fn run_command(opt: &Opt, files: &mut Files, range: DateRange, now: NaiveDateTim let layout = find_layout(files, &entries, range, now); print::print(&layout); } + Some(Command::Cancel { entries: ns }) => { + let entries = find_entries(files, range)?; + let layout = find_layout(files, &entries, range, now); + cancel::cancel(files, &entries, &layout, ns, now)?; + let entries = find_entries(files, range)?; + let layout = find_layout(files, &entries, range, now); + print::print(&layout); + } Some(Command::Fmt) => files.mark_all_dirty(), } Ok(()) diff --git a/src/cli/cancel.rs b/src/cli/cancel.rs new file mode 100644 index 0000000..62cc8ea --- /dev/null +++ b/src/cli/cancel.rs @@ -0,0 +1,37 @@ +use std::vec; + +use chrono::NaiveDateTime; + +use crate::eval::Entry; +use crate::files::commands::{Done, DoneKind}; +use crate::files::Files; + +use super::error::{Error, Result}; +use super::layout::line::LineLayout; + +pub fn cancel( + files: &mut Files, + entries: &[Entry], + layout: &LineLayout, + numbers: &[usize], + now: NaiveDateTime, +) -> Result<()> { + let mut not_tasks = vec![]; + for &number in numbers { + let entry = &entries[layout.look_up_number(number)?]; + let done = Done { + kind: DoneKind::Canceled, + date: entry.dates.map(|dates| dates.into()), + done_at: now.date(), + }; + if !files.add_done(entry.source, done) { + not_tasks.push(number); + } + } + + if not_tasks.is_empty() { + Ok(()) + } else { + Err(Error::NotATask(not_tasks)) + } +} diff --git a/src/cli/done.rs b/src/cli/done.rs index 379d6ce..1d3d809 100644 --- a/src/cli/done.rs +++ b/src/cli/done.rs @@ -3,7 +3,7 @@ use std::vec; use chrono::NaiveDateTime; use crate::eval::Entry; -use crate::files::commands::Done; +use crate::files::commands::{Done, DoneKind}; use crate::files::Files; use super::error::{Error, Result}; @@ -20,6 +20,7 @@ pub fn done( for &number in numbers { let entry = &entries[layout.look_up_number(number)?]; let done = Done { + kind: DoneKind::Done, date: entry.dates.map(|dates| dates.into()), done_at: now.date(), }; diff --git a/src/cli/layout/day.rs b/src/cli/layout/day.rs index e326307..30db056 100644 --- a/src/cli/layout/day.rs +++ b/src/cli/layout/day.rs @@ -73,7 +73,9 @@ impl DayLayout { fn layout_entry(&mut self, index: usize, entry: &Entry) { match entry.kind { EntryKind::Task => self.layout_task(index, entry), - EntryKind::TaskDone(at) => self.layout_task_done(index, entry, at), + EntryKind::TaskDone(at) | EntryKind::TaskCanceled(at) => { + self.layout_task_done(index, entry, at) + } EntryKind::Note | EntryKind::Birthday(_) => self.layout_note(index, entry), } } @@ -208,7 +210,7 @@ impl DayLayout { // 3. entries.sort_by_key(|(_, e, _)| match e.kind { EntryKind::Task => 0, - EntryKind::TaskDone(_) => 1, + EntryKind::TaskDone(_) | EntryKind::TaskCanceled(_) => 1, EntryKind::Birthday(_) => 2, EntryKind::Note => 3, }); diff --git a/src/cli/layout/line.rs b/src/cli/layout/line.rs index e5e8412..637d963 100644 --- a/src/cli/layout/line.rs +++ b/src/cli/layout/line.rs @@ -60,6 +60,7 @@ pub enum Times { pub enum LineKind { Task, Done, + Canceled, Note, Birthday, } @@ -245,6 +246,7 @@ impl LineLayout { match entry.kind { EntryKind::Task => LineKind::Task, EntryKind::TaskDone(_) => LineKind::Done, + EntryKind::TaskCanceled(_) => LineKind::Canceled, EntryKind::Note => LineKind::Note, EntryKind::Birthday(_) => LineKind::Birthday, } diff --git a/src/cli/print.rs b/src/cli/print.rs index 22f5108..657cf1e 100644 --- a/src/cli/print.rs +++ b/src/cli/print.rs @@ -134,6 +134,7 @@ impl ShowLines { match kind { LineKind::Task => "T".magenta().bold(), LineKind::Done => "D".green().bold(), + LineKind::Canceled => "C".red().bold(), LineKind::Note => "N".blue().bold(), LineKind::Birthday => "B".yellow().bold(), } diff --git a/src/cli/show.rs b/src/cli/show.rs index 1b20413..81cac22 100644 --- a/src/cli/show.rs +++ b/src/cli/show.rs @@ -13,6 +13,10 @@ fn show_entry(files: &Files, entry: &Entry) { println!("DONE {}", command.title()); println!("DONE AT {}", when); } + EntryKind::TaskCanceled(when) => { + println!("CANCELED {}", command.title()); + println!("CANCELED AT {}", when); + } EntryKind::Note => println!("NOTE {}", command.title()), EntryKind::Birthday(Some(age)) => { println!("BIRTHDAY {}", command.title()); diff --git a/src/eval/command.rs b/src/eval/command.rs index e65e6e6..b0ec4a5 100644 --- a/src/eval/command.rs +++ b/src/eval/command.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use chrono::{Duration, NaiveDate}; use crate::files::commands::{ - self, BirthdaySpec, Command, Done, DoneDate, Note, Spec, Statement, Task, + self, BirthdaySpec, Command, Done, DoneDate, DoneKind, Note, Spec, Statement, Task, }; use crate::files::primitives::{Span, Spanned, Time}; use crate::files::SourcedCommand; @@ -296,10 +296,12 @@ impl<'a> CommandState<'a> { } fn eval_done(&mut self, done: &Done) -> Result<()> { - self.add_forced(self.entry_with_remind( - EntryKind::TaskDone(done.done_at), - done.date.map(|date| date.into()), - )?); + let kind = match done.kind { + DoneKind::Done => EntryKind::TaskDone(done.done_at), + DoneKind::Canceled => EntryKind::TaskCanceled(done.done_at), + }; + let dates = done.date.map(|date| date.into()); + self.add_forced(self.entry_with_remind(kind, dates)?); Ok(()) } } diff --git a/src/eval/entry.rs b/src/eval/entry.rs index 303900d..09d51a0 100644 --- a/src/eval/entry.rs +++ b/src/eval/entry.rs @@ -9,6 +9,7 @@ use super::range::DateRange; pub enum EntryKind { Task, TaskDone(NaiveDate), + TaskCanceled(NaiveDate), Note, Birthday(Option), } @@ -120,7 +121,7 @@ impl Entries { } // Tasks that were finished inside the range - if let EntryKind::TaskDone(done) = entry.kind { + if let EntryKind::TaskDone(done) | EntryKind::TaskCanceled(done) = entry.kind { if self.range.contains(done) { return true; } diff --git a/src/files/commands.rs b/src/files/commands.rs index 76166b0..7d1ec2d 100644 --- a/src/files/commands.rs +++ b/src/files/commands.rs @@ -309,8 +309,15 @@ impl DoneDate { } } +#[derive(Debug)] +pub enum DoneKind { + Done, + Canceled, +} + #[derive(Debug)] pub struct Done { + pub kind: DoneKind, pub date: Option, pub done_at: NaiveDate, } diff --git a/src/files/format.rs b/src/files/format.rs index e169142..0694462 100644 --- a/src/files/format.rs +++ b/src/files/format.rs @@ -2,6 +2,8 @@ use std::fmt; use chrono::Datelike; +use crate::files::commands::DoneKind; + use super::commands::{ BirthdaySpec, Command, DateSpec, Delta, DeltaStep, Done, DoneDate, Expr, File, FormulaSpec, Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec, @@ -258,7 +260,11 @@ impl fmt::Display for DoneDate { impl fmt::Display for Done { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "DONE [{}]", self.done_at)?; + let kind = match self.kind { + DoneKind::Done => "DONE", + DoneKind::Canceled => "CANCELED", + }; + write!(f, "{} [{}]", kind, self.done_at)?; if let Some(date) = &self.date { write!(f, " {}", date)?; } diff --git a/src/files/grammar.pest b/src/files/grammar.pest index a2712be..24e9a51 100644 --- a/src/files/grammar.pest +++ b/src/files/grammar.pest @@ -118,7 +118,8 @@ donedate = { | datum ~ "--" ~ datum | datum } -done = !{ "DONE" ~ "[" ~ datum ~ "]" ~ donedate? ~ eol } +done_kind = { "DONE" | "CANCELED" } +done = !{ done_kind ~ "[" ~ datum ~ "]" ~ donedate? ~ eol } dones = { done* } desc_line = { "#" ~ (" " ~ rest_any)? ~ eol } diff --git a/src/files/parse.rs b/src/files/parse.rs index 8a756f7..fc869e5 100644 --- a/src/files/parse.rs +++ b/src/files/parse.rs @@ -8,8 +8,8 @@ use pest::prec_climber::{Assoc, Operator, PrecClimber}; use pest::{Parser, Span}; use super::commands::{ - BirthdaySpec, Command, DateSpec, Delta, DeltaStep, Done, DoneDate, Expr, File, FormulaSpec, - Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec, + BirthdaySpec, Command, DateSpec, Delta, DeltaStep, Done, DoneDate, DoneKind, Expr, File, + FormulaSpec, Note, Repeat, Spec, Statement, Task, Var, WeekdaySpec, }; use super::primitives::{Spanned, Time, Weekday}; @@ -704,10 +704,20 @@ fn parse_donedate(p: Pair<'_, Rule>) -> Result { }) } +fn parse_done_kind(p: Pair<'_, Rule>) -> DoneKind { + assert_eq!(p.as_rule(), Rule::done_kind); + match p.as_str() { + "DONE" => DoneKind::Done, + "CANCELED" => DoneKind::Canceled, + _ => unreachable!(), + } +} + fn parse_done(p: Pair<'_, Rule>) -> Result { assert_eq!(p.as_rule(), Rule::done); let mut p = p.into_inner(); + let kind = parse_done_kind(p.next().unwrap()); let done_at = parse_datum(p.next().unwrap())?.value; let date = if let Some(p) = p.next() { Some(parse_donedate(p)?) @@ -717,7 +727,11 @@ fn parse_done(p: Pair<'_, Rule>) -> Result { assert_eq!(p.next(), None); - Ok(Done { date, done_at }) + Ok(Done { + kind, + date, + done_at, + }) } fn parse_dones(p: Pair<'_, Rule>) -> Result> {