tally-todo 0.7.0

Make TODO management a little more automatic
use anyhow::Result;
use std::path::{Path, PathBuf};

use crate::services::storage::project_registry_storage::ProjectRegistryStorage;
use crate::services::storage::task_storage::ListStorage;

pub fn cmd_projects_list() -> Result<()> {
    let mut registry = ProjectRegistryStorage::new()?;
    registry.prune_missing()?;
    let projects = registry.projects();

    if projects.is_empty() {
        println!("No tally projects registered. Run `tally init` in a project first.");
        return Ok(());
    }

    println!("Registered projects:");
    for (idx, project) in projects.iter().enumerate() {
        println!("{}. {}", idx + 1, project.display());
    }

    Ok(())
}

pub fn cmd_projects_status() -> Result<()> {
    let mut registry = ProjectRegistryStorage::new()?;
    registry.prune_missing()?;
    let projects = registry.projects();

    if projects.is_empty() {
        println!("No tally projects registered. Run `tally init` in a project first.");
        return Ok(());
    }

    let mut total_tasks = 0usize;
    let mut total_open = 0usize;
    let mut total_done = 0usize;
    let mut loaded = 0usize;

    for project_root in projects {
        match project_status(&project_root) {
            Ok(summary) => {
                loaded += 1;
                total_tasks += summary.total_tasks;
                total_open += summary.open_tasks;
                total_done += summary.done_tasks;
                println!(
                    "- {} ({} v{}): {} total, {} open, {} done",
                    summary.path.display(),
                    summary.project_name,
                    summary.version,
                    summary.total_tasks,
                    summary.open_tasks,
                    summary.done_tasks
                );
            }
            Err(err) => {
                println!("- {}: skipped ({})", project_root.display(), err);
            }
        }
    }

    println!();
    if total_tasks > 0 {
        let completion = ((total_done as f64 / total_tasks as f64) * 100.0).round();
        println!(
            "Across {} project(s): {} total, {} open, {} done ({}% complete)",
            loaded, total_tasks, total_open, total_done, completion
        );
    } else {
        println!("Across {} project(s): no tasks found", loaded);
    }

    Ok(())
}

pub fn cmd_projects_prune() -> Result<()> {
    let mut registry = ProjectRegistryStorage::new()?;
    let removed = registry.prune_missing()?;
    println!("Pruned {} missing project(s) from registry.", removed);
    Ok(())
}

#[derive(Debug)]
struct ProjectSummary {
    path: PathBuf,
    project_name: String,
    version: String,
    total_tasks: usize,
    open_tasks: usize,
    done_tasks: usize,
}

fn project_status(project_root: &Path) -> Result<ProjectSummary> {
    let todo_file = project_root.join("TODO.md");
    let storage = ListStorage::new(&todo_file)?;
    let list = storage.list();

    let total_tasks = list.tasks.len();
    let done_tasks = list.tasks.iter().filter(|task| task.completed).count();
    let open_tasks = total_tasks.saturating_sub(done_tasks);

    Ok(ProjectSummary {
        path: project_root.to_path_buf(),
        project_name: list.project_name.clone(),
        version: list.project_version.to_string(),
        total_tasks,
        open_tasks,
        done_tasks,
    })
}