todotxt-tui 0.3.0

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
use super::{
    parts::{PartStyle, Parts},
    LineBlock, ToDo,
};
use crate::{
    config::Styles,
    todo::parser::{ParserGrammar, Rule},
};
use anyhow::Context;
use pest::{iterators::Pairs, Parser};
use std::{ops::Deref, str::FromStr};
use todo_txt::task::Simple as Task;
use tui::style::Style;

#[derive(Default, Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Line(Vec<LineBlock>);

impl Line {
    pub fn fill(&self, task: &Task, todo: &ToDo, styles: &Styles) -> Option<Vec<(String, Style)>> {
        let ret: Vec<_> = self
            .0
            .iter()
            .filter_map(|block| block.fill(task, todo, styles))
            .flatten()
            .filter(|(part, _style)| !part.is_empty())
            .collect();
        if ret.is_empty() {
            None
        } else {
            Some(ret)
        }
    }
}

impl Deref for Line {
    type Target = Vec<LineBlock>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

#[derive(Debug)]
pub struct Lines(Vec<Line>);

impl Deref for Lines {
    type Target = Vec<Line>;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl FromStr for Lines {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        fn parse_parts(parts: Pairs<'_, Rule>, style: Option<PartStyle>) -> Vec<LineBlock> {
            let mut blocks = vec![];
            let mut block_parts = Vec::new();
            parts.for_each(|part| match part.as_rule() {
                Rule::text => block_parts.push(Parts::Text(part.as_str().to_string())),
                Rule::var_pending => block_parts.push(Parts::Pending),
                Rule::var_done => block_parts.push(Parts::Done),
                Rule::var_subject => block_parts.push(Parts::Subject),
                Rule::var_priority => block_parts.push(Parts::Priority),
                Rule::var_create_date => block_parts.push(Parts::CreateDate),
                Rule::var_finish_date => block_parts.push(Parts::FinishDate),
                Rule::var_finished => block_parts.push(Parts::Finished),
                Rule::var_treshold_date => block_parts.push(Parts::TresholdDate),
                Rule::var_due_date => block_parts.push(Parts::DueDate),
                Rule::var_contexts => block_parts.push(Parts::Contexts),
                Rule::var_projects => block_parts.push(Parts::Projects),
                Rule::var_hashtags => block_parts.push(Parts::Hashtags),
                Rule::var_any => block_parts.push(Parts::Special(part.as_str().to_string())),
                Rule::block => {
                    let mut inner = part.into_inner();
                    if !block_parts.is_empty() {
                        blocks.push(LineBlock::new(
                            std::mem::take(&mut block_parts),
                            style.as_ref(),
                        ));
                    }
                    blocks.append(&mut parse_parts(
                        inner.next().unwrap().into_inner(),
                        inner
                            .next()
                            .map(|pairs| PartStyle::try_from(pairs.into_inner()).unwrap()),
                    ));
                }
                _ => unreachable!(),
            });
            blocks.push(LineBlock::new(block_parts, style.as_ref()));
            blocks
        }
        let lines = ParserGrammar::parse(Rule::lines, s)
            .context("Failed to parse parser")?
            .next()
            .unwrap() // Safe. Already parsed by pest.
            .into_inner()
            .filter_map(|line| {
                if line.as_rule() == Rule::line {
                    Some(line.into_inner())
                } else {
                    None
                }
            })
            .map(|parts| Line(parse_parts(parts, None)))
            .collect();
        Ok(Lines(lines))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::{Color, TextStyle};
    use crate::todo::ToDoData;
    use anyhow::Result;
    use std::str::FromStr;

    #[test]
    fn line_fill() -> Result<()> {
        let styles = Styles::default();
        let mut todo = ToDo::default();
        todo.new_task("Some task 1").unwrap();
        todo.new_task("Some task 2").unwrap();
        todo.new_task("Some task 3").unwrap();
        todo.set_active(ToDoData::Pending, 0);

        let task = Task::from_str("Some task 1").unwrap();

        assert_eq!(
            Lines::from_str("some text[not empty $done]")?[0].fill(&task, &todo, &styles),
            Some(vec![
                (String::from("some text"), Style::default()),
                (String::from("not empty 0"), Style::default())
            ])
        );

        assert_eq!(
            Lines::from_str("some text[empty $priority]")?[0].fill(&task, &todo, &styles),
            Some(vec![(String::from("some text"), Style::default())])
        );

        Ok(())
    }

    #[test]
    fn parse_variables() -> Result<()> {
        assert_eq!(Lines::from_str("")?[0][0].parts, vec![]);

        assert_eq!(
            Lines::from_str("some text")?[0][0].parts,
            vec![Parts::Text("some text".into())]
        );

        assert_eq!(
            Lines::from_str("some text $done another text")?[0][0].parts,
            vec![
                Parts::Text("some text ".into()),
                Parts::Done,
                Parts::Text(" another text".into())
            ]
        );

        assert_eq!(
            Lines::from_str("there is ${pending}x pending tasks")?[0][0].parts,
            vec![
                Parts::Text("there is ".into()),
                Parts::Pending,
                Parts::Text("x pending tasks".into())
            ]
        );

        assert_eq!(
            Lines::from_str("special task text $some-special")?[0][0].parts,
            vec![
                Parts::Text("special task text ".into()),
                Parts::Special("some-special".into())
            ]
        );

        assert_eq!(
            Lines::from_str("special task text ${some-special}")?[0][0].parts,
            vec![
                Parts::Text("special task text ".into()),
                Parts::Special("some-special".into())
            ]
        );

        assert_eq!(
            Lines::from_str("Pending: $pending Done: $done")?[0][0].parts,
            vec![
                Parts::Text("Pending: ".into()),
                Parts::Pending,
                Parts::Text(" Done: ".into()),
                Parts::Done
            ]
        );

        Ok(())
    }

    #[test]
    fn parse_variables_error() {
        assert!(Lines::from_str("string with $ empty variable").is_err());
        assert!(Lines::from_str("string with empty variable on end $").is_err());
        assert!(Lines::from_str("variable block not closed ${variable ").is_err());
    }

    #[test]
    fn parse() -> Result<()> {
        let task = Task::default();

        assert!(Lines::from_str("")?[0][0].parts.is_empty());
        assert_eq!(
            Lines::from_str("some text")?[0][0].parts,
            vec![Parts::Text("some text".to_string())]
        );

        let lines = Lines::from_str("[some text](Red)")?;
        assert_eq!(
            lines[0][0].parts,
            vec![Parts::Text("some text".to_string())]
        );
        assert_eq!(
            lines[0][0].style.get_style(&task, &Styles::default()),
            TextStyle::default().fg(Color::red())
        );

        let lines = Lines::from_str("[some text] and another text")?;
        assert_eq!(
            lines[0][0].parts,
            vec![Parts::Text("some text".to_string())]
        );
        assert_eq!(
            lines[0][1].parts,
            vec![Parts::Text(" and another text".to_string())]
        );

        let lines = Lines::from_str("[some text]")?;
        assert_eq!(
            lines[0][0].parts,
            vec![Parts::Text("some text".to_string())]
        );

        let lines = Lines::from_str("[some text](red) text between [another text](blue, bold)")?;
        assert_eq!(
            lines[0][0].parts,
            vec![Parts::Text("some text".to_string())]
        );
        assert_eq!(
            lines[0][0].style.get_style(&task, &Styles::default()),
            TextStyle::default().fg(Color::red())
        );
        assert_eq!(
            lines[0][1].parts,
            vec![Parts::Text(" text between ".to_string())]
        );
        assert_eq!(
            lines[0][1].style.get_style(&task, &Styles::default()),
            TextStyle::default()
        );
        assert_eq!(
            lines[0][2].parts,
            vec![Parts::Text("another text".to_string())]
        );
        assert_eq!(
            lines[0][2].style.get_style(&task, &Styles::default()),
            TextStyle::default()
                .fg(Color::blue())
                .modifier(crate::config::TextModifier::Bold)
        );

        let lines = Lines::from_str("[some text](priority:A)")?;
        assert_eq!(
            lines[0][0].parts,
            vec![Parts::Text("some text".to_string())]
        );
        assert_eq!(
            lines[0][0].style.get_style(&task, &Styles::default()),
            TextStyle::default().fg(Color::red())
        );

        let lines = Lines::from_str("some text\nnew line")?;
        assert_eq!(lines.len(), 2);
        assert_eq!(
            lines[0][0].parts,
            vec![Parts::Text("some text".to_string())]
        );
        assert_eq!(lines[1][0].parts, vec![Parts::Text("new line".to_string())]);

        Ok(())
    }
}