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}