toddi 0.3.2

A TODO focuser built on top of todo.txt
Documentation
use std::{fs, path::PathBuf};

use anyhow::{anyhow, Ok, Result};
use clap::{Parser, Subcommand};
use toddi::{
    ingress::TaskSrc,
    model::{CompletedTaskList, Project, TaskList},
};

mod config;
mod tui;

/// Display the next task to do for a given project.
#[derive(Parser)]
#[command(version, about)]
struct Cli {
    /// Sets a custom todo.txt file location.
    #[arg(short, long)]
    todo_txts: Option<Vec<PathBuf>>,

    /// Sets a custom config.toml file location.
    #[arg(short, long)]
    config_file: Option<PathBuf>,

    #[command(subcommand)]
    command: Option<Commands>,
}

#[derive(Subcommand)]
enum Commands {
    /// Focus loop with task completion, can target a specific project
    Focus { project: Option<String> },
    /// Display next task of project passed as argument
    Projects { projects: Vec<String> },
}

fn main() -> Result<()> {
    let args = Cli::parse();

    let config = load_and_set_configuration(&args)?;

    // TODO: This should be a named struct
    let (mut todos, mut dones) = generate_data_structure(&config)?;

    match &args.command {
        Some(Commands::Projects { projects }) => {
            if projects.is_empty() {
                tui::display_no_projects();
            }
            for project in projects {
                if let Some(project) =
                    Project::search_project(project.to_string(), todos.clone(), dones.clone())
                {
                    tui::display_project_status(&project);
                } else {
                    tui::display_project_not_found(project);
                }
            }
        }
        Some(Commands::Focus { project }) => loop {
            match project {
                Some(project) => {
                    if let Some(project) =
                        Project::search_project(project.to_string(), todos.clone(), dones.clone())
                    {
                        tui::display_project_status(&project);
                        let buffer = tui::query_user()?;
                        match buffer.trim() {
                            "y" => {
                                println!("Well done! Next task...");
                                // TODO: this should be handled by a named struct
                                project.complete_current_task()?;
                                (todos, dones) = generate_data_structure(&config)?;
                            }
                            "n" => println!("Then why did you bother answering?"),
                            "q" => {
                                println!("Alright, we are done for now.");
                                break;
                            }
                            _ => println!("Wrong input, try again."),
                        }
                    } else {
                        tui::display_project_not_found(project);
                    }
                }
                None => {
                    show_task(&todos, &dones)?;
                    let buffer = tui::query_user()?;
                    match buffer.trim() {
                        "y" => {
                            println!("Well done! Next task...");
                            // TODO: this should be handled by a named struct
                            todos.complete_current_task()?;
                            (todos, dones) = generate_data_structure(&config)?;
                        }
                        "n" => println!("Then why did you bother answering?"),
                        "q" => {
                            println!("Alright, we are done for now.");
                            break;
                        }
                        _ => println!("Wrong input, try again."),
                    }
                }
            }
        },
        None => show_task(&todos, &dones)?,
    }

    Ok(())
}

fn show_task(
    todos: &TaskList,
    dones: &CompletedTaskList,
) -> std::result::Result<(), anyhow::Error> {
    if let Some(task) = todos.clone().get_current_task() {
        if task.project.is_empty() {
            tui::display_task(task);
        } else if let Some(project) =
            Project::search_project(task.project.clone(), todos.clone(), dones.clone())
        {
            tui::display_project_status(&project);
        } else {
            return Err(anyhow!("It should not be possible to match a project in a task and then not be able to retrieve it!\\ntask: {:?}", task));
        }
    } else {
        tui::display_empty_todo_list();
    }
    Ok(())
}

fn generate_data_structure(config: &config::Config) -> Result<(TaskList, CompletedTaskList)> {
    // Generate task_list _before_ done_list: order matters here, cli test will catch mistake
    let task_list = TaskList::new(TaskSrc::from_file(config.todo_files.clone())?)?;

    let done_files = extrapolate_done_files_path(config.todo_files.clone());
    for file in done_files.clone() {
        if !fs::exists(&file)? {
            if let Some(file_name) = file.to_str() {
                eprintln!("could not find `{}`, creating it.", file_name);
                fs::write(&file, "")?;
            } else {
                return Err(anyhow!(
                    "Failed to access string representation of done.txt path."
                ));
            }
        }
    }

    Ok((
        task_list,
        CompletedTaskList::new(TaskSrc::from_file(done_files.clone())?)?,
    ))
}

fn extrapolate_done_files_path(todo_files: Vec<PathBuf>) -> Vec<std::path::PathBuf> {
    let mut done_files: Vec<PathBuf> = vec![];
    for file in todo_files {
        if let Some(parent) = file.parent() {
            let mut path_buf = parent.to_path_buf();
            path_buf.push(toddi::DONE_FILE);
            done_files.push(path_buf);
        }
    }
    done_files
}
fn load_and_set_configuration(args: &Cli) -> Result<config::Config> {
    let mut config_file_path: PathBuf;
    if let Some(file) = args.config_file.as_deref() {
        config_file_path = file.to_path_buf();
    } else {
        config_file_path = get_configdir()?;
        config_file_path.push("toddi");
        config_file_path.push("config.toml");
    }
    let mut config = config::Config::load(config_file_path)?;
    apply_cli_overrides(args, &mut config);
    Ok(config)
}

fn apply_cli_overrides(args: &Cli, config: &mut config::Config) {
    if let Some(input) = args.todo_txts.as_deref() {
        config.todo_files = input.to_vec();
    }
}

fn get_configdir() -> Result<PathBuf> {
    if let Some(dir) = dirs::config_dir() {
        Ok(dir)
    } else {
        Err(anyhow!(
            "Issue with `dirs` crate: could not fetch configuration directory."
        ))
    }
}