tudor_sql/todo/
mod.rs

1use std::collections::BTreeSet;
2use std::fmt::{Display, Formatter};
3
4use pest_consume::Parser;
5
6use crate::todo::body_token::TodoBodyToken;
7use crate::todo::parser::TodoParserError;
8use crate::todo::parser::{Rule, TodoParser};
9use crate::TODO_DATE_FORMAT;
10
11mod body_token;
12mod builder;
13mod parser;
14
15#[derive(Eq, PartialEq, Debug)]
16pub struct Todo {
17    // TODO: make this read-only, getter-only?
18    pub is_completed: bool,
19    pub priority: Option<char>,
20    pub creation_date: Option<time::Date>,
21    pub completion_date: Option<time::Date>,
22    body_tokens: Vec<TodoBodyToken>,
23    pub threshold_date: Option<time::Date>,
24    pub due_date: Option<time::Date>,
25    pub contexts: BTreeSet<String>,
26    pub projects: BTreeSet<String>,
27    pub is_hidden: bool,
28    // TODO: dependent tasks + NANO IDs
29    // https://cdn.rawgit.com/bram85/topydo/master/docs/index.html#Dependencies
30    // is_blocking_for: Vec<ID>,
31    // is_blocked: bool,
32    // id: Option<ID>,
33}
34
35#[allow(dead_code)]
36impl Todo {
37    pub fn has_context(&self, context: &str) -> bool {
38        if !context.starts_with('@') {
39            let c = "@".to_string() + context;
40            return self.has_context(&c);
41        }
42        self.contexts.contains(context)
43    }
44
45    pub fn has_project(&self, project: &str) -> bool {
46        if !project.starts_with('+') {
47            let c = "+".to_string() + project;
48            return self.has_project(&c);
49        }
50        self.projects.contains(project)
51    }
52
53    pub fn canonical_context(&self) -> Option<String> {
54        self.contexts.first().map(|s| s.to_owned())
55    }
56
57    pub fn canonical_project(&self) -> Option<String> {
58        self.projects.first().map(|s| s.to_owned())
59    }
60
61    pub fn parse(input: &str) -> std::result::Result<Todo, TodoParserError> {
62        let input = input.trim();
63        if input.is_empty() {
64            return Err(TodoParserError::EmptyInput);
65        }
66        let nodes = TodoParser::parse(Rule::todo, input)?;
67        let node = nodes.single()?;
68        let todo = TodoParser::todo(node)?;
69        Ok(todo)
70    }
71}
72
73impl Display for Todo {
74    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
75        let mut tokens = Vec::new();
76        if self.is_completed {
77            tokens.push("x".to_string())
78        };
79        if let Some(d) = self.completion_date {
80            tokens.push(d.format(TODO_DATE_FORMAT))
81        };
82        if let Some(p) = self.priority {
83            tokens.push(format!("({})", p.to_uppercase()))
84        };
85        if let Some(d) = self.creation_date {
86            tokens.push(d.format(TODO_DATE_FORMAT))
87        };
88        for t in &self.body_tokens {
89            tokens.push(format!("{}", t))
90        }
91        write!(f, "{}", tokens.join(" "))?;
92        Ok(())
93    }
94}
95
96#[cfg(test)]
97mod test {
98    use pretty_assertions::assert_eq;
99
100    use super::*;
101
102    #[test]
103    fn round_tripping_a_todo_with_display() {
104        let input = "x 2021-01-03 (A) 2021-01-02 impl Display for Todos @pc +tudor due:2021-01-01 t:2021-01-01";
105        let t = Todo::parse(input).unwrap();
106        let got = format!("{}", t);
107        assert_eq!(input, got);
108    }
109}