From 8c6310eb6140e932d0af4af21b81bfadac42c2f9 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 7 Jun 2024 08:08:51 +0200 Subject: [PATCH 01/10] add fav --- src/api/mod.rs | 10 ++++++++++ src/args.rs | 7 +++++++ src/main.rs | 7 +++++++ 3 files changed, 24 insertions(+) diff --git a/src/api/mod.rs b/src/api/mod.rs index 5efcd33..83dedb1 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -298,6 +298,16 @@ impl VikunjaAPI { serde_json::from_str(&resp).unwrap() } + pub fn fav_task(&self, task_id: isize, fav: bool) -> Task { + let resp = self.post_request( + &format!("/tasks/{task_id}"), + &serde_json::json!({ + "is_favorite": fav + }), + ); + serde_json::from_str(&resp).unwrap() + } + pub fn login(&self, username: &str, password: &str, totp: Option<&str>) -> String { let resp = self.post_request( "/login", diff --git a/src/args.rs b/src/args.rs index 071d7f7..4a2d56b 100644 --- a/src/args.rs +++ b/src/args.rs @@ -68,6 +68,13 @@ pub fn get_args() -> clap::ArgMatches { .arg(arg!([user] "User").required(true)) .arg(arg!([task_id] "Task ID").required(true)), ) + .subcommand( + command!() + .name("fav") + .about("Favorite a task") + .arg(arg!(-u --undo "Remove favorite from task").required(false)) + .arg(arg!([task_id] "Task ID").required(true)), + ) .subcommand( command!() .name("label") diff --git a/src/main.rs b/src/main.rs index fcbeb14..51b5f46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -132,6 +132,13 @@ fn main() { api.done_task(task_id.parse().unwrap(), done); ui::task::print_task_info(task_id.parse().unwrap(), &api); } + Some(("fav", fav_args)) => { + let task_id: &String = fav_args.get_one("task_id").unwrap(); + let undo = fav_args.get_flag("undo"); + + api.fav_task(task_id.parse().unwrap(), !undo); + ui::task::print_task_info(task_id.parse().unwrap(), &api); + } _ => { let done = arg.get_flag("done"); let fav = arg.get_flag("favorite"); From 7c6efb47559a640ac88ebb6dd6735fddcadd4978 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 7 Jun 2024 08:36:34 +0200 Subject: [PATCH 02/10] add comments --- src/api/mod.rs | 6 ++++++ src/api/task.rs | 9 +++++++++ src/args.rs | 6 ++++++ src/main.rs | 19 +++++++++++++++---- src/ui/task.rs | 13 ++++++++++++- 5 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 83dedb1..80ddad4 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,6 +4,7 @@ mod project; mod task; pub use project::Project; +pub use task::Comment; pub use task::Task; use moka::sync::Cache; @@ -349,4 +350,9 @@ impl VikunjaAPI { let user_id = user.first().unwrap().id; self.delete_request(&format!("/tasks/{task_id}/assignees/{user_id}")); } + + pub fn get_task_comments(&self, task_id: isize) -> Vec { + let resp = self.get_request(&format!("/tasks/{task_id}/comments")); + serde_json::from_str(&resp).unwrap() + } } diff --git a/src/api/task.rs b/src/api/task.rs index df4b47b..06a2003 100644 --- a/src/api/task.rs +++ b/src/api/task.rs @@ -34,3 +34,12 @@ pub struct Task { pub kanban_position: f64, pub created_by: Option, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Comment { + pub author: User, + pub comment: String, + pub created: String, + pub id: isize, + pub updated: String, +} diff --git a/src/args.rs b/src/args.rs index 4a2d56b..03526ee 100644 --- a/src/args.rs +++ b/src/args.rs @@ -68,6 +68,12 @@ pub fn get_args() -> clap::ArgMatches { .arg(arg!([user] "User").required(true)) .arg(arg!([task_id] "Task ID").required(true)), ) + .subcommand( + command!() + .name("comments") + .about("Show task comments") + .arg(arg!([task_id] "Task ID").required(true)), + ) .subcommand( command!() .name("fav") diff --git a/src/main.rs b/src/main.rs index 51b5f46..245807d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,10 @@ mod ui; use api::{ProjectID, VikunjaAPI}; +// todo : error handling +// todo : task relations +// todo : task comments + fn main() { let arg = args::get_args(); let config_path = dirs::home_dir().unwrap().join(".config").join("vk.toml"); @@ -86,10 +90,15 @@ fn main() { api.assign_to_task(user, task_id.parse().unwrap()); } } - Some(("labels", label_args)) => match label_args.subcommand() { - Some(("ls", _)) => { - ui::print_all_labels(&api); + Some(("comments", c_arg)) => { + let task_id: &String = c_arg.get_one("task_id").unwrap(); + let comments = api.get_task_comments(task_id.parse().unwrap()); + + for comment in comments { + ui::task::print_comment(&comment); } + } + Some(("labels", label_args)) => match label_args.subcommand() { Some(("rm", rm_label_arg)) => { let title: &String = rm_label_arg.get_one("title").unwrap(); @@ -106,7 +115,9 @@ fn main() { color.map(|x| x.as_str()), ); } - _ => {} + _ => { + ui::print_all_labels(&api); + } }, Some(("label", label_args)) => { let label: &String = label_args.get_one("label").unwrap(); diff --git a/src/ui/task.rs b/src/ui/task.rs index c260c4b..e20cd8d 100644 --- a/src/ui/task.rs +++ b/src/ui/task.rs @@ -1,5 +1,5 @@ use crate::{ - api::{Project, ProjectID, Task, VikunjaAPI}, + api::{Comment, Project, ProjectID, Task, VikunjaAPI}, ui::{ format_html_to_terminal, hex_to_color, is_in_past, parse_datetime, print_color, print_label, time_relative, @@ -171,3 +171,14 @@ pub fn print_task_info(task_id: isize, api: &VikunjaAPI) { // pub percent_done: f64, } + +pub fn print_comment(comment: &Comment) { + print_color(crossterm::style::Color::Blue, &comment.author.username); + print!( + " ({}): ", + time_relative(parse_datetime(&comment.created).unwrap()) + ); + println!(); + print!("{}", format_html_to_terminal(&comment.comment)); + println!(); +} From 41524a18afb5cfba3dfcefd9a13b64b4df00770b Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 7 Jun 2024 09:22:08 +0200 Subject: [PATCH 03/10] comments --- src/api/mod.rs | 10 ++++++++++ src/args.rs | 7 +++++++ src/main.rs | 7 ++++++- 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 80ddad4..b4c5912 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -355,4 +355,14 @@ impl VikunjaAPI { let resp = self.get_request(&format!("/tasks/{task_id}/comments")); serde_json::from_str(&resp).unwrap() } + + pub fn new_comment(&self, task_id: isize, comment: &str) -> Comment { + let resp = self.put_request( + &format!("/tasks/{task_id}/comments"), + &serde_json::json!({ + "comment": comment + }), + ); + serde_json::from_str(&resp).unwrap() + } } diff --git a/src/args.rs b/src/args.rs index 03526ee..8484044 100644 --- a/src/args.rs +++ b/src/args.rs @@ -74,6 +74,13 @@ pub fn get_args() -> clap::ArgMatches { .about("Show task comments") .arg(arg!([task_id] "Task ID").required(true)), ) + .subcommand( + command!() + .name("comment") + .about("Comment on a task") + .arg(arg!([task_id] "Task ID").required(true)) + .arg(arg!([comment] "Comment").required(true)), + ) .subcommand( command!() .name("fav") diff --git a/src/main.rs b/src/main.rs index 245807d..7e45301 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,6 @@ use api::{ProjectID, VikunjaAPI}; // todo : error handling // todo : task relations -// todo : task comments fn main() { let arg = args::get_args(); @@ -98,6 +97,12 @@ fn main() { ui::task::print_comment(&comment); } } + Some(("comment", comment_arg)) => { + let task_id: &String = comment_arg.get_one("task_id").unwrap(); + let comment: &String = comment_arg.get_one("comment").unwrap(); + + api.new_comment(task_id.parse().unwrap(), comment); + } Some(("labels", label_args)) => match label_args.subcommand() { Some(("rm", rm_label_arg)) => { let title: &String = rm_label_arg.get_one("title").unwrap(); From a721556902501569a41a1c1466ad0d3bccd118ff Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 7 Jun 2024 10:48:38 +0200 Subject: [PATCH 04/10] add relations --- src/api/mod.rs | 26 +++++++++++++++ src/api/task.rs | 84 ++++++++++++++++++++++++++++++++++++++++++++++++- src/args.rs | 9 ++++++ src/main.rs | 27 ++++++++++++++-- src/ui/task.rs | 25 ++++++++++++--- 5 files changed, 163 insertions(+), 8 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index b4c5912..1c01bfa 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -5,9 +5,11 @@ mod task; pub use project::Project; pub use task::Comment; +pub use task::Relation; pub use task::Task; use moka::sync::Cache; +use task::TaskRelation; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Label { @@ -356,6 +358,30 @@ impl VikunjaAPI { serde_json::from_str(&resp).unwrap() } + pub fn remove_relation(&self, task_id: isize, relation: Relation, other_task_id: isize) { + self.delete_request(&format!( + "/tasks/{task_id}/relations/{}/{other_task_id}", + relation.api() + )); + } + + pub fn add_relation( + &self, + task_id: isize, + relation: Relation, + other_task_id: isize, + ) -> TaskRelation { + let resp = self.put_request( + &format!("/tasks/{task_id}/relations"), + &serde_json::json!({ + "task_id": task_id, + "other_task_id": other_task_id, + "relation_kind": relation.api() + }), + ); + serde_json::from_str(&resp).unwrap() + } + pub fn new_comment(&self, task_id: isize, comment: &str) -> Comment { let resp = self.put_request( &format!("/tasks/{task_id}/comments"), diff --git a/src/api/task.rs b/src/api/task.rs index 06a2003..91d636a 100644 --- a/src/api/task.rs +++ b/src/api/task.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use super::{Label, User}; @@ -23,7 +25,7 @@ pub struct Task { pub percent_done: f64, pub identifier: String, pub index: usize, - // pub related_tasks + pub related_tasks: Option>>, // pub attachments pub cover_image_attachment_id: usize, pub is_favorite: bool, @@ -43,3 +45,83 @@ pub struct Comment { pub id: isize, pub updated: String, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TaskRelation { + pub created: String, + pub created_by: User, + pub other_task_id: isize, + pub task_id: isize, + pub relation_kind: String, +} + +pub enum Relation { + Unknown, + Subtask, + ParentTask, + Related, + DuplicateOf, + Duplicates, + Blocking, + Blocked, + Precedes, + Follows, + CopiedFrom, + CopiedTo, +} + +impl Relation { + pub fn try_parse(val: &str) -> Option { + match val { + "unknown" => Some(Self::Unknown), + "subtask" | "sub" => Some(Self::Subtask), + "parenttask" | "parent" => Some(Self::ParentTask), + "related" => Some(Self::Related), + "duplicateof" => Some(Self::DuplicateOf), + "duplicates" => Some(Self::Duplicates), + "blocking" => Some(Self::Blocking), + "blocked" => Some(Self::Blocked), + "precedes" => Some(Self::Precedes), + "follows" => Some(Self::Follows), + "copiedfrom" => Some(Self::CopiedFrom), + "copiedto" => Some(Self::CopiedTo), + _ => None, + } + } + + pub fn repr(&self) -> String { + match self { + Relation::Unknown => "Unknown", + Relation::Subtask => "Subtask", + Relation::ParentTask => "Parent Task", + Relation::Related => "Related", + Relation::DuplicateOf => "Duplicate of", + Relation::Duplicates => "Duplicates", + Relation::Blocking => "Blocking", + Relation::Blocked => "Blocked by", + Relation::Precedes => "Precedes", + Relation::Follows => "Follows", + Relation::CopiedFrom => "Copied from", + Relation::CopiedTo => "Copied to", + } + .to_string() + } + + pub fn api(&self) -> String { + match self { + Relation::Unknown => "unknown", + Relation::Subtask => "subtask", + Relation::ParentTask => "parenttask", + Relation::Related => "related", + Relation::DuplicateOf => "duplicateof", + Relation::Duplicates => "duplicates", + Relation::Blocking => "blocking", + Relation::Blocked => "blocked", + Relation::Precedes => "precedes", + Relation::Follows => "follows", + Relation::CopiedFrom => "copiedfrom", + Relation::CopiedTo => "copiedto", + } + .to_string() + } +} diff --git a/src/args.rs b/src/args.rs index 8484044..c0694e0 100644 --- a/src/args.rs +++ b/src/args.rs @@ -81,6 +81,15 @@ pub fn get_args() -> clap::ArgMatches { .arg(arg!([task_id] "Task ID").required(true)) .arg(arg!([comment] "Comment").required(true)), ) + .subcommand( + command!() + .name("relation") + .about("Set task relations") + .arg(arg!(-d --delete "Delete the relation").required(false)) + .arg(arg!([task_id] "Task ID").required(true)) + .arg(arg!([relation] "Relation").required(true)) + .arg(arg!([second_task_id] "Other Task ID").required(true)), + ) .subcommand( command!() .name("fav") diff --git a/src/main.rs b/src/main.rs index 7e45301..72555ca 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,10 +3,9 @@ mod args; mod config; mod ui; -use api::{ProjectID, VikunjaAPI}; +use api::{ProjectID, Relation, VikunjaAPI}; // todo : error handling -// todo : task relations fn main() { let arg = args::get_args(); @@ -155,6 +154,30 @@ fn main() { api.fav_task(task_id.parse().unwrap(), !undo); ui::task::print_task_info(task_id.parse().unwrap(), &api); } + Some(("relation", rel_args)) => { + let task_id: &String = rel_args.get_one("task_id").unwrap(); + let relation: &String = rel_args.get_one("relation").unwrap(); + let sec_task_id: &String = rel_args.get_one("second_task_id").unwrap(); + let delete = rel_args.get_flag("delete"); + + let relation = Relation::try_parse(&relation).unwrap(); + + if delete { + api.remove_relation( + task_id.parse().unwrap(), + relation, + sec_task_id.parse().unwrap(), + ); + } else { + api.add_relation( + task_id.parse().unwrap(), + relation, + sec_task_id.parse().unwrap(), + ); + } + + ui::task::print_task_info(task_id.parse().unwrap(), &api); + } _ => { let done = arg.get_flag("done"); let fav = arg.get_flag("favorite"); diff --git a/src/ui/task.rs b/src/ui/task.rs index e20cd8d..aa10638 100644 --- a/src/ui/task.rs +++ b/src/ui/task.rs @@ -1,5 +1,5 @@ use crate::{ - api::{Comment, Project, ProjectID, Task, VikunjaAPI}, + api::{Comment, Project, ProjectID, Relation, Task, VikunjaAPI}, ui::{ format_html_to_terminal, hex_to_color, is_in_past, parse_datetime, print_color, print_label, time_relative, @@ -157,10 +157,6 @@ pub fn print_task_info(task_id: isize, api: &VikunjaAPI) { println!(); } - if task.description != "

" && !task.description.is_empty() { - println!("---\n{}", format_html_to_terminal(&task.description)); - } - if let Some(assigned) = task.assignees { print!("Assigned to: "); for assignee in assigned { @@ -169,6 +165,25 @@ pub fn print_task_info(task_id: isize, api: &VikunjaAPI) { println!(); } + if let Some(related) = task.related_tasks { + for relation in related { + print_color( + crossterm::style::Color::Magenta, + &format!("{}: ", Relation::try_parse(&relation.0).unwrap().repr()), + ); + for t in relation.1 { + print_color(crossterm::style::Color::Blue, &t.title); + print_color(crossterm::style::Color::Yellow, &format!(" ({})", t.id)); + print!(" "); + } + println!(); + } + } + + if task.description != "

" && !task.description.is_empty() { + println!("---\n{}", format_html_to_terminal(&task.description)); + } + // pub percent_done: f64, } From 477ce5ca98183856172a699e0b36ce6d7a6610c3 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 7 Jun 2024 11:48:16 +0200 Subject: [PATCH 05/10] refactor --- Cargo.lock | 1 + Cargo.toml | 1 + src/api/mod.rs | 42 ++++++++---- src/api/project.rs | 2 +- src/api/task.rs | 48 +++++++------- src/main.rs | 155 +++++++++++++++++++++++++++------------------ src/ui/mod.rs | 9 ++- src/ui/project.rs | 4 +- src/ui/task.rs | 15 +++-- 9 files changed, 164 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c85a573..16af043 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1681,6 +1681,7 @@ dependencies = [ "dirs", "html2text", "moka", + "once_cell", "reqwest", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index f825df1..cfff14d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ crossterm = "0.27.0" dirs = "5.0.1" html2text = "0.12.5" moka = { version = "0.12.7", features = ["sync"] } +once_cell = "1.19.0" reqwest = { version = "0.12.4", features = ["blocking", "json"] } serde = { version = "1.0.203", features = ["derive"] } serde_json = "1.0.117" diff --git a/src/api/mod.rs b/src/api/mod.rs index 1c01bfa..70b1be9 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -11,6 +11,14 @@ pub use task::Task; use moka::sync::Cache; use task::TaskRelation; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VikunjaError { + pub code: Option, + pub message: String, +} + +impl VikunjaError {} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Label { pub id: usize, @@ -155,7 +163,7 @@ impl VikunjaAPI { serde_json::from_str(&resp).unwrap() } - pub fn delete_project(&self, project_id: ProjectID) { + pub fn delete_project(&self, project_id: &ProjectID) { self.delete_request(&format!("/projects/{}", project_id.0)); } @@ -202,7 +210,6 @@ impl VikunjaAPI { "hex_color": color }), ); - serde_json::from_str(&resp).unwrap() } @@ -230,13 +237,13 @@ impl VikunjaAPI { self.delete_request(&format!("/tasks/{task_id}/labels/{label_id}")); } - pub fn label_task(&self, label: &str, task_id: isize) { + pub fn label_task(&self, label: &str, task_id: isize) -> Result<(), String> { let labels = self.get_all_labels(); let label_id = labels .into_iter() .find(|x| x.title.trim() == label) - .unwrap() + .map_or_else(|| Err(format!("Label '{label}' not found")), Ok)? .id; self.put_request( @@ -245,6 +252,8 @@ impl VikunjaAPI { "label_id": label_id }), ); + + Ok(()) } // tasks @@ -262,9 +271,9 @@ impl VikunjaAPI { serde_json::from_str(&resp).unwrap() } - pub fn get_task(&self, id: isize) -> Task { + pub fn get_task(&self, id: isize) -> Result { let resp = self.get_request(&format!("/tasks/{id}")); - serde_json::from_str(&resp).unwrap() + serde_json::from_str(&resp).map_or(Err(()), Ok) } pub fn delete_task(&self, id: isize) { @@ -290,7 +299,7 @@ impl VikunjaAPI { serde_json::from_str(&resp).unwrap() } - pub fn done_task(&self, task_id: isize, done: bool) -> Task { + pub fn done_task(&self, task_id: isize, done: bool) -> Option { let resp = self.post_request( &format!("/tasks/{task_id}"), &serde_json::json!({ @@ -298,17 +307,18 @@ impl VikunjaAPI { "done_at": if done { Some(chrono::Utc::now().to_rfc3339()) } else { None } }), ); - serde_json::from_str(&resp).unwrap() + serde_json::from_str(&resp).ok() } - pub fn fav_task(&self, task_id: isize, fav: bool) -> Task { + pub fn fav_task(&self, task_id: isize, fav: bool) -> Option { let resp = self.post_request( &format!("/tasks/{task_id}"), &serde_json::json!({ "is_favorite": fav }), ); - serde_json::from_str(&resp).unwrap() + + serde_json::from_str(&resp).ok() } pub fn login(&self, username: &str, password: &str, totp: Option<&str>) -> String { @@ -336,8 +346,10 @@ impl VikunjaAPI { serde_json::from_str(&resp).ok() } - pub fn assign_to_task(&self, user: &str, task_id: isize) { - let user = self.search_user(user).unwrap(); + pub fn assign_to_task(&self, user: &str, task_id: isize) -> Result<(), String> { + let user = self + .search_user(user) + .map_or_else(|| Err(String::from("User not found")), Ok)?; self.put_request( &format!("/tasks/{task_id}/assignees"), @@ -345,6 +357,8 @@ impl VikunjaAPI { "user_id": user.first().unwrap().id }), ); + + Ok(()) } pub fn remove_assign_to_task(&self, user: &str, task_id: isize) { @@ -358,7 +372,7 @@ impl VikunjaAPI { serde_json::from_str(&resp).unwrap() } - pub fn remove_relation(&self, task_id: isize, relation: Relation, other_task_id: isize) { + pub fn remove_relation(&self, task_id: isize, relation: &Relation, other_task_id: isize) { self.delete_request(&format!( "/tasks/{task_id}/relations/{}/{other_task_id}", relation.api() @@ -368,7 +382,7 @@ impl VikunjaAPI { pub fn add_relation( &self, task_id: isize, - relation: Relation, + relation: &Relation, other_task_id: isize, ) -> TaskRelation { let resp = self.put_request( diff --git a/src/api/project.rs b/src/api/project.rs index 0cfbf77..9a135f8 100644 --- a/src/api/project.rs +++ b/src/api/project.rs @@ -9,7 +9,7 @@ pub struct Project { pub description: String, pub identifier: String, pub hex_color: String, - pub parent_project_id: usize, + pub parent_project_id: isize, pub default_bucket_id: usize, pub done_bucket_id: usize, pub owner: Option, diff --git a/src/api/task.rs b/src/api/task.rs index 91d636a..52c64d4 100644 --- a/src/api/task.rs +++ b/src/api/task.rs @@ -91,36 +91,36 @@ impl Relation { pub fn repr(&self) -> String { match self { - Relation::Unknown => "Unknown", - Relation::Subtask => "Subtask", - Relation::ParentTask => "Parent Task", - Relation::Related => "Related", - Relation::DuplicateOf => "Duplicate of", - Relation::Duplicates => "Duplicates", - Relation::Blocking => "Blocking", - Relation::Blocked => "Blocked by", - Relation::Precedes => "Precedes", - Relation::Follows => "Follows", - Relation::CopiedFrom => "Copied from", - Relation::CopiedTo => "Copied to", + Self::Unknown => "Unknown", + Self::Subtask => "Subtask", + Self::ParentTask => "Parent Task", + Self::Related => "Related", + Self::DuplicateOf => "Duplicate of", + Self::Duplicates => "Duplicates", + Self::Blocking => "Blocking", + Self::Blocked => "Blocked by", + Self::Precedes => "Precedes", + Self::Follows => "Follows", + Self::CopiedFrom => "Copied from", + Self::CopiedTo => "Copied to", } .to_string() } pub fn api(&self) -> String { match self { - Relation::Unknown => "unknown", - Relation::Subtask => "subtask", - Relation::ParentTask => "parenttask", - Relation::Related => "related", - Relation::DuplicateOf => "duplicateof", - Relation::Duplicates => "duplicates", - Relation::Blocking => "blocking", - Relation::Blocked => "blocked", - Relation::Precedes => "precedes", - Relation::Follows => "follows", - Relation::CopiedFrom => "copiedfrom", - Relation::CopiedTo => "copiedto", + Self::Unknown => "unknown", + Self::Subtask => "subtask", + Self::ParentTask => "parenttask", + Self::Related => "related", + Self::DuplicateOf => "duplicateof", + Self::Duplicates => "duplicates", + Self::Blocking => "blocking", + Self::Blocked => "blocked", + Self::Precedes => "precedes", + Self::Follows => "follows", + Self::CopiedFrom => "copiedfrom", + Self::CopiedTo => "copiedto", } .to_string() } diff --git a/src/main.rs b/src/main.rs index 72555ca..cc5edf2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,14 +3,17 @@ mod args; mod config; mod ui; +use std::path::PathBuf; + use api::{ProjectID, Relation, VikunjaAPI}; +use clap::ArgMatches; +use once_cell::sync::Lazy; +use ui::{hex_to_color, print_color}; -// todo : error handling - -fn main() { - let arg = args::get_args(); - let config_path = dirs::home_dir().unwrap().join(".config").join("vk.toml"); +static CONFIG_PATH: Lazy = + Lazy::new(|| dirs::home_dir().unwrap().join(".config").join("vk.toml")); +fn login_cmd(arg: &ArgMatches) { if let Some(("login", login_arg)) = arg.subcommand() { let username: &String = login_arg.get_one("username").unwrap(); let password: &String = login_arg.get_one("password").unwrap(); @@ -25,14 +28,75 @@ fn main() { let api = VikunjaAPI::new(&host, ""); - let token = api.login(username, password, totp.map(|x| x.as_str())); + let token = api.login(username, password, totp.map(std::string::String::as_str)); let config = format!("host = \"{host}\"\ntoken = \"{token}\""); - std::fs::write(config_path, config).unwrap(); + std::fs::write(CONFIG_PATH.clone(), config).unwrap(); std::process::exit(0); } +} - let content = &std::fs::read_to_string(config_path).unwrap_or_else(|e| { +fn project_commands(arg: &ArgMatches, api: &VikunjaAPI) { + match arg.subcommand() { + Some(("add", add_prj_arg)) => { + let title: &String = add_prj_arg.get_one("title").unwrap(); + let description: Option<&String> = add_prj_arg.get_one("description"); + let color: Option<&String> = add_prj_arg.get_one("color"); + let parent: Option<&String> = add_prj_arg.get_one("parent"); + api.new_project( + title, + description.map(std::string::String::as_str), + color.map(std::string::String::as_str), + parent.map(|x| ProjectID::parse(api, x).unwrap()), + ); + } + Some(("rm", rm_prj_arg)) => { + let prj: &String = rm_prj_arg.get_one("project").unwrap(); + api.delete_project(&ProjectID::parse(api, prj).unwrap()); + } + _ => { + ui::project::list_projects(api); + } + } +} + +fn label_commands(arg: &ArgMatches, api: &VikunjaAPI) { + match arg.subcommand() { + Some(("rm", rm_label_arg)) => { + let title: &String = rm_label_arg.get_one("title").unwrap(); + + api.remove_label(title); + } + Some(("new", new_label_arg)) => { + let description: Option<&String> = new_label_arg.get_one("description"); + let color: Option<&String> = new_label_arg.get_one("color"); + let title: &String = new_label_arg.get_one("title").unwrap(); + + if let Some(color) = color { + if hex_to_color(color).is_err() { + print_color( + crossterm::style::Color::Red, + &format!("'{color}' is no hex color"), + ); + println!(); + std::process::exit(1); + } + } + + api.new_label( + title.as_str(), + description.map(std::string::String::as_str), + color.map(std::string::String::as_str), + ); + } + _ => { + ui::print_all_labels(api); + } + } +} + +fn load_config() -> config::Config { + let content = &std::fs::read_to_string(CONFIG_PATH.clone()).unwrap_or_else(|e| { ui::print_color( crossterm::style::Color::Red, &format!("Could not read config file: {e}"), @@ -41,7 +105,15 @@ fn main() { std::process::exit(1); }); - let config: config::Config = toml::from_str(content).unwrap(); + toml::from_str(content).unwrap() +} + +fn main() { + let arg = args::get_args(); + + login_cmd(&arg); + + let config = load_config(); let api = VikunjaAPI::new(&config.host, &config.token); match arg.subcommand() { @@ -49,30 +121,7 @@ fn main() { let task_id: &String = task_info_arg.get_one("task_id").unwrap(); ui::task::print_task_info(task_id.parse().unwrap(), &api); } - Some(("prj", prj_arg)) => match prj_arg.subcommand() { - Some(("ls", _)) => { - ui::project::list_projects(&api); - } - Some(("add", add_prj_arg)) => { - let title: &String = add_prj_arg.get_one("title").unwrap(); - let description: Option<&String> = add_prj_arg.get_one("description"); - let color: Option<&String> = add_prj_arg.get_one("color"); - let parent: Option<&String> = add_prj_arg.get_one("parent"); - api.new_project( - title, - description.map(|x| x.as_str()), - color.map(|x| x.as_str()), - parent.map(|x| ProjectID::parse(&api, x).unwrap()), - ); - } - Some(("rm", rm_prj_arg)) => { - let prj: &String = rm_prj_arg.get_one("project").unwrap(); - api.delete_project(ProjectID::parse(&api, prj).unwrap()); - } - _ => { - ui::project::list_projects(&api); - } - }, + Some(("prj", prj_arg)) => project_commands(prj_arg, &api), Some(("rm", rm_args)) => { let task_id: &String = rm_args.get_one("task_id").unwrap(); api.delete_task(task_id.parse().unwrap()); @@ -84,8 +133,9 @@ fn main() { if undo { api.remove_assign_to_task(user, task_id.parse().unwrap()); - } else { - api.assign_to_task(user, task_id.parse().unwrap()); + } else if let Err(msg) = api.assign_to_task(user, task_id.parse().unwrap()) { + print_color(crossterm::style::Color::Red, &msg); + println!(); } } Some(("comments", c_arg)) => { @@ -102,27 +152,7 @@ fn main() { api.new_comment(task_id.parse().unwrap(), comment); } - Some(("labels", label_args)) => match label_args.subcommand() { - Some(("rm", rm_label_arg)) => { - let title: &String = rm_label_arg.get_one("title").unwrap(); - - api.remove_label(title); - } - Some(("new", new_label_arg)) => { - let description: Option<&String> = new_label_arg.get_one("description"); - let color: Option<&String> = new_label_arg.get_one("color"); - let title: &String = new_label_arg.get_one("title").unwrap(); - - api.new_label( - title.as_str(), - description.map(|x| x.as_str()), - color.map(|x| x.as_str()), - ); - } - _ => { - ui::print_all_labels(&api); - } - }, + Some(("labels", label_args)) => label_commands(label_args, &api), Some(("label", label_args)) => { let label: &String = label_args.get_one("label").unwrap(); let task_id: &String = label_args.get_one("task_id").unwrap(); @@ -130,9 +160,12 @@ fn main() { if undo { api.label_task_remove(label, task_id.parse().unwrap()); - } else { - api.label_task(label, task_id.parse().unwrap()); + } else if let Err(msg) = api.label_task(label, task_id.parse().unwrap()) { + print_color(crossterm::style::Color::Red, &msg); + println!(); + std::process::exit(1); } + ui::task::print_task_info(task_id.parse().unwrap(), &api); } Some(("new", new_task_arg)) => { let title: &String = new_task_arg.get_one("title").unwrap(); @@ -160,18 +193,18 @@ fn main() { let sec_task_id: &String = rel_args.get_one("second_task_id").unwrap(); let delete = rel_args.get_flag("delete"); - let relation = Relation::try_parse(&relation).unwrap(); + let relation = Relation::try_parse(relation).unwrap(); if delete { api.remove_relation( task_id.parse().unwrap(), - relation, + &relation, sec_task_id.parse().unwrap(), ); } else { api.add_relation( task_id.parse().unwrap(), - relation, + &relation, sec_task_id.parse().unwrap(), ); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 2d1f923..32b7299 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -32,7 +32,7 @@ pub fn print_color_bg(color: Color, txt: &str) { } /// Convert a HEX Color String into a `Color` struct -fn hex_to_color(hex: &str) -> Result { +pub fn hex_to_color(hex: &str) -> Result { let hex = hex.trim_start_matches('#'); if hex.len() != 6 { @@ -78,15 +78,14 @@ pub fn time_relative(event: DateTime) -> String { }; if is_past { - format!("{} ago", time_string) + format!("{time_string} ago") } else { - format!("in {}", time_string) + format!("in {time_string}") } } fn is_in_past(dt: DateTime) -> bool { - let now = Utc::now(); - dt < now + dt < Utc::now() } fn print_label(label: &Label) { diff --git a/src/ui/project.rs b/src/ui/project.rs index 2b22899..d0c2d1e 100644 --- a/src/ui/project.rs +++ b/src/ui/project.rs @@ -10,7 +10,7 @@ use crate::{ pub fn list_projects(api: &VikunjaAPI) { let projects = api.get_all_projects(); - let mut project_map: HashMap> = HashMap::new(); + let mut project_map: HashMap> = HashMap::new(); for prj in projects { project_map @@ -28,7 +28,7 @@ pub fn list_projects(api: &VikunjaAPI) { print_color(color, &prj.title); println!(" [{}]", prj.id); - if let Some(sub_projects) = project_map.get(&(prj.id as usize)) { + if let Some(sub_projects) = project_map.get(&(prj.id)) { for sub_prj in sub_projects { let color = if sub_prj.hex_color.is_empty() { Color::Reset diff --git a/src/ui/task.rs b/src/ui/task.rs index aa10638..9e51667 100644 --- a/src/ui/task.rs +++ b/src/ui/task.rs @@ -88,18 +88,21 @@ pub fn print_current_tasks( } pub fn print_task_info(task_id: isize, api: &VikunjaAPI) { - let task = api.get_task(task_id); + let task = api.get_task(task_id).unwrap_or_else(|()| { + print_color( + crossterm::style::Color::Red, + &format!("Could not get task #{task_id}"), + ); + println!(); + std::process::exit(1); + }); if task.done { print_color( crossterm::style::Color::Green, &format!( "{} ✓ ", - if let Some(dt) = parse_datetime(&task.done_at) { - time_relative(dt) - } else { - String::new() - } + parse_datetime(&task.done_at).map_or_else(String::new, time_relative) ), ); } From 312aed18c809f628d00c9d34c13f49e61273cab1 Mon Sep 17 00:00:00 2001 From: JMARyA Date: Fri, 7 Jun 2024 13:14:28 +0200 Subject: [PATCH 06/10] new task add values --- src/api/mod.rs | 39 ++++++++++++++++++------- src/args.rs | 7 ++++- src/main.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 111 insertions(+), 14 deletions(-) diff --git a/src/api/mod.rs b/src/api/mod.rs index 70b1be9..a8c43e1 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -280,23 +280,40 @@ impl VikunjaAPI { self.delete_request(&format!("/tasks/{id}")); } - pub fn new_task(&self, title: &str, project: &ProjectID) -> Task { + pub fn new_task( + &self, + title: &str, + project: &ProjectID, + description: Option, + due_date: Option, + fav: bool, + label: Option, + priority: Option, + ) -> Result { let id = project.0; + let labels = if let Some(label) = label { + let label = self + .get_all_labels() + .into_iter() + .find(|x| x.title.trim() == label) + .map_or_else(|| Err(format!("Label '{label}' not found")), Ok)?; + vec![label] + } else { + vec![] + }; + let data = serde_json::json!({ - "title": title + "title": title, + "description": description, + "due_date": due_date, + "is_favorite": fav, + "priority": priority, + "labels": labels }); - // todo : - // description - // due_date - // end_date - // is_favorite - // labels - // priority - let resp = self.put_request(&format!("/projects/{id}/tasks"), &data); - serde_json::from_str(&resp).unwrap() + Ok(serde_json::from_str(&resp).unwrap()) } pub fn done_task(&self, task_id: isize, done: bool) -> Option { diff --git a/src/args.rs b/src/args.rs index c0694e0..5e37971 100644 --- a/src/args.rs +++ b/src/args.rs @@ -49,7 +49,12 @@ pub fn get_args() -> clap::ArgMatches { arg!(-p --project "Project to add task to") .required(false) .default_value("Inbox"), - ), + ) + .arg(arg!(-d --description "Task Description").required(false)) + .arg(arg!(--due "Task Due").required(false)) + .arg(arg!(-l --label