chore 0.1.0

plain-text command-line task management utility
Documentation
use crate::date::Date;
use crate::error::*;
use crate::field::*;
use crate::task::TaskBuf;
use crate::token::Token;

pub enum Modification<'a> {
    SetPending,
    SetCompleted,
    AddProject(Project<'a>),
    RemoveProject(Project<'a>),
    AddContext(Context<'a>),
    RemoveContext(Context<'a>),
    SetPair(ModPair<'a>),
    Append(String),
    SetBody(String),
}

pub enum ModPair<'a> {
    SetEnd(String),
    SetPriority(Priority),
    SetEntry(String),
    SetValue(Key<'a>, String),
    RemoveEnd,
    RemovePriority,
    RemoveEntry,
    RemoveKey(Key<'a>),
}

pub struct ModOutput {
    pub add: Option<TaskBuf>,
    pub remove_similar: bool,
}

impl<'a> Modification<'a> {
    const DONE: &'static str = "+done";

    pub fn new(str: &'a str, now: &Date, date_keys: &[Key]) -> Result<Self> {
        let full = str;
        let (str, negate) = match str.strip_prefix('-').or_else(|| str.strip_prefix('!')) {
            Some(str) => (str, true),
            None => (str, false),
        };

        Ok(if str == Modification::DONE {
            match negate {
                false => Modification::SetCompleted,
                true => Modification::SetPending,
            }
        } else if let Some(proj) = Project::new(str) {
            match negate {
                true => Modification::RemoveProject(proj),
                false => Modification::AddProject(proj),
            }
        } else if let Some(ctx) = Context::new(str) {
            match negate {
                true => Modification::RemoveContext(ctx),
                false => Modification::AddContext(ctx),
            }
        } else if let Some(pair) = ModPair::new(str, negate, now, date_keys)? {
            Modification::SetPair(pair)
        } else if let Some(text) = full.strip_prefix(">>") {
            Modification::Append(text.to_owned())
        } else {
            Modification::SetBody(full.to_owned())
        })
    }

    pub fn apply(&self, task: &mut TaskBuf, date_keys: &[Key]) -> ModOutput {
        let mut add = None;
        let mut remove_similar = false;
        match self {
            Modification::SetPending => {
                task.set_end(None);
                task.set_pending();
            }
            Modification::SetCompleted => {
                if !task.as_task().is_completed() {
                    if let Some(off) = task.as_task().get_value(&Key::new("recur")) {
                        let mut new = task.clone();
                        let task = task.as_task();
                        if let Some(entry) =
                            task.get_entry().and_then(|e| Date::from_abs(e.as_str()))
                        {
                            if let Some(new_date) = Date::from_rel(off.as_str(), &entry) {
                                new.set_entry(Some(Entry(&new_date.to_string())))
                            }
                        }
                        for key in date_keys {
                            let value = match task
                                .get_value(key)
                                .and_then(|v| Date::from_abs(v.as_str()))
                            {
                                Some(value) => value,
                                None => continue,
                            };
                            if let Some(new_date) = Date::from_rel(off.as_str(), &value) {
                                new.set_value(key, &Value::new(&new_date.to_string()))
                            }
                        }
                        add = Some(new);
                    }
                    task.set_completed();
                    if task
                        .as_task()
                        .has_token(&Token::Project(Project::new("+update").unwrap()))
                    {
                        remove_similar = true;
                    }
                }
            }
            Modification::AddProject(proj) => {
                if !task.as_task().has_token(&Token::Project(proj.clone())) {
                    task.append_text(proj.as_str())
                }
            }
            Modification::RemoveProject(proj) => task.remove_token(&Token::Project(proj.clone())),
            Modification::AddContext(ctx) => {
                if !task.as_task().has_token(&Token::Context(ctx.clone())) {
                    task.append_text(ctx.as_str())
                }
            }
            Modification::RemoveContext(ctx) => task.remove_token(&Token::Context(ctx.clone())),
            Modification::SetPair(pair) => match pair {
                ModPair::SetEnd(v) => task.set_end(Some(End(v))),
                ModPair::SetPriority(v) => task.set_priority(Some(*v)),
                ModPair::SetEntry(v) => task.set_entry(Some(Entry(v))),
                ModPair::SetValue(k, v) => task.set_value(k, &Value::new(v)),
                ModPair::RemoveEnd => task.set_end(None),
                ModPair::RemovePriority => task.set_priority(None),
                ModPair::RemoveEntry => task.set_entry(None),
                ModPair::RemoveKey(k) => task.remove_pair(k),
            },
            Modification::Append(v) => task.append_text(v),
            Modification::SetBody(v) => {
                task.clear_body();
                task.append_text(v);
            }
        }

        ModOutput {
            add,
            remove_similar,
        }
    }
}

impl<'a> ModPair<'a> {
    const END: &'static str = "end";
    const PRI: &'static str = "pri";
    const ENTRY: &'static str = "entry";

    pub fn new(str: &'a str, negate: bool, now: &Date, date_keys: &[Key]) -> Result<Option<Self>> {
        let (key, value) = match str.split_once(':') {
            None => return Ok(None),
            Some((key, value)) => (key, value),
        };

        let is_date_key = date_keys.iter().any(|k| k == &Key::new(key));
        let empty_value = value.is_empty();

        Ok(Some(match (key, negate, empty_value) {
            (ModPair::END, false, false) => {
                match Date::new(value, now).map(|date| date.to_string()) {
                    Some(date) => match End::new(&date, Stage::End) {
                        Some(_) => ModPair::SetEnd(date),
                        None => return Err(InvalidEnd(str.to_owned())),
                    },
                    None => return Err(KeyExpectsDateValue(str.to_string())),
                }
            }
            (ModPair::PRI, false, false) => {
                match Some(value)
                    .filter(|v| v.len() == 1)
                    .and_then(|v| v.chars().next())
                    .and_then(Priority::from_char)
                {
                    Some(pri) => ModPair::SetPriority(pri),
                    None => return Err(InvalidPriority(str.to_owned())),
                }
            }
            (ModPair::ENTRY, false, false) => {
                match Date::new(value, now).map(|date| date.to_string()) {
                    Some(date) => match Entry::new(&date, Stage::Entry) {
                        Some(_) => ModPair::SetEntry(date),
                        None => return Err(InvalidEntry(str.to_owned())),
                    },
                    None => return Err(KeyExpectsDateValue(str.to_string())),
                }
            }
            (_, false, false) if is_date_key => match Date::new(value, now) {
                Some(date) => ModPair::SetValue(Key::new(key), date.to_string()),
                None => return Err(KeyExpectsDateValue(str.to_string())),
            },
            (_, false, false) => ModPair::SetValue(Key::new(key), value.to_owned()),
            (ModPair::END, false, true) => ModPair::RemoveEnd,
            (ModPair::PRI, false, true) => ModPair::RemovePriority,
            (ModPair::ENTRY, false, true) => ModPair::RemoveEntry,
            (_, false, true) if is_date_key => ModPair::RemoveKey(Key::new(key)),
            (_, false, true) => ModPair::SetValue(Key::new(key), value.to_owned()),
            (_, true, false) => return Err(CannotModNegateKeyValue(str.to_owned())),
            (ModPair::END, true, true) => ModPair::RemoveEnd,
            (ModPair::PRI, true, true) => ModPair::RemovePriority,
            (ModPair::ENTRY, true, true) => ModPair::RemoveEntry,
            (_, true, true) => ModPair::RemoveKey(Key::new(key)),
        }))
    }
}