todotxt-tui 0.2.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, ToDo};
use crate::{
    config::{Styles, StylesValue},
    {Result, ToDoError},
};
use tui::style::Style;

#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct LineBlock {
    pub parts: Vec<Parts>,
    pub style: StylesValue,
}

impl LineBlock {
    fn parse_variables(block: &str) -> Result<Vec<Parts>> {
        let mut ret = Vec::new();
        let mut iter = block.chars();
        let mut read_variable = false;
        let mut variable_block = false;
        let mut read = String::new();
        while let Some(c) = iter.next() {
            match c {
                '$' => {
                    read_variable = true;
                    ret.push(Parts::Text(read));
                    read = String::new();
                    match iter.next() {
                        Some('{') => variable_block = true,
                        Some(ch) if ch.is_whitespace() => {
                            return Err(ToDoError::EmptyVariableName(block.to_string()))
                        }
                        Some(ch) => read.push(ch),
                        None => return Err(ToDoError::EmptyVariableName(block.to_string())),
                    }
                }
                '}' if read_variable && variable_block => {
                    variable_block = false;
                    read_variable = false;
                    ret.push(Parts::from(read));
                    read = String::new();
                }
                c if read_variable && !variable_block && c.is_whitespace() => {
                    read_variable = false;
                    ret.push(Parts::from(read));
                    read = String::from(c);
                }
                '\\' => read.push(match iter.next() {
                    Some(ch) => ch,
                    None => return Err(ToDoError::ParseBlockEscapeOnEnd(block.to_string())),
                }),
                _ => read.push(c),
            };
        }
        ret.push(if read_variable {
            if variable_block {
                return Err(ToDoError::ParseVariableNotClosed(read));
            }
            Parts::from(read)
        } else {
            Parts::Text(read)
        });

        Ok(ret)
    }

    pub fn fill(&self, todo: &ToDo, styles: &Styles) -> Option<(String, Style)> {
        let mut ret = String::new();
        for part in &self.parts {
            ret += &part.fill(todo)?;
        }
        Some((
            ret,
            match todo.get_active() {
                Some(task) => self.style.get_style(task, styles),
                None => Style::default(),
            },
        ))
    }

    pub fn try_from_styled(value: &str, style: Option<String>, styles: &Styles) -> Result<Self> {
        Ok(LineBlock {
            parts: Self::parse_variables(value)?,
            style: match style {
                Some(style) => styles.get_style(&style)?,
                None => StylesValue::default(),
            },
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parse_variables() -> Result<()> {
        let parts = LineBlock::parse_variables("")?;
        assert_eq!(parts[0], Parts::Text("".into()));

        let parts = LineBlock::parse_variables("some text")?;
        assert_eq!(parts[0], Parts::Text("some text".into()));

        let parts = LineBlock::parse_variables("some text $done another text")?;
        assert_eq!(parts[0], Parts::Text("some text ".into()));
        assert_eq!(parts[1], Parts::Done);
        assert_eq!(parts[2], Parts::Text(" another text".into()));

        let parts = LineBlock::parse_variables("there is ${pending}x pending tasks")?;
        assert_eq!(parts[0], Parts::Text("there is ".into()));
        assert_eq!(parts[1], Parts::Pending);
        assert_eq!(parts[2], Parts::Text("x pending tasks".into()));

        let parts = LineBlock::parse_variables("special task text $some-special")?;
        assert_eq!(parts[0], Parts::Text("special task text ".into()));
        assert_eq!(parts[1], Parts::Special("some-special".into()));

        let parts = LineBlock::parse_variables("special \\$ character")?;
        assert_eq!(parts[0], Parts::Text("special $ character".into()));

        let parts = LineBlock::parse_variables("Pending: $pending Done: $done")?;
        assert_eq!(parts[0], Parts::Text("Pending: ".into()));
        assert_eq!(parts[1], Parts::Pending);
        assert_eq!(parts[2], Parts::Text(" Done: ".into()));
        assert_eq!(parts[3], Parts::Done);

        Ok(())
    }

    #[test]
    fn parse_variables_error() {
        assert_eq!(
            LineBlock::parse_variables("string with $ empty variable")
                .unwrap_err()
                .to_string(),
            ToDoError::EmptyVariableName(String::from("string with $ empty variable")).to_string()
        );

        assert_eq!(
            LineBlock::parse_variables("string with empty variable on end $")
                .unwrap_err()
                .to_string(),
            ToDoError::EmptyVariableName(String::from("string with empty variable on end $"))
                .to_string()
        );

        assert_eq!(
            LineBlock::parse_variables("invalid escape \\")
                .unwrap_err()
                .to_string(),
            ToDoError::ParseBlockEscapeOnEnd(String::from("invalid escape \\")).to_string()
        );

        assert_eq!(
            LineBlock::parse_variables("variable block not closed ${variable ")
                .unwrap_err()
                .to_string(),
            ToDoError::ParseVariableNotClosed(String::from("variable ")).to_string()
        );
    }
}