todoscope 0.1.0

A simple CLI todo list manager
Documentation
use std::io;

use chrono::{DateTime, NaiveDate, Utc};
use clap::{CommandFactory, Parser};
use clap_complete::{
    generate,
    shells::{Bash, Fish, PowerShell, Zsh},
};
mod cli;
mod input;
mod menu;
mod notify;
mod priority;
mod recurrence;
mod sortby;
mod status;
mod storage;
mod todo;
mod todo_cli;
mod utils;
use colored::*;
use menu::display_menu;
use uuid::Uuid;

use crate::{
    cli::{Cli, Commands, Shell},
    input::UpdateTodoInput,
};
use crate::{
    input::AddTodoInput,
    todo_cli::{add_todo_cli, delete_todo_cli, list_todos_cli, search_todo_cli, update_todo_cli},
};

fn main() {
    let file_path = "todos.json";
    let cli = Cli::parse();

    if cli.menu {
        display_menu(file_path);
        return;
    }

    match cli.command {
        Some(Commands::Add {
            title,
            description,
            priority,
            status,
            due_date,
            recurrence,
            tags,
            parent_id,
            subtasks,
        }) => {
            let due_date = due_date.map(|d| {
                let nd = NaiveDate::parse_from_str(&d, "%Y-%m-%d").unwrap_or_else(|_| {
                    panic!(
                        "{}",
                        format!("❌ Invalid date format: {d} (expected YYY-MM-DD)").red()
                    )
                });
                let ndt = nd
                    .and_hms_opt(0, 0, 0)
                    .unwrap_or_else(|| panic!("{}", "❌  Invalid time components".red()));
                DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc)
            });

            let parent_id =
                parent_id.map(|pid| Uuid::parse_str(&pid).expect("❌ Invalid parent UUID"));

            let subtasks = subtasks.map(|subs| {
                subs.into_iter()
                    .map(|s| Uuid::parse_str(&s).expect("❌ Invalid subtask UUID"))
                    .collect()
            });

            add_todo_cli(
                file_path,
                AddTodoInput {
                    title,
                    description,
                    priority,
                    status,
                    due_date,
                    tags,
                    recurrence,
                    parent_id,
                    subtasks,
                },
            );
        }
        Some(Commands::List { sort_by }) => {
            list_todos_cli(file_path, &sort_by);
        }
        Some(Commands::Search {
            id,
            title,
            priority,
            status,
            due_date,
            recurrence,
            tags,
            parent_task_id,
        }) => {
            let due_date = due_date.map(|d| {
                let nd = NaiveDate::parse_from_str(&d, "%Y-%m-%d").unwrap_or_else(|_| {
                    panic!(
                        "{}",
                        format!("❌ Invalid date format: {d} (expected YYY-MM-DD)").red()
                    )
                });
                let ndt = nd
                    .and_hms_opt(0, 0, 0)
                    .unwrap_or_else(|| panic!("{}", "❌  Invalid time components".red()));
                DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc)
            });

            let parent_id =
                parent_task_id.map(|pid| Uuid::parse_str(&pid).expect("❌ Invalid parent UUID"));

            search_todo_cli(
                file_path,
                input::SearchTodoInput {
                    id,
                    title,
                    priority,
                    status,
                    due_date,
                    recurrence,
                    tags,
                    parent_id,
                },
            );
        }
        Some(Commands::Update {
            id,
            title,
            description,
            priority,
            status,
            due_date,
            recurrence,
            tags,
            parent_task_id,
            subtasks,
        }) => {
            let id = match Uuid::parse_str(&id) {
                Ok(u) => u,
                Err(_) => {
                    eprintln!("{}", "❌ Invalid UUID".red());
                    std::process::exit(1);
                }
            };

            let due_date = due_date.map(|d| {
                let nd = NaiveDate::parse_from_str(&d, "%Y-%m-%d").unwrap_or_else(|_| {
                    panic!(
                        "{}",
                        format!("❌ Invalid date format: {d} (expected YYY-MM-DD)").red()
                    )
                });
                let ndt = nd
                    .and_hms_opt(0, 0, 0)
                    .unwrap_or_else(|| panic!("{}", "❌  Invalid time components".red()));
                DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc)
            });

            let parent_id =
                parent_task_id.map(|pid| Uuid::parse_str(&pid).expect("❌ Invalid parent UUID"));

            let parsed_subtasks = subtasks.map(|subs| {
                subs.into_iter()
                    .filter_map(|s| {
                        Uuid::parse_str(&s)
                            .inspect_err(|_| {
                                eprintln!(
                                    "{}",
                                    format!("Invalid UUID format in subtasks: {s}")
                                        .yellow()
                                        .bold()
                                );
                            })
                            .ok()
                    })
                    .collect::<Vec<Uuid>>()
            });

            if update_todo_cli(
                file_path,
                UpdateTodoInput {
                    id,
                    new_title: title,
                    new_description: description,
                    new_priority: priority,
                    new_status: status,
                    new_due_date: due_date,
                    new_recurrence: recurrence,
                    new_tags: tags,
                    new_parent_id: parent_id,
                    new_subtasks: parsed_subtasks,
                },
            ) {
                println!("{}", "✅ Task updated successfully!".green().bold());
            } else {
                println!(
                    "{}",
                    format!("⚠️ No task found with id {id}").yellow().bold()
                );
            }
        }
        Some(Commands::Delete { id }) => {
            let id = Uuid::parse_str(&id).expect("❌ Invalid UUID");
            if delete_todo_cli(file_path, id) {
                println!("{}", "🗑️ Task deleted".red().bold());
            } else {
                println!(
                    "{}",
                    format!("⚠️ No task found with id {id}").yellow().bold()
                );
            }
        }
        Some(Commands::Completions { shell }) => {
            let mut cmd = Cli::command();
            match shell {
                Shell::Bash => generate(Bash, &mut cmd, "todo", &mut io::stdout()),
                Shell::Zsh => generate(Zsh, &mut cmd, "todo", &mut io::stdout()),
                Shell::Fish => generate(Fish, &mut cmd, "todo", &mut io::stdout()),
                Shell::Powershell => generate(PowerShell, &mut cmd, "todo", &mut io::stdout()),
            }
        }
        Some(Commands::Notify) => {
            notify::send_due_notifications(file_path);
            println!(
                "{}",
                "🔔 Notifications dispatched for due tasks".green().bold()
            );
        }
        None => {
            println!(
                "{}",
                "⚠️ No command provided. Use --help for usage."
                    .yellow()
                    .bold()
            );
        }
    }
}