Compare commits
No commits in common. "a57b417c5ecb377ff582ac7e956fe7fd91338880" and "d038923726b12018aab84d1a7ce3a2c3177f275c" have entirely different histories.
a57b417c5e
...
d038923726
11 changed files with 326 additions and 904 deletions
604
Cargo.lock
generated
604
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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"
|
||||||
|
|
25
README.md
25
README.md
|
@ -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
|
|
||||||
```
|
|
||||||
|
|
128
src/api/mod.rs
128
src/api/mod.rs
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
36
src/args.rs
36
src/args.rs
|
@ -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")
|
||||||
|
|
266
src/main.rs
266
src/main.rs
|
@ -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);
|
||||||
|
}
|
||||||
|
Some(("rm", rm_label_arg)) => {
|
||||||
|
let title: &String = rm_label_arg.get_one("title").unwrap();
|
||||||
|
|
||||||
for comment in comments {
|
api.remove_label(title);
|
||||||
ui::task::print_comment(&comment);
|
|
||||||
}
|
}
|
||||||
}
|
Some(("new", new_label_arg)) => {
|
||||||
Some(("comment", comment_arg)) => {
|
let description: Option<&String> = new_label_arg.get_one("description");
|
||||||
let task_id: &String = comment_arg.get_one("task_id").unwrap();
|
let color: Option<&String> = new_label_arg.get_one("color");
|
||||||
let comment: &String = comment_arg.get_one("comment").unwrap();
|
let title: &String = new_label_arg.get_one("title").unwrap();
|
||||||
|
|
||||||
api.new_comment(task_id.parse().unwrap(), comment);
|
api.new_label(
|
||||||
|
title.as_str(),
|
||||||
|
description.map(|x| x.as_str()),
|
||||||
|
color.map(|x| x.as_str()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Some(("labels", label_args)) => label_commands(label_args, &api),
|
_ => {}
|
||||||
|
},
|
||||||
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");
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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!();
|
|
||||||
}
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue