create.rs

  1use anyhow::{anyhow, Result};
  2use loro::LoroMap;
  3use std::path::Path;
  4
  5use crate::db;
  6use crate::editor;
  7
  8pub struct Opts<'a> {
  9    pub title: Option<&'a str>,
 10    pub priority: db::Priority,
 11    pub effort: db::Effort,
 12    pub task_type: &'a str,
 13    pub desc: Option<&'a str>,
 14    pub parent: Option<&'a str>,
 15    pub labels: Option<&'a str>,
 16    pub json: bool,
 17}
 18
 19/// Template shown in the editor when the user runs `td create` without a title.
 20const TEMPLATE: &str = "
 21TD: Please provide the task title on the first line, and an optional
 22TD: description below. Lines starting with 'TD: ' will be ignored.
 23TD: An empty message aborts.";
 24
 25pub fn run(root: &Path, opts: Opts) -> Result<()> {
 26    // If neither title nor description were supplied, try to open an editor.
 27    // We treat the presence of TD_FORCE_EDITOR as an explicit interactive
 28    // signal (used by tests); otherwise we check whether stdin is a tty.
 29    let (title_owned, desc_owned);
 30    let (title, desc) = if opts.title.is_none() && opts.desc.is_none() {
 31        let interactive = std::env::var("TD_FORCE_EDITOR").is_ok()
 32            || std::io::IsTerminal::is_terminal(&std::io::stdin());
 33        if interactive {
 34            let (t, d) = editor::open(TEMPLATE)?;
 35            title_owned = t;
 36            desc_owned = d;
 37            (title_owned.as_str(), desc_owned.as_str())
 38        } else {
 39            return Err(anyhow!(
 40                "title required; provide it as a positional argument or run interactively to open an editor"
 41            ));
 42        }
 43    } else {
 44        (
 45            opts.title.ok_or_else(|| anyhow!("title required"))?,
 46            opts.desc.unwrap_or(""),
 47        )
 48    };
 49
 50    let ts = db::now_utc();
 51    let store = db::open(root)?;
 52    let id = db::gen_id();
 53
 54    let parent = if let Some(raw) = opts.parent {
 55        Some(db::resolve_task_id(&store, raw, false)?)
 56    } else {
 57        None
 58    };
 59
 60    store.apply_and_persist(|doc| {
 61        let tasks = doc.get_map("tasks");
 62        let task = db::insert_task_map(&tasks, &id)?;
 63
 64        task.insert("title", title)?;
 65        task.insert("description", desc)?;
 66        task.insert("type", opts.task_type)?;
 67        task.insert("priority", db::priority_label(opts.priority))?;
 68        task.insert("status", db::status_label(db::Status::Open))?;
 69        task.insert("effort", db::effort_label(opts.effort))?;
 70        task.insert("parent", parent.as_ref().map(|p| p.as_str()).unwrap_or(""))?;
 71        task.insert("created_at", ts.clone())?;
 72        task.insert("updated_at", ts.clone())?;
 73        task.insert("deleted_at", "")?;
 74        task.insert_container("labels", LoroMap::new())?;
 75        task.insert_container("blockers", LoroMap::new())?;
 76        task.insert_container("logs", LoroMap::new())?;
 77
 78        if let Some(label_str) = opts.labels {
 79            let labels = db::get_or_create_child_map(&task, "labels")?;
 80            for lbl in label_str
 81                .split(',')
 82                .map(str::trim)
 83                .filter(|l| !l.is_empty())
 84            {
 85                labels.insert(lbl, true)?;
 86            }
 87        }
 88
 89        Ok(())
 90    })?;
 91
 92    let task = store
 93        .get_task(&id, false)?
 94        .ok_or_else(|| anyhow!("failed to reload created task"))?;
 95
 96    if opts.json {
 97        println!("{}", serde_json::to_string(&task)?);
 98    } else {
 99        let c = crate::color::stdout_theme();
100        println!("{}created{} {}: {}", c.green, c.reset, task.id, task.title);
101    }
102
103    Ok(())
104}