Skip to main content

singularity_cli/commands/
project.rs

1use anyhow::Result;
2use chrono_tz::Tz;
3use clap::Subcommand;
4
5use crate::client::ApiClient;
6use crate::models::project::{Project, ProjectCreate, ProjectListResponse, ProjectUpdate};
7
8#[derive(Subcommand)]
9pub enum ProjectCmd {
10    #[command(about = "List all projects")]
11    List {
12        #[arg(long, help = "Maximum number of projects to return (max 1000)")]
13        max_count: Option<u32>,
14        #[arg(long, help = "Number of projects to skip for pagination")]
15        offset: Option<u32>,
16        #[arg(long, help = "Include soft-deleted projects")]
17        include_removed: bool,
18        #[arg(long, help = "Include archived projects")]
19        include_archived: bool,
20    },
21    #[command(about = "Get a single project by ID")]
22    Get {
23        #[arg(help = "Project ID (P-<uuid> format)")]
24        id: String,
25    },
26    #[command(about = "Create a new project")]
27    Create {
28        #[arg(long, help = "Project title (required)")]
29        title: String,
30        #[arg(long, help = "Project description/notes")]
31        note: Option<String>,
32        #[arg(long, help = "Parent project ID (P-<uuid>) for nesting")]
33        parent: Option<String>,
34        #[arg(long, help = "Color hex code (e.g. #FF0000)")]
35        color: Option<String>,
36        #[arg(long, help = "Emoji icon for the project")]
37        emoji: Option<String>,
38        #[arg(long, help = "Start date (ISO 8601 format)")]
39        start: Option<String>,
40        #[arg(long, help = "End date (ISO 8601 format)")]
41        end: Option<String>,
42        #[arg(long, help = "Create as a notebook instead of a project")]
43        notebook: bool,
44    },
45    #[command(about = "Update an existing project (only specified fields are changed)")]
46    Update {
47        #[arg(help = "Project ID to update (P-<uuid> format)")]
48        id: String,
49        #[arg(long, help = "New project title")]
50        title: Option<String>,
51        #[arg(long, help = "New project description/notes")]
52        note: Option<String>,
53        #[arg(long, help = "New parent project ID (P-<uuid>)")]
54        parent: Option<String>,
55        #[arg(long, help = "New color hex code")]
56        color: Option<String>,
57        #[arg(long, help = "New emoji icon")]
58        emoji: Option<String>,
59        #[arg(long, help = "New start date (ISO 8601)")]
60        start: Option<String>,
61        #[arg(long, help = "New end date (ISO 8601)")]
62        end: Option<String>,
63        #[arg(long, help = "Set notebook flag (true/false)")]
64        notebook: Option<bool>,
65    },
66    #[command(about = "Delete a project by ID (soft-delete)")]
67    Delete {
68        #[arg(help = "Project ID to delete (P-<uuid> format)")]
69        id: String,
70    },
71}
72
73pub fn run(client: &ApiClient, cmd: ProjectCmd, json: bool, tz: Option<Tz>) -> Result<()> {
74    match cmd {
75        ProjectCmd::List {
76            max_count,
77            offset,
78            include_removed,
79            include_archived,
80        } => {
81            let mut query: Vec<(&str, String)> = Vec::new();
82            if let Some(v) = max_count {
83                query.push(("maxCount", v.to_string()));
84            }
85            if let Some(v) = offset {
86                query.push(("offset", v.to_string()));
87            }
88            if include_removed {
89                query.push(("includeRemoved", "true".to_string()));
90            }
91            if include_archived {
92                query.push(("includeArchived", "true".to_string()));
93            }
94
95            if json {
96                let resp: serde_json::Value = client.get("/v2/project", &query)?;
97                println!("{}", serde_json::to_string_pretty(&resp)?);
98            } else {
99                let resp: ProjectListResponse = client.get("/v2/project", &query)?;
100                if resp.projects.is_empty() {
101                    println!("No projects found.");
102                } else {
103                    for p in &resp.projects {
104                        println!("{}\n", p.display_list_item());
105                    }
106                }
107            }
108        }
109        ProjectCmd::Get { id } => {
110            if json {
111                let resp: serde_json::Value = client.get(&format!("/v2/project/{}", id), &[])?;
112                println!("{}", serde_json::to_string_pretty(&resp)?);
113            } else {
114                let project: Project = client.get(&format!("/v2/project/{}", id), &[])?;
115                println!("{}", project.display_detail(tz));
116            }
117        }
118        ProjectCmd::Create {
119            title,
120            note,
121            parent,
122            color,
123            emoji,
124            start,
125            end,
126            notebook,
127        } => {
128            let data = ProjectCreate {
129                title,
130                note,
131                parent,
132                color,
133                emoji,
134                start,
135                end,
136                is_notebook: if notebook { Some(true) } else { None },
137            };
138            if json {
139                let resp: serde_json::Value = client.post("/v2/project", &data)?;
140                println!("{}", serde_json::to_string_pretty(&resp)?);
141            } else {
142                let project: Project = client.post("/v2/project", &data)?;
143                println!("Created project {}", project.id);
144            }
145        }
146        ProjectCmd::Update {
147            id,
148            title,
149            note,
150            parent,
151            color,
152            emoji,
153            start,
154            end,
155            notebook,
156        } => {
157            let data = ProjectUpdate {
158                title,
159                note,
160                parent,
161                color,
162                emoji,
163                start,
164                end,
165                is_notebook: notebook,
166            };
167            if json {
168                let resp: serde_json::Value =
169                    client.patch(&format!("/v2/project/{}", id), &data)?;
170                println!("{}", serde_json::to_string_pretty(&resp)?);
171            } else {
172                let project: Project = client.patch(&format!("/v2/project/{}", id), &data)?;
173                println!("Updated project {}", project.id);
174            }
175        }
176        ProjectCmd::Delete { id } => {
177            client.delete(&format!("/v2/project/{}", id))?;
178            println!("Deleted project {}", id);
179        }
180    }
181    Ok(())
182}