todotxt-tui 0.2.1

Todo.txt TUI is a highly customizable terminal-based application for managing your todo tasks. It follows the todo.txt format and offers a wide range of configuration options to suit your needs.
Documentation
mod line;
mod line_block;
mod parts;

use super::{ToDo, ToDoData};
use crate::{config::Styles, Result, ToDoError};
use line::Line;
use line_block::LineBlock;
use parts::Parts;
use std::iter::Peekable;
use tui::style::Style;

pub struct Parser {
    lines: Vec<Line>,
    styles: Styles,
}

impl Parser {
    pub fn new(value: &str, styles: Styles) -> Result<Self> {
        let lines = Parser::parse(value, &styles)?;
        log::debug!("Loaded parser: {:#?}", lines);
        Ok(Parser { lines, styles })
    }

    fn read_block(iter: &mut Peekable<std::str::Chars<'_>>, delimiter: char) -> Result<String> {
        let mut read = String::default();
        loop {
            let c = match iter.next() {
                Some(c) => c,
                None => return Err(ToDoError::ParseBlockNotClosed(read.to_string())),
            };
            match c {
                '\\' => read.push(match iter.next() {
                    Some(ch) => ch,
                    None => return Err(ToDoError::ParseBlockEscapeOnEnd(read + "\\")),
                }),
                c if c == delimiter => break,
                _ => read.push(c),
            };
        }
        Ok(read)
    }

    fn parse(template: &str, styles: &Styles) -> Result<Vec<Line>> {
        let mut ret = Vec::new();
        let mut line = Line::default();
        let mut act = String::default();
        let mut iter = template.chars().peekable();
        while let Some(c) = iter.next() {
            match c {
                '[' => {
                    let block = Parser::read_block(&mut iter, ']')?;
                    line.add_span_styled(&act, None, styles)?;
                    act = String::default();
                    let mut style = None;
                    if Some(&'(') == iter.peek() {
                        iter.next();
                        style = Some(Parser::read_block(&mut iter, ')')?);
                    }
                    line.add_span_styled(&block, style, styles)?;
                }
                '\\' => act.push(match iter.next() {
                    Some(ch) => ch,
                    None => return Err(ToDoError::ParseBlockEscapeOnEnd(act + "\\")),
                }),
                '\n' => {
                    line.add_span_styled(&act, None, styles)?;
                    act = String::default();
                    ret.push(line);
                    line = Line::default();
                }
                _ => act.push(c),
            }
        }
        line.add_span_styled(&act, None, styles)?;
        ret.push(line);
        Ok(ret)
    }

    pub fn fill(&self, todo: &ToDo) -> Vec<Vec<(String, Style)>> {
        self.lines
            .iter()
            .filter_map(|line| line.fill(todo, &self.styles))
            .collect()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::StylesValue;
    use tui::style::{Color, Modifier};

    #[test]
    fn read_block() -> Result<()> {
        let mut iter = "block to parse]".chars().peekable();
        assert_eq!(&Parser::read_block(&mut iter, ']')?, "block to parse");
        assert_eq!(&iter.collect::<String>(), "");

        let mut iter = "Some style block)".chars().peekable();
        assert_eq!(&Parser::read_block(&mut iter, ')')?, "Some style block");
        assert_eq!(&iter.collect::<String>(), "");

        let mut iter = "block to parse] some other text".chars().peekable();
        assert_eq!(&Parser::read_block(&mut iter, ']')?, "block to parse");
        assert_eq!(&iter.collect::<String>(), " some other text");

        let mut iter = "block to parse \\] with some \\\\ escapes]"
            .chars()
            .peekable();
        assert_eq!(
            &Parser::read_block(&mut iter, ']')?,
            "block to parse ] with some \\ escapes"
        );
        assert_eq!(&iter.collect::<String>(), "");

        Ok(())
    }

    #[test]
    fn read_block_error() {
        let mut iter = "not closed block".chars().peekable();
        assert_eq!(
            Parser::read_block(&mut iter, ']').unwrap_err().to_string(),
            ToDoError::ParseBlockNotClosed("not closed block".to_string()).to_string()
        );

        let mut iter = "not closed block \\".chars().peekable();
        assert_eq!(
            Parser::read_block(&mut iter, ']').unwrap_err().to_string(),
            ToDoError::ParseBlockEscapeOnEnd("not closed block \\".to_string()).to_string()
        );
    }

    #[test]
    fn parse() -> Result<()> {
        assert_eq!(Parser::parse("", &Styles::default())?[0], Line::default());
        assert_eq!(
            Parser::parse("some text", &Styles::default())?[0],
            Line(vec![LineBlock {
                parts: vec![Parts::Text("some text".to_string())],
                style: StylesValue::default(),
            }])
        );
        assert_eq!(
            Parser::parse("some text \\[ with escapes \\]", &Styles::default())?[0],
            Line(vec![LineBlock {
                parts: vec![Parts::Text("some text [ with escapes ]".to_string())],
                style: StylesValue::default(),
            }])
        );
        assert_eq!(
            Parser::parse("[some text](Red)", &Styles::default())?[0],
            Line(vec![LineBlock {
                parts: vec![Parts::Text("some text".to_string())],
                style: Style::default().fg(Color::Red).into(),
            }])
        );
        assert_eq!(
            Parser::parse("[some text] and another text", &Styles::default())?[0],
            Line(vec![
                LineBlock {
                    parts: vec![Parts::Text("some text".to_string())],
                    style: StylesValue::default(),
                },
                LineBlock {
                    parts: vec![Parts::Text(" and another text".to_string())],
                    style: StylesValue::default(),
                }
            ])
        );
        assert_eq!(
            Parser::parse("[some text]\\[ and escaped text \\]", &Styles::default())?[0],
            Line(vec![
                LineBlock {
                    parts: vec![Parts::Text("some text".to_string())],
                    style: StylesValue::default(),
                },
                LineBlock {
                    parts: vec![Parts::Text("[ and escaped text ]".to_string())],
                    style: StylesValue::default(),
                }
            ])
        );
        assert_eq!(
            Parser::parse("[some text]", &Styles::default())?[0],
            Line(vec![LineBlock {
                parts: vec![Parts::Text("some text".to_string())],
                style: StylesValue::default(),
            }])
        );
        assert_eq!(
            Parser::parse(
                "[some text](red) text between [another text](blue bold)",
                &Styles::default()
            )?[0],
            Line(vec![
                LineBlock {
                    parts: vec![Parts::Text("some text".to_string())],
                    style: Style::default().fg(Color::Red).into(),
                },
                LineBlock {
                    parts: vec![Parts::Text(" text between ".to_string())],
                    style: StylesValue::default(),
                },
                LineBlock {
                    parts: vec![Parts::Text("another text".to_string())],
                    style: Style::default()
                        .fg(Color::Blue)
                        .add_modifier(Modifier::BOLD)
                        .into(),
                }
            ])
        );
        assert_eq!(
            Parser::parse("[some text](priority:A)", &Styles::default())?[0],
            Line(vec![LineBlock {
                parts: vec![Parts::Text("some text".to_string())],
                style: Style::default().fg(Color::Red).into(),
            },])
        );
        let parse = Parser::parse("some text\nnew line", &Styles::default())?;
        assert_eq!(parse.len(), 2);
        assert_eq!(
            parse[0],
            Line(vec![LineBlock {
                parts: vec![Parts::Text("some text".to_string())],
                style: StylesValue::default(),
            }])
        );
        assert_eq!(
            parse[1],
            Line(vec![LineBlock {
                parts: vec![Parts::Text("new line".to_string())],
                style: StylesValue::default(),
            }])
        );

        Ok(())
    }

    #[test]
    fn parse_error() {
        assert_eq!(
            Parser::parse("escape on end of line \\", &Styles::default())
                .unwrap_err()
                .to_string(),
            ToDoError::ParseBlockEscapeOnEnd("escape on end of line \\".to_string()).to_string()
        );
    }

    #[test]
    fn fill_base() -> Result<()> {
        let parser = Parser::new("some text", Styles::default())?;
        let mut todo = ToDo::default();
        todo.new_task("task").unwrap();
        todo.new_task("x done task").unwrap();

        assert_eq!(parser.fill(&todo), Vec::<Vec<(String, Style)>>::new());

        todo.set_active(ToDoData::Pending, 0);
        assert_eq!(
            parser.fill(&todo),
            vec![vec![(String::from("some text"), Style::default())]]
        );

        Ok(())
    }

    #[test]
    fn fill_counts() -> Result<()> {
        let parser = Parser::new("Done: $done Pending: $pending", Styles::default())?;
        let mut todo = ToDo::default();
        todo.new_task("task").unwrap();
        todo.new_task("x done task").unwrap();
        todo.set_active(ToDoData::Pending, 0);

        assert_eq!(
            parser.fill(&todo),
            vec![vec![(String::from("Done: 1 Pending: 1"), Style::default())]]
        );

        Ok(())
    }
}