Skip to main content

singularity_cli/commands/
task.rs

1use anyhow::Result;
2use clap::{Subcommand, ValueEnum};
3use tabled::Table;
4
5use crate::client::ApiClient;
6use crate::models::task::{Task, TaskCreate, TaskListResponse, TaskUpdate};
7
8#[derive(Clone, ValueEnum)]
9pub enum Priority {
10    High,
11    Normal,
12    Low,
13}
14
15impl Priority {
16    fn as_i32(&self) -> i32 {
17        match self {
18            Priority::High => 0,
19            Priority::Normal => 1,
20            Priority::Low => 2,
21        }
22    }
23}
24
25#[derive(Clone, ValueEnum)]
26pub enum CheckedStatus {
27    Empty,
28    Checked,
29    Cancelled,
30}
31
32impl CheckedStatus {
33    fn as_i32(&self) -> i32 {
34        match self {
35            CheckedStatus::Empty => 0,
36            CheckedStatus::Checked => 1,
37            CheckedStatus::Cancelled => 2,
38        }
39    }
40}
41
42#[derive(Subcommand)]
43pub enum TaskCmd {
44    #[command(about = "List tasks with optional filters")]
45    List {
46        #[arg(long, help = "Filter by project ID (P-<uuid>)")]
47        project_id: Option<String>,
48        #[arg(long, help = "Filter by parent task ID (T-<uuid>)")]
49        parent: Option<String>,
50        #[arg(
51            long,
52            help = "Filter tasks starting on or after this date (ISO 8601, inclusive)"
53        )]
54        start_from: Option<String>,
55        #[arg(
56            long,
57            help = "Filter tasks starting on or before this date (ISO 8601, inclusive)"
58        )]
59        start_to: Option<String>,
60        #[arg(long, help = "Maximum number of tasks to return (max 1000)")]
61        max_count: Option<u32>,
62        #[arg(long, help = "Number of tasks to skip for pagination")]
63        offset: Option<u32>,
64        #[arg(long, help = "Include soft-deleted tasks")]
65        include_removed: bool,
66        #[arg(long, help = "Include archived tasks")]
67        include_archived: bool,
68    },
69    #[command(about = "Get a single task by ID")]
70    Get {
71        #[arg(help = "Task ID (T-<uuid> format)")]
72        id: String,
73    },
74    #[command(about = "Create a new task")]
75    Create {
76        #[arg(long, help = "Task title (required)")]
77        title: String,
78        #[arg(long, help = "Task description/notes")]
79        note: Option<String>,
80        #[arg(long, value_enum, help = "Task priority: high, normal, or low")]
81        priority: Option<Priority>,
82        #[arg(long, help = "Assign to project (P-<uuid>)")]
83        project_id: Option<String>,
84        #[arg(long, help = "Parent task ID for subtasks (T-<uuid>)")]
85        parent: Option<String>,
86        #[arg(long, help = "Task group ID (Q-<uuid>)")]
87        group: Option<String>,
88        #[arg(long, help = "Deadline date (ISO 8601)")]
89        deadline: Option<String>,
90        #[arg(long, help = "Start date (ISO 8601)")]
91        start: Option<String>,
92        #[arg(long, value_delimiter = ',', help = "Comma-separated tag IDs")]
93        tags: Option<Vec<String>>,
94    },
95    #[command(about = "Update an existing task (only specified fields are changed)")]
96    Update {
97        #[arg(help = "Task ID to update (T-<uuid> format)")]
98        id: String,
99        #[arg(long, help = "New task title")]
100        title: Option<String>,
101        #[arg(long, help = "New task description/notes")]
102        note: Option<String>,
103        #[arg(long, value_enum, help = "New priority: high, normal, or low")]
104        priority: Option<Priority>,
105        #[arg(
106            long,
107            value_enum,
108            help = "Completion status: empty, checked, or cancelled"
109        )]
110        checked: Option<CheckedStatus>,
111        #[arg(long, help = "Move to project (P-<uuid>)")]
112        project_id: Option<String>,
113        #[arg(long, help = "New parent task ID (T-<uuid>)")]
114        parent: Option<String>,
115        #[arg(long, help = "New task group ID (Q-<uuid>)")]
116        group: Option<String>,
117        #[arg(long, help = "New deadline date (ISO 8601)")]
118        deadline: Option<String>,
119        #[arg(long, help = "New start date (ISO 8601)")]
120        start: Option<String>,
121        #[arg(
122            long,
123            value_delimiter = ',',
124            help = "Replace tags with comma-separated tag IDs"
125        )]
126        tags: Option<Vec<String>>,
127    },
128    #[command(about = "Delete a task by ID (soft-delete)")]
129    Delete {
130        #[arg(help = "Task ID to delete (T-<uuid> format)")]
131        id: String,
132    },
133}
134
135pub fn run(client: &ApiClient, cmd: TaskCmd, json: bool) -> Result<()> {
136    match cmd {
137        TaskCmd::List {
138            project_id,
139            parent,
140            start_from,
141            start_to,
142            max_count,
143            offset,
144            include_removed,
145            include_archived,
146        } => {
147            let mut query: Vec<(&str, String)> = Vec::new();
148            if let Some(ref v) = project_id {
149                query.push(("projectId", v.to_string()));
150            }
151            if let Some(ref v) = parent {
152                query.push(("parent", v.to_string()));
153            }
154            if let Some(ref v) = start_from {
155                query.push(("startDateFrom", v.to_string()));
156            }
157            if let Some(ref v) = start_to {
158                query.push(("startDateTo", v.to_string()));
159            }
160            if let Some(v) = max_count {
161                query.push(("maxCount", v.to_string()));
162            }
163            if let Some(v) = offset {
164                query.push(("offset", v.to_string()));
165            }
166            if include_removed {
167                query.push(("includeRemoved", "true".to_string()));
168            }
169            if include_archived {
170                query.push(("includeArchived", "true".to_string()));
171            }
172
173            if json {
174                let resp: serde_json::Value = client.get("/v2/task", &query)?;
175                println!("{}", serde_json::to_string_pretty(&resp)?);
176            } else {
177                let resp: TaskListResponse = client.get("/v2/task", &query)?;
178                if resp.tasks.is_empty() {
179                    println!("No tasks found.");
180                } else {
181                    println!("{}", Table::new(&resp.tasks));
182                }
183            }
184        }
185        TaskCmd::Get { id } => {
186            if json {
187                let resp: serde_json::Value = client.get(&format!("/v2/task/{}", id), &[])?;
188                println!("{}", serde_json::to_string_pretty(&resp)?);
189            } else {
190                let task: Task = client.get(&format!("/v2/task/{}", id), &[])?;
191                task.display_detail();
192            }
193        }
194        TaskCmd::Create {
195            title,
196            note,
197            priority,
198            project_id,
199            parent,
200            group,
201            deadline,
202            start,
203            tags,
204        } => {
205            let data = TaskCreate {
206                title,
207                note,
208                priority: priority.map(|p| p.as_i32()),
209                project_id,
210                parent,
211                group,
212                deadline,
213                start,
214                tags,
215                is_note: None,
216            };
217            if json {
218                let resp: serde_json::Value = client.post("/v2/task", &data)?;
219                println!("{}", serde_json::to_string_pretty(&resp)?);
220            } else {
221                let task: Task = client.post("/v2/task", &data)?;
222                println!("Created task {}", task.id);
223            }
224        }
225        TaskCmd::Update {
226            id,
227            title,
228            note,
229            priority,
230            checked,
231            project_id,
232            parent,
233            group,
234            deadline,
235            start,
236            tags,
237        } => {
238            let data = TaskUpdate {
239                title,
240                note,
241                priority: priority.map(|p| p.as_i32()),
242                checked: checked.map(|c| c.as_i32()),
243                project_id,
244                parent,
245                group,
246                deadline,
247                start,
248                tags,
249                is_note: None,
250            };
251            if json {
252                let resp: serde_json::Value = client.patch(&format!("/v2/task/{}", id), &data)?;
253                println!("{}", serde_json::to_string_pretty(&resp)?);
254            } else {
255                let task: Task = client.patch(&format!("/v2/task/{}", id), &data)?;
256                println!("Updated task {}", task.id);
257            }
258        }
259        TaskCmd::Delete { id } => {
260            client.delete(&format!("/v2/task/{}", id))?;
261            println!("Deleted task {}", id);
262        }
263    }
264    Ok(())
265}