Compare commits

..

No commits in common. "a57b417c5ecb377ff582ac7e956fe7fd91338880" and "d038923726b12018aab84d1a7ce3a2c3177f275c" have entirely different histories.

11 changed files with 326 additions and 904 deletions

604
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,6 @@ crossterm = "0.27.0"
dirs = "5.0.1" dirs = "5.0.1"
html2text = "0.12.5" html2text = "0.12.5"
moka = { version = "0.12.7", features = ["sync"] } moka = { version = "0.12.7", features = ["sync"] }
once_cell = "1.19.0"
reqwest = { version = "0.12.4", features = ["blocking", "json"] } reqwest = { version = "0.12.4", features = ["blocking", "json"] }
serde = { version = "1.0.203", features = ["derive"] } serde = { version = "1.0.203", features = ["derive"] }
serde_json = "1.0.117" serde_json = "1.0.117"

View file

@ -45,15 +45,10 @@ vk rm 42
# Mark as done # Mark as done
vk done 42 vk done 42
vk done -u 42 # You can undo this
# Mark as favorite
vk fav 42
vk fav -u 42 # Undo
# Assign a user to a task # Assign a user to a task
vk assign me 42 vk assign me 42
vk assign -u me 42 # Undo vk assign -u me 42 # You can undo this
``` ```
**Working with projects:** **Working with projects:**
@ -83,21 +78,3 @@ vk labels new mylabel
# Remove a label # Remove a label
vk labels rm mylabel vk labels rm mylabel
``` ```
**Working with comments:**
```shell
# Show comments of task
vk comments 42
# Comment on a task
vk comment 42 "my comment"
```
**Relations:**
```shell
# Make Task #42 be a parent task to #7
vk relation 7 parent 42
# Make #42 blocked by #7
vk relation 42 blocked 7
```

View file

