tasks-cli-rs 0.6.1

Markdown-based TODO task management CLI: each task is a Markdown file with YAML front matter
use crate::cli::StepCommand;
use crate::error::{Error, Result};
use crate::storage;

use super::task::active_library_root;

pub fn run(command: StepCommand) -> Result<()> {
    match command {
        StepCommand::Get { id } => get(&id),
        StepCommand::Add { id, title } => add(&id, &title),
        StepCommand::Done { id, step } => done(&id, &step),
        StepCommand::Remove { id, step } => remove(&id, &step),
    }
}

fn get(id: &str) -> Result<()> {
    let root = active_library_root()?;
    let st = storage::resolve_task(&root, id)?;
    if st.task.steps.is_empty() {
        println!("#{} {} has no steps", st.task.meta.seq, st.task.meta.title);
        return Ok(());
    }
    println!("#{} {}", st.task.meta.seq, st.task.meta.title);
    for (i, step) in st.task.steps.iter().enumerate() {
        let mark = if step.done { "x" } else { " " };
        println!("  s{} [{mark}] {}", i + 1, step.title);
    }
    print_progress(&st.task);
    Ok(())
}

fn parse_step_index(step: &str, total: usize) -> Result<usize> {
    let n = step
        .strip_prefix('s')
        .and_then(|n| n.parse::<usize>().ok())
        .filter(|n| *n >= 1 && *n <= total)
        .ok_or_else(|| {
            Error::TaskNotFound(format!("step {step} (task has {total} steps: s1..s{total})"))
        })?;
    Ok(n - 1)
}

fn add(id: &str, title: &str) -> Result<()> {
    let root = active_library_root()?;
    let mut st = storage::resolve_task(&root, id)?;
    st.task.body = crate::checkbox::append(&st.task.body, title);
    st.task.steps = crate::checkbox::parse_checkboxes(&st.task.body);
    storage::write_task(&st.path, &st.task)?;
    println!("added step s{} to #{}", st.task.steps.len(), st.task.meta.seq);
    print_progress(&st.task);
    Ok(())
}

fn done(id: &str, step: &str) -> Result<()> {
    let root = active_library_root()?;
    let mut st = storage::resolve_task(&root, id)?;
    let idx = parse_step_index(step, st.task.steps.len())?;
    st.task.body = crate::checkbox::set_done(&st.task.body, idx).map_err(Error::InvalidTaskFile)?;
    st.task.steps = crate::checkbox::parse_checkboxes(&st.task.body);
    storage::write_task(&st.path, &st.task)?;
    println!("step {step} -> done");
    print_progress(&st.task);
    Ok(())
}

fn remove(id: &str, step: &str) -> Result<()> {
    let root = active_library_root()?;
    let mut st = storage::resolve_task(&root, id)?;
    let idx = parse_step_index(step, st.task.steps.len())?;
    st.task.body =
        crate::checkbox::remove_line(&st.task.body, idx).map_err(Error::InvalidTaskFile)?;
    st.task.steps = crate::checkbox::parse_checkboxes(&st.task.body);
    storage::write_task(&st.path, &st.task)?;
    println!("removed step {step} from #{}", st.task.meta.seq);
    Ok(())
}

fn print_progress(task: &crate::model::Task) {
    let done = task.steps.iter().filter(|s| s.done).count();
    println!("progress: {done}/{}", task.steps.len());
}