create.rs

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