use crate::cli::StepCommand;
use crate::error::{Error, Result};
use crate::model::{Status, Step, Task};
use crate::storage;
use super::task::{active_library_root, parse_due};
pub fn run(command: StepCommand) -> Result<()> {
match command {
StepCommand::Add { id, title, due, depends_on } => add(&id, title, due, depends_on),
StepCommand::Done { id, step } => set_status(&id, &step, Status::Done),
StepCommand::Status { id, step, status } => {
let status = status.parse::<Status>().map_err(Error::InvalidTaskFile)?;
set_status(&id, &step, status)
}
StepCommand::Remove { id, step } => remove(&id, &step),
}
}
pub fn next_step_id(task: &Task) -> String {
let max = task
.meta
.steps
.iter()
.filter_map(|s| s.id.strip_prefix('s').and_then(|n| n.parse::<u64>().ok()))
.max()
.unwrap_or(0);
format!("s{}", max + 1)
}
fn add(id: &str, title: String, due: Option<String>, depends_on: Option<String>) -> Result<()> {
let root = active_library_root()?;
let mut st = storage::resolve_task(&root, id)?;
let deps: Vec<String> = depends_on
.map(|d| d.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect())
.unwrap_or_default();
for dep in &deps {
if !st.task.meta.steps.iter().any(|s| &s.id == dep) {
return Err(Error::InvalidTaskFile(format!("unknown step id in --depends-on: {dep}")));
}
}
let step_id = next_step_id(&st.task);
st.task.meta.steps.push(Step {
id: step_id.clone(),
title,
status: Status::Todo,
due_date: due.map(|d| parse_due(&d).map(|dt| dt.date_naive())).transpose()?,
depends_on: deps,
});
storage::write_task(&st.path, &st.task)?;
println!("added step {step_id} to #{}", st.task.meta.seq);
print_progress(&st.task);
Ok(())
}
fn set_status(id: &str, step_id: &str, status: Status) -> Result<()> {
let root = active_library_root()?;
let mut st = storage::resolve_task(&root, id)?;
let step = st
.task
.meta
.steps
.iter_mut()
.find(|s| s.id == step_id)
.ok_or_else(|| Error::TaskNotFound(format!("step {step_id} in #{}", st.task.meta.seq)))?;
step.status = status;
storage::write_task(&st.path, &st.task)?;
println!("step {step_id} -> {status}");
print_progress(&st.task);
Ok(())
}
fn remove(id: &str, step_id: &str) -> Result<()> {
let root = active_library_root()?;
let mut st = storage::resolve_task(&root, id)?;
let before = st.task.meta.steps.len();
st.task.meta.steps.retain(|s| s.id != step_id);
if st.task.meta.steps.len() == before {
return Err(Error::TaskNotFound(format!("step {step_id} in #{}", st.task.meta.seq)));
}
let dangling: Vec<String> = st
.task
.meta
.steps
.iter()
.filter(|s| s.depends_on.iter().any(|d| d == step_id))
.map(|s| s.id.clone())
.collect();
for s in st.task.meta.steps.iter_mut() {
s.depends_on.retain(|d| d != step_id);
}
if !dangling.is_empty() {
println!("note: removed dependency on {step_id} from: {}", dangling.join(", "));
}
storage::write_task(&st.path, &st.task)?;
println!("removed step {step_id} from #{}", st.task.meta.seq);
Ok(())
}
fn print_progress(task: &Task) {
let done = task.meta.steps.iter().filter(|s| s.status == Status::Done).count();
println!("progress: {done}/{}", task.meta.steps.len());
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn step_ids_increment() {
let mut task = Task::new(1, "t");
assert_eq!(next_step_id(&task), "s1");
task.meta.steps.push(Step {
id: "s1".into(),
title: "a".into(),
status: Status::Todo,
due_date: None,
depends_on: vec![],
});
task.meta.steps.push(Step {
id: "s7".into(),
title: "b".into(),
status: Status::Todo,
due_date: None,
depends_on: vec![],
});
assert_eq!(next_step_id(&task), "s8");
}
}