1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
use std::collections::BTreeSet;
use std::fmt::{Display, Formatter};

use pest_consume::Parser;

use crate::todo::body_token::TodoBodyToken;
use crate::todo::parser::TodoParserError;
use crate::todo::parser::{Rule, TodoParser};
use crate::TODO_DATE_FORMAT;

mod body_token;
mod builder;
mod parser;

#[derive(Eq, PartialEq, Debug)]
pub struct Todo {
    // TODO: make this read-only, getter-only?
    pub is_completed: bool,
    pub priority: Option<char>,
    pub creation_date: Option<time::Date>,
    pub completion_date: Option<time::Date>,
    body_tokens: Vec<TodoBodyToken>,
    pub threshold_date: Option<time::Date>,
    pub due_date: Option<time::Date>,
    pub contexts: BTreeSet<String>,
    pub projects: BTreeSet<String>,
    pub is_hidden: bool,
    // TODO: dependent tasks + NANO IDs
    // https://cdn.rawgit.com/bram85/topydo/master/docs/index.html#Dependencies
    // is_blocking_for: Vec<ID>,
    // is_blocked: bool,
    // id: Option<ID>,
}

#[allow(dead_code)]
impl Todo {
    pub fn has_context(&self, context: &str) -> bool {
        if !context.starts_with('@') {
            let c = "@".to_string() + context;
            return self.has_context(&c);
        }
        self.contexts.contains(context)
    }

    pub fn has_project(&self, project: &str) -> bool {
        if !project.starts_with('+') {
            let c = "+".to_string() + project;
            return self.has_project(&c);
        }
        self.projects.contains(project)
    }

    pub fn canonical_context(&self) -> Option<String> {
        self.contexts.first().map(|s| s.to_owned())
    }

    pub fn canonical_project(&self) -> Option<String> {
        self.projects.first().map(|s| s.to_owned())
    }

    pub fn parse(input: &str) -> std::result::Result<Todo, TodoParserError> {
        let input = input.trim();
        if input.is_empty() {
            return Err(TodoParserError::EmptyInput);
        }
        let nodes = TodoParser::parse(Rule::todo, input)?;
        let node = nodes.single()?;
        let todo = TodoParser::todo(node)?;
        Ok(todo)
    }
}

impl Display for Todo {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let mut tokens = Vec::new();
        if self.is_completed {
            tokens.push("x".to_string())
        };
        if let Some(d) = self.completion_date {
            tokens.push(d.format(TODO_DATE_FORMAT))
        };
        if let Some(p) = self.priority {
            tokens.push(format!("({})", p.to_uppercase()))
        };
        if let Some(d) = self.creation_date {
            tokens.push(d.format(TODO_DATE_FORMAT))
        };
        for t in &self.body_tokens {
            tokens.push(format!("{}", t))
        }
        write!(f, "{}", tokens.join(" "))?;
        Ok(())
    }
}

#[cfg(test)]
mod test {
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn round_tripping_a_todo_with_display() {
        let input = "x 2021-01-03 (A) 2021-01-02 impl Display for Todos @pc +tudor due:2021-01-01 t:2021-01-01";
        let t = Todo::parse(input).unwrap();
        let got = format!("{}", t);
        assert_eq!(input, got);
    }
}