toddi 0.2.0

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

use anyhow::{anyhow, Context, Result};
use clap::{Parser, Subcommand};
use toddi::{config::Config, get_project, get_top_task};

// TODO: move to lib.rs
mod tui;

// TODO: Soon I will have to replicate todo.txt's `do` action to fill done.txt myself.
//          - it would be smart to keep the raw task string saved somewhere
// TODO: first run setup
//          - if todo.txt program / files absent, use toddi default
// TODO: interactive mode
//          - tag as done
//          - all actions in memory until final commit
// TODO: recurring tasks
//          - daily
//          - weekly
//          - monthly
// TODO: list following task (if any)
// TODO: todo.txt api mode
//          - display task number
// TODO: timer on project to keep track of time spent
// TODO: log instead of printing to stderr
// TODO: generate visualisation from report.txt

/// 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_txt: Option<PathBuf>,

    /// Sets a custom done.txt file location.
    #[arg(short, long)]
    done_txt: Option<PathBuf>,

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

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

#[derive(Subcommand)]
enum Commands {
    /// Focus on specific projects
    Focus { projects: Vec<String> },
}

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

    // Load and set configuration
    let config = Config::load(args.config_file);
    let mut todo_txt_path: PathBuf;
    let mut done_txt_path: PathBuf;
    match config {
        Ok(config) => {
            todo_txt_path = config.todo_file;
            done_txt_path = config.done_file;
        }
        Err(err) => {
            return Err(anyhow!("Could not load configuration!\nconfig: {:?}", err));
        }
    }
    if let Some(input) = args.todo_txt.as_deref() {
        todo_txt_path = input.to_path_buf();
    }
    if let Some(input) = args.done_txt.as_deref() {
        done_txt_path = input.to_path_buf();
    }

    // Load data
    let todos = get_file_content(&todo_txt_path)?;
    let dones = get_file_content(&done_txt_path)?;

    match &args.command {
        Some(Commands::Focus { projects }) => {
            for project in projects {
                if let Some(project) = get_project(project.clone(), &todos, &dones)? {
                    tui::display_project_status(project);
                } else {
                    tui::display_no_project_found(project);
                }
            }
        }
        None => {
            if let Some(task) = get_top_task(&todos)? {
                if task.project.is_empty() {
                    tui::display_task(task);
                } else if let Some(project) = get_project(task.project.clone(), &todos, &dones)? {
                    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 get_file_content(file_path: &std::path::Path) -> Result<String> {
    std::fs::read_to_string(file_path)
        .with_context(|| format!("could not read file `{}`", file_path.display()))
}