taskfinder 2.15.0

A terminal user interface that extracts and displays tasks from plain text files, hooking into your default terminal-based editor for editing.
#![forbid(unsafe_code)]
//! Defining and working with tasks and task-related data.

use std::cmp::Reverse;
use std::fmt::{self, Display, Formatter};
use std::path::PathBuf;

use chrono::NaiveDate;

use crate::{
    App, TfError, files::FileStatus, modes::tasks_mode::RecurringStatus, priority::Priority,
};

/// Status of a task.
#[derive(Clone, PartialEq)]
pub enum CompletionStatus {
    Incomplete,
    Completed,
}

impl Display for CompletionStatus {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            CompletionStatus::Incomplete => write!(f, "incomplete"),
            CompletionStatus::Completed => write!(f, "completed"),
        }
    }
}

/// A group of tasks.
#[derive(Debug, Clone)]
pub struct TaskSet {
    pub name: String,
    pub priority: Option<Priority>,
    pub due_date: Option<NaiveDate>,
    pub completed_date: Option<NaiveDate>,
    pub tasks: Vec<Task>,
    pub tags: Vec<String>,
}

impl TaskSet {
    pub fn new(
        name: String,
        priority: Option<Priority>,
        due_date: Option<NaiveDate>,
        completed_date: Option<NaiveDate>,
        tags: Vec<String>,
    ) -> Self {
        Self {
            name,
            priority,
            due_date,
            completed_date,
            tasks: vec![],
            tags,
        }
    }
}

/// An individual task.
#[derive(Debug, Clone)]
pub struct Task {
    pub text: String,
    pub completed: bool,
    pub recurring: bool,
    pub due_date: Option<NaiveDate>,
    pub completed_date: Option<NaiveDate>,
    pub priority: Option<Priority>,
    pub tags: Vec<String>,
    pub line: usize,
    pub order: Option<usize>,
}

/// A task that includes the task set and file it belongs to.
#[derive(Debug, Clone)]
pub struct RichTask {
    pub task: Task,
    pub task_set: String,
    pub file_name: String,
    pub file_path: PathBuf,
    pub file_status: FileStatus,
}

impl RichTask {
    /// Collect all rich tasks.
    pub fn collect(app: &App) -> Result<Vec<Self>, TfError> {
        // Start from files that have already been collected - that's an expensive operation
        // and no need to re-do it here.
        let mut files = app.filemode.files.clone();

        // Filter files by status.
        match app.taskmode.file_status {
            FileStatus::Active => {
                files.retain(|f| f.status == FileStatus::Active);
            }
            FileStatus::Archived => {
                files.retain(|f| f.status == FileStatus::Archived);
            }
            FileStatus::Stale => {
                files.retain(|f| f.status == FileStatus::Stale);
            }
        }

        // Collect tasks as RichTasks.
        let mut tasks = vec![];
        for file in files {
            for task_set in file.task_sets {
                for task in task_set.tasks {
                    tasks.push(RichTask {
                        task,
                        task_set: task_set.name.clone(),
                        file_name: file.head[0].clone(),
                        file_path: file.file.clone(),
                        file_status: file.status.clone(),
                    })
                }
            }
        }

        // Filters, in order of least to most expensive operations.
        // Filter by completion status.
        tasks.retain(|task| match app.taskmode.completion_status {
            CompletionStatus::Incomplete => !task.task.completed,
            CompletionStatus::Completed => task.task.completed,
        });

        // Filter by tag.
        if !app.taskmode.tag_dialog.submitted_input.is_empty() {
            tasks.retain(|task| {
                let input = &app.taskmode.tag_dialog.submitted_input;

                // Exclude tasks.
                if input.starts_with('!') {
                    let input = input.strip_prefix('!').unwrap().to_string();
                    !task.task.tags.contains(&input)
                // Include tasks.
                } else {
                    task.task.tags.contains(input)
                }
            })
        }

        // Filter by search term.
        if !app.taskmode.search_dialog.submitted_input.is_empty() {
            tasks.retain(|task| {
                task.task
                    .text
                    .to_lowercase()
                    .contains(&app.taskmode.search_dialog.submitted_input)
            })
        }

        // Filter by recurring status.
        if app.taskmode.recurring_status != RecurringStatus::All {
            tasks.retain(|task| match app.taskmode.recurring_status {
                RecurringStatus::All => true,
                RecurringStatus::Recurring => task.task.recurring,
                RecurringStatus::NonRecurring => !task.task.recurring,
            });
        }

        // Sort, first by priority.
        tasks.sort_by_key(|task| Reverse(task.task.priority));

        // Sort, next by order.
        tasks.sort_by_key(|task| task.task.order.unwrap_or(9999));

        // Sort, next by due or completed due.
        match app.taskmode.completion_status {
            CompletionStatus::Incomplete => {
                // Split vec in two in order to do this - otherwise it will put those without
                // due dates first.
                let mut with_due_dates = tasks.clone();
                with_due_dates.retain(|task| task.task.due_date.is_some());
                with_due_dates.sort_by_key(|task| task.task.due_date);

                let mut without_due_dates = tasks;
                without_due_dates.retain(|task| task.task.due_date.is_none());

                with_due_dates.append(&mut without_due_dates);
                tasks = with_due_dates;
            }
            CompletionStatus::Completed => {
                tasks.sort_by_key(|task| Reverse(task.task.completed_date));
            }
        }

        Ok(tasks)
    }
}