@ -4,20 +4,9 @@ mod project;
mod task; mod task;
pub use project::Project; pub use project::Project;
pub use task::Comment;
pub use task::Relation;
pub use task::Task; pub use task::Task;
use moka::sync::Cache; use moka::sync::Cache;
use task::TaskRelation;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VikunjaError {
pub code: Option<isize>,
pub message: String,
}
impl VikunjaError {}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Label { pub struct Label {
@ -68,7 +57,7 @@ impl ProjectID {
Some(Self( Some(Self(
api.get_all_projects() api.get_all_projects()
.into_iter() .into_iter()
.find(|x| x.title.to_lowercase().contains(&project.to_lowercase()))? .find(|x| x.title.contains(project))?
.id, .id,
)) ))
} }
@ -163,7 +152,7 @@ impl VikunjaAPI {
serde_json::from_str(&resp).unwrap() 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)); self.delete_request(&format!("/projects/{}", project_id.0));
} }
@ -210,6 +199,7 @@ impl VikunjaAPI {
"hex_color": color "hex_color": color
}), }),
); );
serde_json::from_str(&resp).unwrap() serde_json::from_str(&resp).unwrap()
} }
@ -237,13 +227,13 @@ impl VikunjaAPI {
self.delete_request(&format!("/tasks/{task_id}/labels/{label_id}")); self.delete_request(&format!("/tasks/{task_id}/labels/{label_id}"));
} }
pub fn label_task(&self, label: &str, task_id: isize) -> Result<(), String> { pub fn label_task(&self, label: &str, task_id: isize) {
let labels = self.get_all_labels(); let labels = self.get_all_labels();
let label_id = labels let label_id = labels
.into_iter() .into_iter()
.find(|x| x.title.trim() == label) .find(|x| x.title.trim() == label)
.map_or_else(|| Err(format!("Label '{label}' not found")), Ok)? .unwrap()
.id; .id;
self.put_request( self.put_request(
@ -252,8 +242,6 @@ impl VikunjaAPI {
"label_id": label_id "label_id": label_id
}), }),
); );
Ok(())
} }
// tasks // tasks
@ -271,52 +259,35 @@ impl VikunjaAPI {
serde_json::from_str(&resp).unwrap() serde_json::from_str(&resp).unwrap()
} }
pub fn get_task(&self, id: isize) -> Result<Task, ()> { pub fn get_task(&self, id: isize) -> Task {
let resp = self.get_request(&format!("/tasks/{id}")); let resp = self.get_request(&format!("/tasks/{id}"));
serde_json::from_str(&resp).map_or(Err(()), Ok) serde_json::from_str(&resp).unwrap()
} }
pub fn delete_task(&self, id: isize) { pub fn delete_task(&self, id: isize) {
self.delete_request(&format!("/tasks/{id}")); self.delete_request(&format!("/tasks/{id}"));
} }
pub fn new_task( pub fn new_task(&self, title: &str, project: &ProjectID) -> Task {
&self,
title: &str,
project: &ProjectID,
description: Option<String>,
due_date: Option<String>,
fav: bool,
label: Option<String>,
priority: Option<isize>,
) -> Result<Task, String> {
let id = project.0; 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!({ 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); let resp = self.put_request(&format!("/projects/{id}/tasks"), &data);
Ok(serde_json::from_str(&resp).unwrap()) serde_json::from_str(&resp).unwrap()
} }
pub fn done_task(&self, task_id: isize, done: bool) -> Option<Task> { pub fn done_task(&self, task_id: isize, done: bool) -> Task {
let resp = self.post_request( let resp = self.post_request(
&format!("/tasks/{task_id}"), &format!("/tasks/{task_id}"),
&serde_json::json!({ &serde_json::json!({
@ -324,25 +295,13 @@ impl VikunjaAPI {
"done_at": if done { Some(chrono::Utc::now().to_rfc3339()) } else { None } "done_at": if done { Some(chrono::Utc::now().to_rfc3339()) } else { None }
}), }),
); );
serde_json::from_str(&resp).ok() serde_json::from_str(&resp).unwrap()
}
pub fn fav_task(&self, task_id: isize, fav: bool) -> Option<Task> {
let resp = self.post_request(
&format!("/tasks/{task_id}"),
&serde_json::json!({
"is_favorite": fav
}),
);
serde_json::from_str(&resp).ok()
} }
pub fn login(&self, username: &str, password: &str, totp: Option<&str>) -> String { pub fn login(&self, username: &str, password: &str, totp: Option<&str>) -> String {
let resp = self.post_request( let resp = self.post_request(
"/login", "/login",
&serde_json::json!({ &serde_json::json!({
"long_token": true,
"username": username, "username": username,
"password": password, "password": password,
"totp_passcode": totp "totp_passcode": totp
@ -364,10 +323,8 @@ impl VikunjaAPI {
serde_json::from_str(&resp).ok() serde_json::from_str(&resp).ok()
} }
pub fn assign_to_task(&self, user: &str, task_id: isize) -> Result<(), String> { pub fn assign_to_task(&self, user: &str, task_id: isize) {
let user = self let user = self.search_user(user).unwrap();
.search_user(user)
.map_or_else(|| Err(String::from("User not found")), Ok)?;
self.put_request( self.put_request(
&format!("/tasks/{task_id}/assignees"), &format!("/tasks/{task_id}/assignees"),
@ -375,8 +332,6 @@ impl VikunjaAPI {
"user_id": user.first().unwrap().id "user_id": user.first().unwrap().id
}), }),
); );
Ok(())
} }
pub fn remove_assign_to_task(&self, user: &str, task_id: isize) { pub fn remove_assign_to_task(&self, user: &str, task_id: isize) {
@ -384,43 +339,4 @@ impl VikunjaAPI {
let user_id = user.first().unwrap().id; let user_id = user.first().unwrap().id;
self.delete_request(&format!("/tasks/{task_id}/assignees/{user_id}")); self.delete_request(&format!("/tasks/{task_id}/assignees/{user_id}"));
} }
pub fn get_task_comments(&self, task_id: isize) -> Vec<Comment> {
let resp = self.get_request(&format!("/tasks/{task_id}/comments"));
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"),
&serde_json::json!({
"comment": comment
}),
);
serde_json::from_str(&resp).unwrap()
}
} }

View file

@ -9,9 +9,9 @@ pub struct Project {
pub description: String, pub description: String,
pub identifier: String, pub identifier: String,
pub hex_color: String, pub hex_color: String,
pub parent_project_id: isize, pub parent_project_id: usize,
pub default_bucket_id: Option<usize>, pub default_bucket_id: usize,
pub done_bucket_id: Option<usize>, pub done_bucket_id: usize,
pub owner: Option<User>, pub owner: Option<User>,
pub is_archived: bool, pub is_archived: bool,
pub background_information: Option<String>, pub background_information: Option<String>,

View file

@ -1,5 +1,3 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{Label, User}; use super::{Label, User};
@ -25,7 +23,7 @@ pub struct Task {
pub percent_done: f64, pub percent_done: f64,
pub identifier: String, pub identifier: String,
pub index: usize, pub index: usize,
pub related_tasks: Option<HashMap<String, Vec<Task>>>, // pub related_tasks
// pub attachments // pub attachments
pub cover_image_attachment_id: usize, pub cover_image_attachment_id: usize,
pub is_favorite: bool, pub is_favorite: bool,
@ -33,95 +31,6 @@ pub struct Task {
pub updated: String, pub updated: String,
pub bucket_id: usize, pub bucket_id: usize,
pub position: f64, pub position: f64,
pub kanban_position: Option<f64>, pub kanban_position: f64,
pub created_by: Option<User>, pub created_by: Option<User>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Comment {
pub id: isize,
pub author: User,
pub comment: String,
pub created: String,
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<Self> {
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 {
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 {
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()
}
}

View file

@ -49,12 +49,7 @@ pub fn get_args() -> clap::ArgMatches {
arg!(-p --project <project> "Project to add task to") arg!(-p --project <project> "Project to add task to")
.required(false) .required(false)
.default_value("Inbox"), .default_value("Inbox"),
) ),
.arg(arg!(-d --description <description> "Task Description").required(false))
.arg(arg!(--due <due> "Task Due").required(false))
.arg(arg!(-l --label <label> "Task Label").required(false))
.arg(arg!(--priority <priority> "Task Label").required(false))
.arg(arg!(-f --favorite "Mark task as favorite").required(false)),
) )
.subcommand( .subcommand(
command!() command!()
@ -73,35 +68,6 @@ pub fn get_args() -> clap::ArgMatches {
.arg(arg!([user] "User").required(true)) .arg(arg!([user] "User").required(true))
.arg(arg!([task_id] "Task ID").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("comment")
.about("Comment on a task")
.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")
.about("Favorite a task")
.arg(arg!(-u --undo "Remove favorite from task").required(false))
.arg(arg!([task_id] "Task ID").required(true)),
)
.subcommand( .subcommand(
command!() command!()
.name("label") .name("label")

View file

@ -3,17 +3,12 @@ mod args;
mod config; mod config;
mod ui; mod ui;
use std::path::PathBuf; use api::{ProjectID, VikunjaAPI};
use api::{ProjectID, Relation, VikunjaAPI}; fn main() {
use clap::ArgMatches; let arg = args::get_args();
use once_cell::sync::Lazy; let config_path = dirs::home_dir().unwrap().join(".config").join("vk.toml");
use ui::{hex_to_color, print_color};
static CONFIG_PATH: Lazy<PathBuf> =
Lazy::new(|| dirs::home_dir().unwrap().join(".config").join("vk.toml"));
fn login_cmd(arg: &ArgMatches) {
if let Some(("login", login_arg)) = arg.subcommand() { if let Some(("login", login_arg)) = arg.subcommand() {
let username: &String = login_arg.get_one("username").unwrap(); let username: &String = login_arg.get_one("username").unwrap();
let password: &String = login_arg.get_one("password").unwrap(); let password: &String = login_arg.get_one("password").unwrap();
@ -28,75 +23,14 @@ fn login_cmd(arg: &ArgMatches) {
let api = VikunjaAPI::new(&host, ""); let api = VikunjaAPI::new(&host, "");
let token = api.login(username, password, totp.map(std::string::String::as_str)); let token = api.login(username, password, totp.map(|x| x.as_str()));
let config = format!("host = \"{host}\"\ntoken = \"{token}\""); let config = format!("host = \"{host}\"\ntoken = \"{token}\"");
std::fs::write(CONFIG_PATH.clone(), config).unwrap(); std::fs::write(config_path, config).unwrap();
std::process::exit(0); std::process::exit(0);
} }
}
fn project_commands(arg: &ArgMatches, api: &VikunjaAPI) { let content = &std::fs::read_to_string(config_path).unwrap_or_else(|e| {
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( ui::print_color(
crossterm::style::Color::Red, crossterm::style::Color::Red,
&format!("Could not read config file: {e}"), &format!("Could not read config file: {e}"),
@ -105,52 +39,7 @@ fn load_config() -> config::Config {
std::process::exit(1); std::process::exit(1);
}); });
toml::from_str(content).unwrap() let config: config::Config = toml::from_str(content).unwrap();
}
fn parse_datetime(input: &str) -> Option<chrono::DateTime<chrono::Utc>> {
let formats = [
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y-%m-%d",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%dT%H:%M:%S%.f",
"%Y-%m-%dT%H:%M:%S%.fZ",
"%Y-%m-%dT%H:%M:%S%:z",
"%Y-%m-%dT%H:%M:%S%z",
"%+%",
];
let input = input.trim();
for format in &formats {
if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(input, format) {
let naive_datetime = naive_date.and_hms_opt(0, 0, 0).unwrap();
return Some(chrono::TimeZone::from_utc_datetime(
&chrono::Utc,
&naive_datetime,
));
}
if let Ok(naive_datetime) = chrono::NaiveDateTime::parse_from_str(input, format) {
return Some(chrono::TimeZone::from_utc_datetime(
&chrono::Utc,
&naive_datetime,
));
}
if let Ok(datetime) = chrono::DateTime::parse_from_rfc3339(input) {
return Some(datetime.with_timezone(&chrono::Utc));
}
}
None
}
fn main() {
let arg = args::get_args();
login_cmd(&arg);
let config = load_config();
let api = VikunjaAPI::new(&config.host, &config.token); let api = VikunjaAPI::new(&config.host, &config.token);
match arg.subcommand() { match arg.subcommand() {
@ -158,7 +47,30 @@ fn main() {
let task_id: &String = task_info_arg.get_one("task_id").unwrap(); let task_id: &String = task_info_arg.get_one("task_id").unwrap();
ui::task::print_task_info(task_id.parse().unwrap(), &api); ui::task::print_task_info(task_id.parse().unwrap(), &api);
} }
Some(("prj", prj_arg)) => project_commands(prj_arg, &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(("rm", rm_args)) => { Some(("rm", rm_args)) => {
let task_id: &String = rm_args.get_one("task_id").unwrap(); let task_id: &String = rm_args.get_one("task_id").unwrap();
api.delete_task(task_id.parse().unwrap()); api.delete_task(task_id.parse().unwrap());
@ -170,26 +82,32 @@ fn main() {
if undo { if undo {
api.remove_assign_to_task(user, task_id.parse().unwrap()); api.remove_assign_to_task(user, task_id.parse().unwrap());
} else if let Err(msg) = api.assign_to_task(user, task_id.parse().unwrap()) { } else {
print_color(crossterm::style::Color::Red, &msg); api.assign_to_task(user, task_id.parse().unwrap());
println!();
} }
} }
Some(("comments", c_arg)) => { Some(("labels", label_args)) => match label_args.subcommand() {
let task_id: &String = c_arg.get_one("task_id").unwrap(); Some(("ls", _)) => {
let comments = api.get_task_comments(task_id.parse().unwrap()); ui::print_all_labels(&api);
for comment in comments {
ui::task::print_comment(&comment);
} }
} Some(("rm", rm_label_arg)) => {
Some(("comment", comment_arg)) => { let title: &String = rm_label_arg.get_one("title").unwrap();
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); api.remove_label(title);
} }
Some(("labels", label_args)) => label_commands(label_args, &api), 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()),
);
}
_ => {}
},
Some(("label", label_args)) => { Some(("label", label_args)) => {
let label: &String = label_args.get_one("label").unwrap(); let label: &String = label_args.get_one("label").unwrap();
let task_id: &String = label_args.get_one("task_id").unwrap(); let task_id: &String = label_args.get_one("task_id").unwrap();
@ -197,57 +115,16 @@ fn main() {
if undo { if undo {
api.label_task_remove(label, task_id.parse().unwrap()); api.label_task_remove(label, task_id.parse().unwrap());
} else if let Err(msg) = api.label_task(label, task_id.parse().unwrap()) { } else {
print_color(crossterm::style::Color::Red, &msg); api.label_task(label, task_id.parse().unwrap());
println!();
std::process::exit(1);
} }
ui::task::print_task_info(task_id.parse().unwrap(), &api);
} }
Some(("new", new_task_arg)) => { Some(("new", new_task_arg)) => {
let title: &String = new_task_arg.get_one("title").unwrap(); let title: &String = new_task_arg.get_one("title").unwrap();
let project: &String = new_task_arg.get_one("project").unwrap(); let project: &String = new_task_arg.get_one("project").unwrap();
let project = ProjectID::parse(&api, project).unwrap(); let project = ProjectID::parse(&api, project).unwrap();
let description: Option<String> = new_task_arg let task = api.new_task(title.as_str(), &project);
.get_one::<String>("description") ui::task::print_task_info(task.id, &api);
.map(std::borrow::ToOwned::to_owned);
let due_date: Option<String> = new_task_arg
.get_one::<String>("due")
.map(std::borrow::ToOwned::to_owned);
let due_date = due_date.map(|x| {
if let Some(parsed) = parse_datetime(&x) {
parsed.to_rfc3339()
} else {
print_color(crossterm::style::Color::Red, "Failed to parse due date");
println!();
std::process::exit(1);
}
});
let label: Option<String> = new_task_arg
.get_one::<String>("label")
.map(std::borrow::ToOwned::to_owned);
let priority: Option<String> = new_task_arg
.get_one::<String>("priority")
.map(std::borrow::ToOwned::to_owned);
let fav = new_task_arg.get_flag("favorite");
// todo : add args
let task = api.new_task(
title.as_str(),
&project,
description,
due_date,
fav,
label,
priority.map(|x| x.parse().unwrap()),
);
if let Err(msg) = task {
print_color(crossterm::style::Color::Red, &msg);
println!();
std::process::exit(1);
} else {
ui::task::print_task_info(task.unwrap().id, &api);
}
} }
Some(("done", done_args)) => { Some(("done", done_args)) => {
let task_id: &String = done_args.get_one("task_id").unwrap(); let task_id: &String = done_args.get_one("task_id").unwrap();
@ -255,37 +132,6 @@ fn main() {
api.done_task(task_id.parse().unwrap(), done); api.done_task(task_id.parse().unwrap(), done);
ui::task::print_task_info(task_id.parse().unwrap(), &api); 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);
}
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 done = arg.get_flag("done");
let fav = arg.get_flag("favorite"); let fav = arg.get_flag("favorite");

View file

@ -32,7 +32,7 @@ pub fn print_color_bg(color: Color, txt: &str) {
} }
/// Convert a HEX Color String into a `Color` struct /// Convert a HEX Color String into a `Color` struct
pub fn hex_to_color(hex: &str) -> Result<Color, String> { fn hex_to_color(hex: &str) -> Result<Color, String> {
let hex = hex.trim_start_matches('#'); let hex = hex.trim_start_matches('#');
if hex.len() != 6 { if hex.len() != 6 {
@ -78,14 +78,15 @@ pub fn time_relative(event: DateTime<Utc>) -> String {
}; };
if is_past { if is_past {
format!("{time_string} ago") format!("{} ago", time_string)
} else { } else {
format!("in {time_string}") format!("in {}", time_string)
} }
} }
fn is_in_past(dt: DateTime<Utc>) -> bool { fn is_in_past(dt: DateTime<Utc>) -> bool {
dt < Utc::now() let now = Utc::now();
dt < now
} }
fn print_label(label: &Label) { fn print_label(label: &Label) {

View file

@ -10,7 +10,7 @@ use crate::{
pub fn list_projects(api: &VikunjaAPI) { pub fn list_projects(api: &VikunjaAPI) {
let projects = api.get_all_projects(); let projects = api.get_all_projects();
let mut project_map: HashMap<isize, Vec<Project>> = HashMap::new(); let mut project_map: HashMap<usize, Vec<Project>> = HashMap::new();
for prj in projects { for prj in projects {
project_map project_map
@ -28,7 +28,7 @@ pub fn list_projects(api: &VikunjaAPI) {
print_color(color, &prj.title); print_color(color, &prj.title);
println!(" [{}]", prj.id); println!(" [{}]", prj.id);
if let Some(sub_projects) = project_map.get(&(prj.id)) { if let Some(sub_projects) = project_map.get(&(prj.id as usize)) {
for sub_prj in sub_projects { for sub_prj in sub_projects {
let color = if sub_prj.hex_color.is_empty() { let color = if sub_prj.hex_color.is_empty() {
Color::Reset Color::Reset

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
api::{Comment, Project, ProjectID, Relation, Task, VikunjaAPI}, api::{Project, ProjectID, Task, VikunjaAPI},
ui::{ ui::{
format_html_to_terminal, hex_to_color, is_in_past, parse_datetime, print_color, format_html_to_terminal, hex_to_color, is_in_past, parse_datetime, print_color,
print_label, time_relative, print_label, time_relative,
@ -88,21 +88,18 @@ pub fn print_current_tasks(
} }
pub fn print_task_info(task_id: isize, api: &VikunjaAPI) { pub fn print_task_info(task_id: isize, api: &VikunjaAPI) {
let task = api.get_task(task_id).unwrap_or_else(|()| { let task = api.get_task(task_id);
print_color(
crossterm::style::Color::Red,
&format!("Could not get task #{task_id}"),
);
println!();
std::process::exit(1);
});
if task.done { if task.done {
print_color( print_color(
crossterm::style::Color::Green, crossterm::style::Color::Green,
&format!( &format!(
"{} ✓ ", "{} ✓ ",
parse_datetime(&task.done_at).map_or_else(String::new, time_relative) if let Some(dt) = parse_datetime(&task.done_at) {
time_relative(dt)
} else {
String::new()
}
), ),
); );
} }
@ -160,6 +157,10 @@ pub fn print_task_info(task_id: isize, api: &VikunjaAPI) {
println!(); println!();
} }
if task.description != "<p></p>" && !task.description.is_empty() {
println!("---\n{}", format_html_to_terminal(&task.description));
}
if let Some(assigned) = task.assignees { if let Some(assigned) = task.assignees {
print!("Assigned to: "); print!("Assigned to: ");
for assignee in assigned { for assignee in assigned {
@ -168,36 +169,5 @@ pub fn print_task_info(task_id: isize, api: &VikunjaAPI) {
println!(); 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 {
// todo : add done indication
print_color(crossterm::style::Color::Blue, &t.title);
print_color(crossterm::style::Color::Yellow, &format!(" ({})", t.id));
print!(" ");
}
println!();
}
}
if task.description != "<p></p>" && !task.description.is_empty() {
println!("---\n{}", format_html_to_terminal(&task.description));
}
// pub percent_done: f64, // 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!();
}