Skip to main content

singularity_cli/commands/
project.rs

1use anyhow::Result;
2use clap::Subcommand;
3use tabled::Table;
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) -> 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                    println!("{}", Table::new(&resp.projects));
104                }
105            }
106        }
107        ProjectCmd::Get { id } => {
108            if json {
109                let resp: serde_json::Value = client.get(&format!("/v2/project/{}", id), &[])?;
110                println!("{}", serde_json::to_string_pretty(&resp)?);
111            } else {
112                let project: Project = client.get(&format!("/v2/project/{}", id), &[])?;
113                project.display_detail();
114            }
115        }
116        ProjectCmd::Create {
117            title,
118            note,
119            parent,
120            color,
121            emoji,
122            start,
123            end,
124            notebook,
125        } => {
126            let data = ProjectCreate {
127                title,
128                note,
129                parent,
130                color,
131                emoji,
132                start,
133                end,
134                is_notebook: if notebook { Some(true) } else { None },
135            };
136            if json {
137                let resp: serde_json::Value = client.post("/v2/project", &data)?;
138                println!("{}", serde_json::to_string_pretty(&resp)?);
139            } else {
140                let project: Project = client.post("/v2/project", &data)?;
141                println!("Created project {}", project.id);
142            }
143        }
144        ProjectCmd::Update {
145            id,
146            title,
147            note,
148            parent,
149            color,
150            emoji,
151            start,
152            end,
153            notebook,
154        } => {
155            let data = ProjectUpdate {
156                title,
157                note,
158                parent,
159                color,
160                emoji,
161                start,
162                end,
163                is_notebook: notebook,
164            };
165            if json {
166                let resp: serde_json::Value =
167                    client.patch(&format!("/v2/project/{}", id), &data)?;
168                println!("{}", serde_json::to_string_pretty(&resp)?);
169            } else {
170                let project: Project = client.patch(&format!("/v2/project/{}", id), &data)?;
171                println!("Updated project {}", project.id);
172            }
173        }
174        ProjectCmd::Delete { id } => {
175            client.delete(&format!("/v2/project/{}", id))?;
176            println!("Deleted project {}", id);
177        }
178    }
179    Ok(())
180}