use chrono::{Local, NaiveDate};
use crate::cli::RecurCommand;
use crate::error::{Error, Result};
use crate::model::Priority;
use crate::recur::{self, Rule, RuleMeta, StoredRule};
use super::task::{active_library_root, confirm};
pub fn run(command: RecurCommand) -> Result<()> {
match command {
RecurCommand::Add {
title,
rule,
description,
priority,
tags,
start,
until,
max_count,
} => add(title, &rule, description, priority, tags, start, until, max_count),
RecurCommand::List => list(),
RecurCommand::Show { rule } => show(&rule),
RecurCommand::Pause { rule } => set_enabled(&rule, false),
RecurCommand::Resume { rule } => set_enabled(&rule, true),
RecurCommand::Remove { rule, force } => remove(&rule, force),
RecurCommand::Tick => tick(),
}
}
fn parse_date(label: &str, raw: &str) -> Result<NaiveDate> {
raw.parse().map_err(|_| {
Error::InvalidTaskFile(format!("invalid {label} '{raw}', expected YYYY-MM-DD"))
})
}
#[allow(clippy::too_many_arguments)]
fn add(
title: String,
rule: &str,
description: Option<String>,
priority: Option<String>,
tags: Vec<String>,
start: Option<String>,
until: Option<String>,
max_count: Option<u64>,
) -> Result<()> {
let root = active_library_root()?;
let rule: Rule = rule.parse().map_err(Error::BadRule)?;
let start_date = match &start {
Some(s) => parse_date("start date", s)?,
None => Local::now().date_naive(),
};
let mut meta = RuleMeta::new(title, rule, start_date);
meta.description = description;
meta.tags = tags;
meta.until = until.as_deref().map(|u| parse_date("until date", u)).transpose()?;
meta.max_count = max_count;
if let Some(p) = &priority {
meta.priority = p.parse::<Priority>().map_err(Error::InvalidTaskFile)?;
} else if let Some(p) = crate::config::Config::load().ok().and_then(|c| c.default_priority) {
meta.priority = p.parse::<Priority>().map_err(Error::InvalidTaskFile)?;
}
if let Some(until) = meta.until
&& until < meta.start_date
{
return Err(Error::BadRule(format!(
"--until {until} is before the start date {}",
meta.start_date
)));
}
let path = recur::rule_path(&root, &meta);
if path.exists() {
return Err(Error::InvalidTaskFile(format!("{} already exists", path.display())));
}
let stored = StoredRule { path, meta, body: String::new() };
recur::save(&stored)?;
println!(
"created rule [{}] {} ({})",
stored.meta.short_id(),
stored.meta.title,
stored.meta.rule
);
println!(" next: {}", stored.meta.next_run);
println!(" {}", stored.path.display());
Ok(())
}
fn list() -> Result<()> {
let root = active_library_root()?;
let rules = recur::load_all(&root)?;
if rules.is_empty() {
println!("no rules; create one with 'tasks recur add <title> --rule daily'");
return Ok(());
}
println!("{:<9} {:<16} {:<12} {:<10} {:<7} TITLE", "ID", "RULE", "NEXT", "STATE", "COUNT");
for stored in &rules {
let m = &stored.meta;
let state = if !m.enabled {
"paused".to_string()
} else if m.exhausted() {
"finished".to_string()
} else {
"active".to_string()
};
let count = match m.max_count {
Some(max) => format!("{}/{max}", m.created_count),
None => m.created_count.to_string(),
};
println!(
"{:<9} {:<16} {:<12} {:<10} {:<7} {}",
m.short_id(),
m.rule.to_string(),
m.next_run.to_string(),
state,
count,
m.title
);
}
Ok(())
}
fn show(query: &str) -> Result<()> {
let root = active_library_root()?;
let stored = recur::resolve(&root, query)?;
let m = &stored.meta;
println!("[{}] {}", m.short_id(), m.title);
println!(" uuid: {}", m.id);
println!(" rule: {}", m.rule);
println!(" enabled: {}", m.enabled);
println!(" priority: {}", m.priority);
println!(" tags: {}", if m.tags.is_empty() { "-".into() } else { m.tags.join(", ") });
if let Some(d) = &m.description {
println!(" desc: {d}");
}
println!(" start: {}", m.start_date);
if let Some(until) = m.until {
println!(" until: {until}");
}
println!(" next: {}", m.next_run);
if let Some(last) = m.last_run {
println!(" last: {last}");
}
match m.max_count {
Some(max) => println!(" generated: {}/{max}", m.created_count),
None => println!(" generated: {}", m.created_count),
}
if m.exhausted() {
println!(" state: finished");
}
println!(" file: {}", stored.path.display());
if !stored.body.is_empty() {
println!("\n{}", stored.body.trim_end());
}
Ok(())
}
fn set_enabled(query: &str, enabled: bool) -> Result<()> {
let root = active_library_root()?;
let mut stored = recur::resolve(&root, query)?;
if stored.meta.enabled == enabled {
println!(
"rule {} is already {}",
stored.meta.title,
if enabled { "active" } else { "paused" }
);
return Ok(());
}
stored.meta.enabled = enabled;
recur::save(&stored)?;
println!(
"{} {}",
if enabled { "resumed" } else { "paused" },
stored.meta.title
);
if enabled {
println!(" next: {}", stored.meta.next_run);
}
Ok(())
}
fn remove(query: &str, force: bool) -> Result<()> {
let root = active_library_root()?;
let stored = recur::resolve(&root, query)?;
if !force && !confirm(&format!("delete rule '{}'?", stored.meta.title))? {
println!("aborted");
return Ok(());
}
std::fs::remove_file(&stored.path)?;
println!("removed rule '{}'", stored.meta.title);
Ok(())
}
fn tick() -> Result<()> {
let root = active_library_root()?;
let generated = recur::run_due(&root, Local::now().date_naive())?;
if generated.is_empty() {
println!("no rules are due");
return Ok(());
}
for g in &generated {
println!("created task #{} {} (for {})", g.seq, g.title, g.occurrence);
println!(" {}", g.path.display());
}
println!("generated {} task(s)", generated.len());
Ok(())
}
pub fn autorun() {
let Ok(root) = active_library_root() else { return };
match recur::run_due(&root, Local::now().date_naive()) {
Ok(generated) if !generated.is_empty() => {
let summary: Vec<String> =
generated.iter().map(|g| format!("#{} {}", g.seq, g.title)).collect();
eprintln!(
"generated {} recurring task(s): {}",
generated.len(),
summary.join(", ")
);
}
Ok(_) => {}
Err(err) => eprintln!("warning: recurring task generation failed: {err}"),
}
}