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"
|
||||
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"
|
||||
|
|
25
README.md
25
README.md
|
@ -45,15 +45,10 @@ vk rm 42
|
|||
|
||||
# Mark as done
|
||||
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
|
||||
vk assign me 42
|
||||
vk assign -u me 42 # Undo
|
||||
vk assign -u me 42 # You can undo this
|
||||
```
|
||||
|
||||
**Working with projects:**
|
||||
|
@ -83,21 +78,3 @@ vk labels new mylabel
|
|||
# Remove a label
|
||||
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;
|
||||
|
||||
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 VikunjaError {
|
||||
pub code: Option<isize>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl VikunjaError {}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Label {
|
||||
|
@ -68,7 +57,7 @@ impl ProjectID {
|
|||
Some(Self(
|
||||
api.get_all_projects()
|
||||
.into_iter()
|
||||
.find(|x| x.title.to_lowercase().contains(&project.to_lowercase()))?
|
||||
.find(|x| x.title.contains(project))?
|
||||
.id,
|
||||
))
|
||||
}
|
||||
|
@ -163,7 +152,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));
|
||||
}
|
||||
|
||||
|
@ -210,6 +199,7 @@ impl VikunjaAPI {
|
|||
"hex_color": color
|
||||
}),
|
||||
);
|
||||
|
||||
serde_json::from_str(&resp).unwrap()
|
||||
}
|
||||
|
||||
|
@ -237,13 +227,13 @@ impl VikunjaAPI {
|
|||
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 label_id = labels
|
||||
.into_iter()
|
||||
.find(|x| x.title.trim() == label)
|
||||
.map_or_else(|| Err(format!("Label '{label}' not found")), Ok)?
|
||||
.unwrap()
|
||||
.id;
|
||||
|
||||
self.put_request(
|
||||
|
@ -252,8 +242,6 @@ impl VikunjaAPI {
|
|||
"label_id": label_id
|
||||
}),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// tasks
|
||||
|
@ -271,52 +259,35 @@ impl VikunjaAPI {
|
|||
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}"));
|
||||
serde_json::from_str(&resp).map_or(Err(()), Ok)
|
||||
serde_json::from_str(&resp).unwrap()
|
||||
}
|
||||
|
||||
pub fn delete_task(&self, id: isize) {
|
||||
self.delete_request(&format!("/tasks/{id}"));
|
||||
}
|
||||
|
||||
pub fn new_task(
|
||||
&self,
|
||||
title: &str,
|
||||
project: &ProjectID,
|
||||
description: Option<String>,
|
||||
due_date: Option<String>,
|
||||
fav: bool,
|
||||
label: Option<String>,
|
||||
priority: Option<isize>,
|
||||
) -> Result<Task, String> {
|
||||
pub fn new_task(&self, title: &str, project: &ProjectID) -> Task {
|
||||
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,
|
||||
"description": description,
|
||||
"due_date": due_date,
|
||||
"is_favorite": fav,
|
||||
"priority": priority,
|
||||
"labels": labels
|
||||
"title": title
|
||||
});
|
||||
|
||||
// todo :
|
||||
// description
|
||||
// due_date
|
||||
// end_date
|
||||
// is_favorite
|
||||
// labels
|
||||
// priority
|
||||
|
||||
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(
|
||||
&format!("/tasks/{task_id}"),
|
||||
&serde_json::json!({
|
||||
|
@ -324,25 +295,13 @@ impl VikunjaAPI {
|
|||
"done_at": if done { Some(chrono::Utc::now().to_rfc3339()) } else { None }
|
||||
}),
|
||||
);
|
||||
serde_json::from_str(&resp).ok()
|
||||
}
|
||||
|
||||
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()
|
||||
serde_json::from_str(&resp).unwrap()
|
||||
}
|
||||
|
||||
pub fn login(&self, username: &str, password: &str, totp: Option<&str>) -> String {
|
||||
let resp = self.post_request(
|
||||
"/login",
|
||||
&serde_json::json!({
|
||||
"long_token": true,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"totp_passcode": totp
|
||||
|
@ -364,10 +323,8 @@ impl VikunjaAPI {
|
|||
serde_json::from_str(&resp).ok()
|
||||
}
|
||||
|
||||
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)?;
|
||||
pub fn assign_to_task(&self, user: &str, task_id: isize) {
|
||||
let user = self.search_user(user).unwrap();
|
||||
|
||||
self.put_request(
|
||||
&format!("/tasks/{task_id}/assignees"),
|
||||
|
@ -375,8 +332,6 @@ impl VikunjaAPI {
|
|||
"user_id": user.first().unwrap().id
|
||||
}),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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;
|
||||
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 identifier: String,
|
||||
pub hex_color: String,
|
||||
pub parent_project_id: isize,
|
||||
pub default_bucket_id: Option<usize>,
|
||||
pub done_bucket_id: Option<usize>,
|
||||
pub parent_project_id: usize,
|
||||
pub default_bucket_id: usize,
|
||||
pub done_bucket_id: usize,
|
||||
pub owner: Option<User>,
|
||||
pub is_archived: bool,
|
||||
pub background_information: Option<String>,
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Label, User};
|
||||
|
@ -25,7 +23,7 @@ pub struct Task {
|
|||
pub percent_done: f64,
|
||||
pub identifier: String,
|
||||
pub index: usize,
|
||||
pub related_tasks: Option<HashMap<String, Vec<Task>>>,
|
||||
// pub related_tasks
|
||||
// pub attachments
|
||||
pub cover_image_attachment_id: usize,
|
||||
pub is_favorite: bool,
|
||||
|
@ -33,95 +31,6 @@ pub struct Task {
|
|||
pub updated: String,
|
||||
pub bucket_id: usize,
|
||||
pub position: f64,
|
||||
pub kanban_position: Option<f64>,
|
||||
pub kanban_position: f64,
|
||||
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")
|
||||
.required(false)
|
||||
.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(
|
||||
command!()
|
||||
|
@ -73,35 +68,6 @@ 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("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(
|
||||
command!()
|
||||
.name("label")
|
||||
|
|
270
src/main.rs
270
src/main.rs
|
@ -3,17 +3,12 @@ mod args;
|
|||
mod config;
|
||||
mod ui;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use api::{ProjectID, VikunjaAPI};
|
||||
|
||||
use api::{ProjectID, Relation, VikunjaAPI};
|
||||
use clap::ArgMatches;
|
||||
use once_cell::sync::Lazy;
|
||||
use ui::{hex_to_color, print_color};
|
||||
fn main() {
|
||||
let arg = args::get_args();
|
||||
let config_path = dirs::home_dir().unwrap().join(".config").join("vk.toml");
|
||||
|
||||
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() {
|
||||
let username: &String = login_arg.get_one("username").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 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}\"");
|
||||
|
||||
std::fs::write(CONFIG_PATH.clone(), config).unwrap();
|
||||
std::fs::write(config_path, config).unwrap();
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
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| {
|
||||
let content = &std::fs::read_to_string(config_path).unwrap_or_else(|e| {
|
||||
ui::print_color(
|
||||
crossterm::style::Color::Red,
|
||||
&format!("Could not read config file: {e}"),
|
||||
|
@ -105,52 +39,7 @@ fn load_config() -> config::Config {
|
|||
std::process::exit(1);
|
||||
});
|
||||
|
||||
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 config: config::Config = toml::from_str(content).unwrap();
|
||||
let api = VikunjaAPI::new(&config.host, &config.token);
|
||||
|
||||
match arg.subcommand() {
|
||||
|
@ -158,7 +47,30 @@ 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)) => 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)) => {
|
||||
let task_id: &String = rm_args.get_one("task_id").unwrap();
|
||||
api.delete_task(task_id.parse().unwrap());
|
||||
|
@ -170,26 +82,32 @@ fn main() {
|
|||
|
||||
if undo {
|
||||
api.remove_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!();
|
||||
} else {
|
||||
api.assign_to_task(user, task_id.parse().unwrap());
|
||||
}
|
||||
}
|
||||
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(("ls", _)) => {
|
||||
ui::print_all_labels(&api);
|
||||
}
|
||||
}
|
||||
Some(("comment", comment_arg)) => {
|
||||
let task_id: &String = comment_arg.get_one("task_id").unwrap();
|
||||
let comment: &String = comment_arg.get_one("comment").unwrap();
|
||||
Some(("rm", rm_label_arg)) => {
|
||||
let title: &String = rm_label_arg.get_one("title").unwrap();
|
||||
|
||||
api.new_comment(task_id.parse().unwrap(), comment);
|
||||
}
|
||||
Some(("labels", label_args)) => label_commands(label_args, &api),
|
||||
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()),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Some(("label", label_args)) => {
|
||||
let label: &String = label_args.get_one("label").unwrap();
|
||||
let task_id: &String = label_args.get_one("task_id").unwrap();
|
||||
|
@ -197,57 +115,16 @@ fn main() {
|
|||
|
||||
if undo {
|
||||
api.label_task_remove(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);
|
||||
} else {
|
||||
api.label_task(label, task_id.parse().unwrap());
|
||||
}
|
||||
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();
|
||||
let project: &String = new_task_arg.get_one("project").unwrap();
|
||||
let project = ProjectID::parse(&api, project).unwrap();
|
||||
let description: Option<String> = new_task_arg
|
||||
.get_one::<String>("description")
|
||||
.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);
|
||||
}
|
||||
let task = api.new_task(title.as_str(), &project);
|
||||
ui::task::print_task_info(task.id, &api);
|
||||
}
|
||||
Some(("done", done_args)) => {
|
||||
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);
|
||||
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 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
|
||||
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('#');
|
||||
|
||||
if hex.len() != 6 {
|
||||
|
@ -78,14 +78,15 @@ pub fn time_relative(event: DateTime<Utc>) -> String {
|
|||
};
|
||||
|
||||
if is_past {
|
||||
format!("{time_string} ago")
|
||||
format!("{} ago", time_string)
|
||||
} else {
|
||||
format!("in {time_string}")
|
||||
format!("in {}", time_string)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_in_past(dt: DateTime<Utc>) -> bool {
|
||||
dt < Utc::now()
|
||||
let now = Utc::now();
|
||||
dt < now
|
||||
}
|
||||
|
||||
fn print_label(label: &Label) {
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::{
|
|||
pub fn list_projects(api: &VikunjaAPI) {
|
||||
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 {
|
||||
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)) {
|
||||
if let Some(sub_projects) = project_map.get(&(prj.id as usize)) {
|
||||
for sub_prj in sub_projects {
|
||||
let color = if sub_prj.hex_color.is_empty() {
|
||||
Color::Reset
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
api::{Comment, Project, ProjectID, Relation, Task, VikunjaAPI},
|
||||
api::{Project, ProjectID, Task, VikunjaAPI},
|
||||
ui::{
|
||||
format_html_to_terminal, hex_to_color, is_in_past, parse_datetime, print_color,
|
||||
print_label, time_relative,
|
||||
|
@ -88,21 +88,18 @@ pub fn print_current_tasks(
|
|||
}
|
||||
|
||||
pub fn print_task_info(task_id: isize, api: &VikunjaAPI) {
|
||||
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);
|
||||
});
|
||||
let task = api.get_task(task_id);
|
||||
|
||||
if task.done {
|
||||
print_color(
|
||||
crossterm::style::Color::Green,
|
||||
&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!();
|
||||
}
|
||||
|
||||
if task.description != "<p></p>" && !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 {
|
||||
|
@ -168,36 +169,5 @@ 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 {
|
||||
// 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 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