use std::io::IsTerminal;
use std::path::PathBuf;
use crate::cli::CycleCommand;
use crate::config::Config;
use crate::error::{Result, TixError};
use crate::planning::Planner;
use crate::storage::Store;
fn root_from_store(store: &Store) -> PathBuf {
Config::project_root(&store.issues_dir).unwrap_or_else(|| PathBuf::from("."))
}
pub fn run(config: &Config, store: &Store, cmd: &CycleCommand) -> Result<()> {
let mut planner = Planner::from_config(config.clone(), root_from_store(store));
match cmd {
CycleCommand::Create {
name,
description,
start_date,
due_date,
} => create(
&mut planner,
name.as_deref(),
description.as_deref(),
start_date.as_deref(),
due_date.as_deref(),
),
CycleCommand::List => list(&planner, store),
CycleCommand::Edit {
name,
new_name,
description,
start_date,
due_date,
} => edit(
&mut planner,
store,
name,
new_name.as_deref(),
description.as_deref(),
start_date.as_deref(),
due_date.as_deref(),
),
CycleCommand::Delete { name, yes } => delete(&mut planner, store, name, *yes),
}
}
fn create(
planner: &mut Planner,
name: Option<&str>,
description: Option<&str>,
start_date: Option<&str>,
due_date: Option<&str>,
) -> Result<()> {
let name = planner.add_cycle(name.map(str::to_string))?;
planner.set_cycle_fields(&name, description, start_date, due_date)?;
let overlaps = planner.overlapping_cycles(&name);
println!("{name}");
if !overlaps.is_empty() {
eprintln!(
"warning: cycle `{name}` overlaps existing cycle(s): {}",
overlaps.join(", ")
);
}
Ok(())
}
fn list(planner: &Planner, store: &Store) -> Result<()> {
for name in planner.cycle_names() {
let Some(cycle) = planner.cycle(&name) else {
continue;
};
let issues = store
.list()?
.into_iter()
.filter(|issue| {
issue
.cycles
.as_ref()
.is_some_and(|cycles| cycles.iter().any(|cycle_name| cycle_name == &name))
})
.count();
let description = cycle.description.as_deref().unwrap_or("");
let start = cycle.start_date.as_deref().unwrap_or("");
let due = cycle.due_date.as_deref().unwrap_or("");
println!("{name}\t{description}\t{start}\t{due}\t{issues}");
}
Ok(())
}
fn edit(
planner: &mut Planner,
store: &Store,
name: &str,
new_name: Option<&str>,
description: Option<&str>,
start_date: Option<&str>,
due_date: Option<&str>,
) -> Result<()> {
let final_name = if let Some(new_name) = new_name {
let impact = planner.rename_cycle_cascade(store, name, new_name)?;
println!(
"renamed cycle `{name}` to `{new_name}`; updated {} issue(s)",
impact.changed_issue_count
);
new_name
} else {
name
};
if description.is_some() || start_date.is_some() || due_date.is_some() {
planner.set_cycle_fields(final_name, description, start_date, due_date)?;
}
Ok(())
}
fn delete(planner: &mut Planner, store: &Store, name: &str, yes: bool) -> Result<()> {
let impact = planner.delete_impact(store, name)?;
if !impact.referencing_issues.is_empty() {
eprintln!(
"cycle `{name}` is referenced by {} issue(s): {}",
impact.referencing_issues.len(),
impact.referencing_issues.join(", ")
);
}
let mut confirmed = yes || !std::io::stdin().is_terminal();
if !confirmed {
eprint!("Delete cycle '{name}'? [y/N] ");
let mut line = String::new();
std::io::stdin()
.read_line(&mut line)
.map_err(TixError::Io)?;
confirmed = line.trim().eq_ignore_ascii_case("y");
}
if !confirmed {
eprintln!("aborted");
return Ok(());
}
planner.delete_cycle(name)?;
println!("deleted cycle `{name}`");
if !impact.referencing_issues.is_empty() {
println!("note: issue cycle references were left unchanged");
}
Ok(())
}