From 7796f313effd0191457608ddf08986d0ce54806b Mon Sep 17 00:00:00 2001 From: ghassmo Date: Sun, 8 May 2022 03:10:22 +0300 Subject: [PATCH] tau: rework on tau-cli functionalities and design --- bin/tau/README.md | 214 ++++++-------------- bin/tau/tau-cli/src/cli.rs | 112 +++++++++++ bin/tau/tau-cli/src/filter.rs | 71 +++++++ bin/tau/tau-cli/src/jsonrpc.rs | 14 +- bin/tau/tau-cli/src/main.rs | 120 ++++-------- bin/tau/tau-cli/src/primitives.rs | 63 ++++++ bin/tau/tau-cli/src/util.rs | 312 +----------------------------- bin/tau/tau-cli/src/view.rs | 125 ++++++++++++ bin/tau/taud/src/jsonrpc.rs | 127 ++++++------ 9 files changed, 539 insertions(+), 619 deletions(-) create mode 100644 bin/tau/tau-cli/src/cli.rs create mode 100644 bin/tau/tau-cli/src/filter.rs create mode 100644 bin/tau/tau-cli/src/primitives.rs create mode 100644 bin/tau/tau-cli/src/view.rs diff --git a/bin/tau/README.md b/bin/tau/README.md index 1105f37bc..0287f4768 100644 --- a/bin/tau/README.md +++ b/bin/tau/README.md @@ -32,16 +32,6 @@ seed node shouldn't be advertised in the list of connectable nodes. The seed node does not participate as a normal node in the p2p network. It simply allows new nodes to discover other nodes in the network during the bootstrapping phase. -Also note that for the first time ever running seed node you must run it with -`--key-gen`: -```shell -% taud --key-gen -``` -This will generate a new secret key in `/home/${USER}/.config/tau/secret_key` that -you can share with nodes you want them to get and decrypt your tasks, otherwise if you -have already generated or got a copy from a peer place it in the same directory -`/home/${USER}/.config/tau/secret_key`. - ### Inbound Node This is a node accepting inbound connections on the network but which is not @@ -74,175 +64,81 @@ connect to in the p2p network. ## Seed nodes to connect to seeds=["127.0.0.1:11001"] + +Also note that for the first time ever running seed node you must run it with +`--key-gen`: +```shell +% taud --key-gen +``` +This will generate a new secret key in `/home/${USER}/.config/tau/secret_key` that +you can share with nodes you want them to get and decrypt your tasks, otherwise if you +have already generated or got a copy from a peer place it in the same directory +`/home/${USER}/.config/tau/secret_key`. + + ## Usage (CLI) ```shell % tau --help ``` - tau 0.3.0 Tau cli - + USAGE: - tau [FLAGS] [OPTIONS] [ARGS] [SUBCOMMAND] - + tau [FLAGS] [OPTIONS] [ARGS] [SUBCOMMAND] + FLAGS: - -h, --help Prints help information - -V, --version Prints version information - -v Increase verbosity - + -h, --help Prints help information + -V, --version Prints version information + -v Increase verbosity + OPTIONS: - -c, --config Sets a custom config file - --rpc JSON-RPC listen URL [default: 127.0.0.1:11055] - + -c, --config Sets a custom config file + --rpc JSON-RPC listen URL [default: 127.0.0.1:11055] + ARGS: - Get task by ID - ... Search criteria (zero or more) - + Get task by ID + ... Search criteria (zero or more) + SUBCOMMANDS: - add Add a new task - comment Set or Get comment for a task - help Prints this message or the help of the given subcommand(s) - list List all tasks - state Set or Get task state - update Update/Edit an existing task by ID + add Add a new task + comment Set or Get comment for a task + help Prints this message or the help of the given subcommand(s) + list List all tasks + state Set or Get task state + update Update/Edit an existing task by ID ```shell % tau help [SUBCOMMAND] ``` -### Add new tasks +### Example ```shell -% tau add title1 description person1,person2 project1,project2 0405 4.74 -% tau add title2 "some description" person1 project1 0805 18 -% # this will prompt terminal for title -% tau add -Title: new title -% # then your system's default editor will open up and you could write some description -% # you should have/add environment variable EDITOR pointing to your favorite text editor -``` -for more information: -```shell -% tau add --help +$ # add new task +$ tau add "new title" +$ tau add "new title" project:blockchain desc:"new description" rank:3 assign:dark +$ +$ # lists tasks +$ tau +$ tau open # open tasks +$ tau pause # paused tasks +$ tau 0522 # created at May 2022 +$ tau project:blockchain assign:dark +$ tau rank:gt:n # lists all tasks that have rank greater than n +$ tau rank:ls:n # lists all tasks that have rank lesser than n +$ +$ # update task +$ tau update 3 project:network rank:20 +$ +$ # state +$ tau state 3 # get state +$ tau state 3 pause # set the state to pause +$ +$ # comments +$ tau comments 1 # list comments +$ tau comments 3 "new comment" # add new comment ``` -### List existing tasks -```shell -% tau list # or just tau -``` -Output: -```text - ID | Title | Project | Assigned | Due | Rank -----+-----------+-------------------+-----------------+-----------------+------ - 2 | title2 | project1 | person1 | Sunday 8 May | 18 - 1 | title1 | project1,project2 | person1,person2 | Wednesday 4 May | 4.74 - 3 | new title | | | | 0 -``` - - -### List tasks with filters - -```shell -% tau # lists all tasks -% tau open # lists currently open tasks -% tau pause # lists currently paused tasks -% tau 0522 # lists tasks created at May 2022 -% tau project:value # lists all tasks that have "value" in their Project -% tau assign:value # lists all tasks that have "value" in their Assign -% tau "rank>number" # lists all tasks that have rank greater than "number" -% tau "rank, + #[structopt(subcommand)] + pub command: Option, + /// Get task by ID + pub id: Option, + /// Search criteria (zero or more) + #[structopt(multiple = true)] + pub filters: Vec, +} + +#[derive(StructOpt, Deserialize, Debug)] +pub enum CliTauSubCommands { + /// Add a new task + Add { values: Vec }, + /// Update/Edit an existing task by ID + Update { + /// Task ID + id: u64, + /// Values (ex: project:blockchain) + values: Vec, + }, + /// Set or Get task state + State { + /// Task ID + id: u64, + /// Set task state + state: Option, + }, + /// Set or Get comment for a task + Comment { + /// Task ID + id: u64, + /// Comment content + content: Option, + }, + /// List all tasks + List {}, +} + +#[derive(Serialize, Debug, Clone)] +pub struct CliBaseTask { + pub title: String, + pub desc: Option, + pub assign: Vec, + pub project: Vec, + pub due: Option, + pub rank: Option, +} + +pub fn task_from_cli_values(values: Vec) -> Result { + let mut title: String = String::new(); + let mut desc: Option = None; + let mut project: Vec = vec![]; + let mut assign: Vec = vec![]; + let mut due: Option = None; + let mut rank: Option = None; + + for val in values { + let field: Vec<&str> = val.split(':').collect(); + if field.len() == 1 { + title = field[0].into(); + continue + } + + if field.len() != 2 { + continue + } + + if field[0].starts_with("project") { + project.push(field[1].into()); + } + if field[0].starts_with("desc") { + desc = Some(field[1].into()); + } + if field[0].starts_with("assign") { + assign.push(field[1].into()); + } + if field[0].starts_with("due") { + due = due_as_timestamp(&field[1]) + } + if field[0].starts_with("rank") { + rank = Some(field[1].parse::()?); + } + } + + let task = CliBaseTask { title, desc, project, assign, due, rank }; + + Ok(task) +} diff --git a/bin/tau/tau-cli/src/filter.rs b/bin/tau/tau-cli/src/filter.rs new file mode 100644 index 000000000..2c4d19d71 --- /dev/null +++ b/bin/tau/tau-cli/src/filter.rs @@ -0,0 +1,71 @@ +use chrono::{Datelike, NaiveDate, NaiveDateTime}; +use serde_json::Value; + +use super::primitives::{TaskEvent, TaskInfo}; + +// Helper function to check task's state +fn check_task_state(task: &TaskInfo, state: &str) -> bool { + let last_state = task.events.last().unwrap_or(&TaskEvent::default()).action.clone(); + state == last_state +} + +pub fn apply_filter(tasks: &mut Vec, filter: String) { + match filter.as_str() { + "open" => tasks.retain(|task| check_task_state(task, "open")), + "pause" => tasks.retain(|task| check_task_state(task, "pause")), + + _ if filter.len() == 4 && filter.parse::().is_ok() => { + let (month, year) = + (filter[..2].parse::().unwrap(), filter[2..].parse::().unwrap()); + + let year = year + 2000; + + tasks.retain(|task| { + let date = task.created_at; + let task_date = NaiveDateTime::from_timestamp(date, 0).date(); + let filter_date = NaiveDate::from_ymd(year, month, 1); + task_date.month() == filter_date.month() && task_date.year() == filter_date.year() + }) + } + + _ if filter.contains("assign:") => { + let kv: Vec<&str> = filter.split(':').collect(); + if kv.len() == 2 { + let value = Value::from(kv[1]); + if value.as_str().is_some() { + tasks.retain(|task| task.assign.contains(&value.as_str().unwrap().into())) + } + } + } + + _ if filter.contains("project:") => { + let kv: Vec<&str> = filter.split(':').collect(); + if kv.len() == 2 { + let value = Value::from(kv[1]); + if value.as_str().is_some() { + tasks.retain(|task| task.project.contains(&value.as_str().unwrap().into())) + } + } + } + + _ if filter.contains("rank:") => { + let kv: Vec<&str> = filter.split(':').collect(); + + if kv.len() == 3 { + let value = kv[2].parse::().unwrap_or(0.0); + + tasks.retain(|task| { + if filter.contains("lt") { + task.rank < value + } else if filter.contains("gt") { + task.rank > value + } else { + true + } + }) + } + } + + _ => {} + }; +} diff --git a/bin/tau/tau-cli/src/jsonrpc.rs b/bin/tau/tau-cli/src/jsonrpc.rs index 8f23d5096..820baf9aa 100644 --- a/bin/tau/tau-cli/src/jsonrpc.rs +++ b/bin/tau/tau-cli/src/jsonrpc.rs @@ -75,14 +75,6 @@ pub async fn set_state(url: &str, id: u64, state: &str) -> Result { request(req, url.to_string()).await } -// Get task's state. -// --> {"jsonrpc": "2.0", "method": "get_state", "params": [task_id], "id": 1} -// <-- {"jsonrpc": "2.0", "result": "state", "id": 1} -pub async fn get_state(url: &str, id: u64) -> Result { - let req = jsonrpc::request(json!("get_state"), json!([id])); - request(req, url.to_string()).await -} - // Set comment for a task and returns `true` upon success. // --> {"jsonrpc": "2.0", "method": "set_comment", "params": [task_id, comment_content], "id": 1} // <-- {"jsonrpc": "2.0", "result": true, "id": 1} @@ -92,9 +84,9 @@ pub async fn set_comment(url: &str, id: u64, content: &str) -> Result { } // Get task by id. -// --> {"jsonrpc": "2.0", "method": "get_by_id", "params": [task_id], "id": 1} +// --> {"jsonrpc": "2.0", "method": "get_task_by_id", "params": [task_id], "id": 1} // <-- {"jsonrpc": "2.0", "result": "task", "id": 1} -pub async fn get_by_id(url: &str, id: u64) -> Result { - let req = jsonrpc::request(json!("get_by_id"), json!([id])); +pub async fn get_task_by_id(url: &str, id: u64) -> Result { + let req = jsonrpc::request(json!("get_task_by_id"), json!([id])); request(req, url.to_string()).await } diff --git a/bin/tau/tau-cli/src/main.rs b/bin/tau/tau-cli/src/main.rs index 49ada93c0..dc5a34b56 100644 --- a/bin/tau/tau-cli/src/main.rs +++ b/bin/tau/tau-cli/src/main.rs @@ -1,6 +1,7 @@ use log::error; -use serde_json::{json, Value}; +use serde_json::json; use simplelog::{ColorChoice, TermLogger, TerminalMode}; +use structopt_toml::StructOptToml; use darkfi::{ util::{ @@ -9,29 +10,29 @@ use darkfi::{ }, Result, }; -use structopt_toml::StructOptToml; +mod cli; +mod filter; mod jsonrpc; +mod primitives; mod util; +mod view; -use crate::{ - jsonrpc::{add, get_by_id, get_state, list, set_comment, set_state, update}, - util::{ - desc_in_editor, due_as_timestamp, get_comments, list_tasks, set_title, show_task, CliTau, - CliTauSubCommands, TaskInfo, CONFIG_FILE, CONFIG_FILE_CONTENTS, - }, -}; +use cli::CliTauSubCommands; +use primitives::TaskInfo; +use util::{desc_in_editor, CONFIG_FILE, CONFIG_FILE_CONTENTS}; +use view::{comments_as_string, events_as_string, print_list_of_task, print_task_info}; -async fn start(mut options: CliTau) -> Result<()> { +async fn start(mut options: cli::CliTau) -> Result<()> { let rpc_addr = &format!("tcp://{}", &options.rpc_listen.clone()); + let states: Vec = vec!["stop".into(), "open".into(), "pause".into()]; + match options.id { Some(id) if id.len() < 4 && id.parse::().is_ok() => { - let task = get_by_id(rpc_addr, id.parse::().unwrap()).await?; + let task = jsonrpc::get_task_by_id(rpc_addr, id.parse::().unwrap()).await?; let taskinfo: TaskInfo = serde_json::from_value(task.clone())?; - let current_state: String = - serde_json::from_value(get_state(rpc_addr, id.parse::().unwrap()).await?)?; - show_task(task, taskinfo, current_state)?; + print_task_info(taskinfo)?; return Ok(()) } Some(id) => options.filters.push(id), @@ -39,93 +40,58 @@ async fn start(mut options: CliTau) -> Result<()> { } match options.command { - Some(CliTauSubCommands::Add { title, desc, assign, project, due, rank }) => { - let title = match title { - Some(t) => t, - None => set_title()?, + Some(CliTauSubCommands::Add { values }) => { + let mut task = cli::task_from_cli_values(values)?; + if task.title.is_empty() { + error!("Provide a title for the task"); + return Ok(()) }; - let desc = match desc { - Some(d) => Some(d), - None => desc_in_editor()?, + if task.desc.is_none() { + task.desc = desc_in_editor()?; }; - let assign: Vec = match assign { - Some(a) => a.split(',').map(|s| s.into()).collect(), - None => vec![], - }; - - let project: Vec = match project { - Some(p) => p.split(',').map(|s| s.into()).collect(), - None => vec![], - }; - - let due = match due { - Some(d) => due_as_timestamp(&d), - None => None, - }; - - let rank = rank.unwrap_or(0.0); - - add(rpc_addr, - json!([{"title": title, "desc": desc, "assign": assign, "project": project, "due": due, "rank": rank}]), - ).await?; + jsonrpc::add(rpc_addr, json!([task])).await?; } - Some(CliTauSubCommands::Update { id, key, value }) => { - let value = value.as_str().trim(); - - let updated_value: Value = match key.as_str() { - "due" => { - json!(due_as_timestamp(value)) - } - "rank" => { - json!(value.parse::()?) - } - "project" | "assign" => { - json!(value.split(',').collect::>()) - } - _ => { - json!(value) - } - }; - - update(rpc_addr, id, json!({ key: updated_value })).await?; + Some(CliTauSubCommands::Update { id, values }) => { + let task = cli::task_from_cli_values(values)?; + jsonrpc::update(rpc_addr, id, json!([task])).await?; } Some(CliTauSubCommands::State { id, state }) => match state { Some(state) => { - if state.as_str() == "open" { - set_state(rpc_addr, id, state.trim()).await?; - } else if state.as_str() == "pause" { - set_state(rpc_addr, id, state.trim()).await?; - } else if state.as_str() == "stop" { - set_state(rpc_addr, id, state.trim()).await?; + let state = state.trim().to_lowercase(); + if states.contains(&state) { + jsonrpc::set_state(rpc_addr, id, &state).await?; } else { error!("Task state could only be one of three states: open, pause or stop"); } } None => { - let state = get_state(rpc_addr, id).await?; - println!("Task with id {} is: {}", id, state); + let task = jsonrpc::get_task_by_id(rpc_addr, id).await?; + let taskinfo: TaskInfo = serde_json::from_value(task.clone())?; + let state = events_as_string(taskinfo.events); + println!("Task {}: {}", id, state); } }, Some(CliTauSubCommands::Comment { id, content }) => match content { Some(content) => { - set_comment(rpc_addr, id, content.trim()).await?; + jsonrpc::set_comment(rpc_addr, id, content.trim()).await?; } None => { - let rep = get_by_id(rpc_addr, id).await?; - let comments = get_comments(rep)?; - - println!("Comments on Task with id {}:\n{}", id, comments); + let task = jsonrpc::get_task_by_id(rpc_addr, id).await?; + let taskinfo: TaskInfo = serde_json::from_value(task.clone())?; + let comments = comments_as_string(taskinfo.comments); + println!("Comments {}:\n{}", id, comments); } }, Some(CliTauSubCommands::List {}) | None => { - let rep = list(rpc_addr, json!([])).await?; - list_tasks(rep, options.filters)?; + let tasks = jsonrpc::list(rpc_addr, json!([])).await?; + let mut tasks: Vec = serde_json::from_value(tasks)?; + print_list_of_task(&mut tasks, options.filters)?; } } @@ -134,10 +100,10 @@ async fn start(mut options: CliTau) -> Result<()> { #[async_std::main] async fn main() -> Result<()> { - let args = CliTau::from_args_with_toml("").unwrap(); + let args = cli::CliTau::from_args_with_toml("").unwrap(); let cfg_path = get_config_path(args.config, CONFIG_FILE)?; spawn_config(&cfg_path, CONFIG_FILE_CONTENTS.as_bytes())?; - let args = CliTau::from_args_with_toml(&std::fs::read_to_string(cfg_path)?).unwrap(); + let args = cli::CliTau::from_args_with_toml(&std::fs::read_to_string(cfg_path)?).unwrap(); let (lvl, conf) = log_config(args.verbose.into())?; TermLogger::init(lvl, conf, TerminalMode::Mixed, ColorChoice::Auto)?; diff --git a/bin/tau/tau-cli/src/primitives.rs b/bin/tau/tau-cli/src/primitives.rs new file mode 100644 index 000000000..c9c6ba663 --- /dev/null +++ b/bin/tau/tau-cli/src/primitives.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; + +use super::util::timestamp_to_date; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TaskInfo { + pub ref_id: String, + pub id: u32, + pub title: String, + pub desc: String, + pub owner: String, + pub assign: Vec, + pub project: Vec, + pub due: Option, + pub rank: f32, + pub created_at: i64, + pub events: Vec, + pub comments: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TaskEvent { + pub action: String, + pub timestamp: Timestamp, +} + +impl std::fmt::Display for TaskEvent { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "action: {}, timestamp: {}", self.action, self.timestamp) + } +} + +impl Default for TaskEvent { + fn default() -> Self { + TaskEvent { + action: "open".to_string(), + timestamp: Timestamp(chrono::offset::Local::now().timestamp()), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Comment { + content: String, + author: String, + timestamp: Timestamp, +} + +impl std::fmt::Display for Comment { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{} author: {}, content: {} ", self.timestamp, self.author, self.content) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Timestamp(pub i64); + +impl std::fmt::Display for Timestamp { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let date = timestamp_to_date(self.0, "datetime"); + write!(f, "{}", date) + } +} diff --git a/bin/tau/tau-cli/src/util.rs b/bin/tau/tau-cli/src/util.rs index 177614876..2dba03e95 100644 --- a/bin/tau/tau-cli/src/util.rs +++ b/bin/tau/tau-cli/src/util.rs @@ -1,109 +1,19 @@ use std::{ env::{temp_dir, var}, fs::{self, File}, - io::{self, Read, Write}, - net::SocketAddr, - ops::Index, + io::Read, process::Command, }; use chrono::{Datelike, Local, NaiveDate, NaiveDateTime}; use log::error; -use prettytable::{cell, format, row, table, Cell, Row, Table}; use rand::distributions::{Alphanumeric, DistString}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; use darkfi::{Error, Result}; -use structopt::StructOpt; -use structopt_toml::StructOptToml; pub const CONFIG_FILE: &str = "taud_config.toml"; pub const CONFIG_FILE_CONTENTS: &str = include_str!("../../taud_config.toml"); -#[derive(StructOpt, Deserialize, Debug)] -pub enum CliTauSubCommands { - /// Add a new task - Add { - /// Specify task title - title: Option, - /// Specify task description - desc: Option, - /// Assign task to user - assign: Option, - /// Task project (can be hierarchical: crypto.zk) - project: Option, - /// Due date in DDMM format: "2202" for 22 Feb - due: Option, - /// Project rank single precision decimal real value: 4.8761 - rank: Option, - }, - /// Update/Edit an existing task by ID - Update { - /// Task ID - id: u64, - /// Field's name (ex title) - key: String, - /// New value - value: String, - }, - /// Set or Get task state - State { - /// Task ID - id: u64, - /// Set task state - state: Option, - }, - /// Set or Get comment for a task - Comment { - /// Task ID - id: u64, - /// Comment content - content: Option, - }, - /// List all tasks - List {}, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct TaskInfo { - pub ref_id: String, - pub id: u32, - pub title: String, - pub desc: String, - pub owner: String, - pub assign: Vec, - pub project: Vec, - pub due: Option, - pub rank: f32, - pub created_at: i64, - pub events: Vec, - pub comments: Vec, -} - -/// Tau cli -#[derive(Debug, Deserialize, StructOpt, StructOptToml)] -#[serde(default)] -#[structopt(name = "tau")] -pub struct CliTau { - /// Increase verbosity - #[structopt(short, parse(from_occurrences))] - pub verbose: u8, - /// JSON-RPC listen URL - #[structopt(long = "rpc", default_value = "127.0.0.1:11055")] - pub rpc_listen: SocketAddr, - /// Sets a custom config file - #[structopt(short, long)] - pub config: Option, - #[structopt(subcommand)] - pub command: Option, - /// Get task by ID - pub id: Option, - /// Search criteria (zero or more) - #[structopt(multiple = true)] - pub filters: Vec, -} - pub fn due_as_timestamp(due: &str) -> Option { if due.len() == 4 { let (day, month) = (due[..2].parse::().unwrap(), due[2..].parse::().unwrap()); @@ -130,24 +40,6 @@ pub fn due_as_timestamp(due: &str) -> Option { None } -pub fn set_title() -> Result { - print!("Title: "); - io::stdout().flush()?; - let mut t = String::new(); - io::stdin().read_line(&mut t)?; - - if t.is_empty() { - error!("You can't have a task without a title"); - return Err(Error::OperationFailed) - } - - if &t[(t.len() - 1)..] == "\n" { - t.pop(); - } - - Ok(t) -} - pub fn desc_in_editor() -> Result> { // Create a temporary file with some comments inside let mut file_path = temp_dir(); @@ -186,71 +78,7 @@ pub fn desc_in_editor() -> Result> { Ok(Some(description)) } -pub fn show_task(task: Value, taskinfo: TaskInfo, current_state: String) -> Result<()> { - let mut table = table!([Bd => "ref_id", &taskinfo.ref_id], - ["id", &taskinfo.id.to_string()], - [Bd =>"owner", &taskinfo.owner], - [Bd =>"title", &taskinfo.title], - ["desc", &taskinfo.desc], - [Bd =>"assign", get_from_task(task.clone(), "assign")?], - ["project", get_from_task(task.clone(), "project")?], - [Bd =>"due", timestamp_to_date(task["due"].clone(),"date")], - ["rank", &taskinfo.rank.to_string()], - [Bd =>"created_at", timestamp_to_date(task["created_at"].clone(), "datetime")], - ["current_state", ¤t_state], - [Bd => "comments", get_comments(task.clone())?]); - - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.set_titles(row!["Name", "Value"]); - - table.printstd(); - - let mut event_table = table!(["events", get_events(task)?]); - event_table.set_format(*format::consts::FORMAT_NO_COLSEP); - - event_table.printstd(); - - Ok(()) -} - -pub fn get_comments(rep: Value) -> Result { - let task: Value = serde_json::from_value(rep)?; - - let comments: Vec = serde_json::from_value(task["comments"].clone())?; - let mut result = String::new(); - - for comment in comments { - result.push_str(comment["author"].as_str().ok_or(Error::OperationFailed)?); - result.push_str(": "); - result.push_str(comment["content"].as_str().ok_or(Error::OperationFailed)?); - result.push('\n'); - } - result.pop(); - - Ok(result) -} - -pub fn get_events(rep: Value) -> Result { - let task: Value = serde_json::from_value(rep)?; - - let events: Vec = serde_json::from_value(task["events"].clone())?; - let mut ev = String::new(); - - for event in events { - ev.push_str("State changed to "); - ev.push_str(event["action"].as_str().ok_or(Error::OperationFailed)?); - ev.push_str(" at "); - ev.push_str(×tamp_to_date(event["timestamp"].clone(), "datetime")); - ev.push('\n'); - } - ev.pop(); - - Ok(ev) -} - -pub fn timestamp_to_date(timestamp: Value, dt: &str) -> String { - let timestamp = timestamp.as_i64().unwrap_or(0); - +pub fn timestamp_to_date(timestamp: i64, dt: &str) -> String { if timestamp <= 0 { return "".to_string() } @@ -265,139 +93,3 @@ pub fn timestamp_to_date(timestamp: Value, dt: &str) -> String { _ => "".to_string(), } } - -pub fn get_from_task(task: Value, value: &str) -> Result { - let vec_values: Vec = serde_json::from_value(task[value].clone())?; - let mut result = String::new(); - for (i, _) in vec_values.iter().enumerate() { - if !result.is_empty() { - result.push(','); - } - result.push_str(vec_values.index(i).as_str().unwrap()); - } - Ok(result) -} - -// Helper function to check task's state -fn check_task_state(task: &Value, state: &str) -> bool { - let events = match task["events"].as_array() { - Some(t) => t.to_owned(), - None => { - error!("Value is not an array!"); - vec![] - } - }; - - let last_state = match events.last() { - Some(s) => s["action"].as_str().unwrap(), - None => "open", - }; - state == last_state -} - -fn apply_filter(tasks: &mut Vec, filter: String) { - match filter.as_str() { - "open" => tasks.retain(|task| check_task_state(task, "open")), - "pause" => tasks.retain(|task| check_task_state(task, "pause")), - - _ if filter.len() == 4 && filter.parse::().is_ok() => { - let (month, year) = - (filter[..2].parse::().unwrap(), filter[2..].parse::().unwrap()); - - let year = year + 2000; - - tasks.retain(|task| { - let date = task["created_at"].as_i64().unwrap(); - let task_date = NaiveDateTime::from_timestamp(date, 0).date(); - let filter_date = NaiveDate::from_ymd(year, month, 1); - task_date.month() == filter_date.month() && task_date.year() == filter_date.year() - }) - } - - _ if filter.contains("assign:") | filter.contains("project:") => { - let kv: Vec<&str> = filter.split(':').collect(); - let key = kv[0]; - let value = Value::from(kv[1]); - - tasks.retain(|task| task[key].as_array().unwrap_or(&vec![]).contains(&value)) - } - - _ if filter.contains("rank>") | filter.contains("rank<") => { - let kv: Vec<&str> = if filter.contains('>') { - filter.split('>').collect() - } else { - filter.split('<').collect() - }; - let key = kv[0]; - let value = kv[1].parse::().unwrap_or(0.0); - - tasks.retain(|task| { - let rank = task[key].as_f64().unwrap_or(0.0) as f32; - if filter.contains('>') { - rank > value - } else { - rank < value - } - }) - } - - _ => {} - }; -} - -pub fn list_tasks(rep: Value, filters: Vec) -> Result<()> { - let mut table = Table::new(); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]); - - let mut tasks: Vec = serde_json::from_value(rep)?; - - for filter in filters { - apply_filter(&mut tasks, filter); - } - - tasks.sort_by(|a, b| b["rank"].as_f64().partial_cmp(&a["rank"].as_f64()).unwrap()); - - let (max_rank, min_rank) = if !tasks.is_empty() { - ( - serde_json::from_value(tasks[0]["rank"].clone())?, - serde_json::from_value(tasks[tasks.len() - 1]["rank"].clone())?, - ) - } else { - (0.0, 0.0) - }; - - for task in tasks { - let events: Vec = serde_json::from_value(task["events"].clone())?; - let state = match events.last() { - Some(s) => s["action"].as_str().unwrap(), - None => "open", - }; - - let rank = task["rank"].as_f64().unwrap_or(0.0) as f32; - - let (max_style, min_style, mid_style, gen_style) = if state == "open" { - ("bFC", "Fb", "Fc", "") - } else { - ("iFYBd", "iFYBd", "iFYBd", "iFYBd") - }; - - table.add_row(Row::new(vec![ - Cell::new(&task["id"].to_string()).style_spec(gen_style), - Cell::new(task["title"].as_str().unwrap()).style_spec(gen_style), - Cell::new(&get_from_task(task.clone(), "project")?).style_spec(gen_style), - Cell::new(&get_from_task(task.clone(), "assign")?).style_spec(gen_style), - Cell::new(×tamp_to_date(task["due"].clone(), "date")).style_spec(gen_style), - if rank == max_rank { - Cell::new(&rank.to_string()).style_spec(max_style) - } else if rank == min_rank { - Cell::new(&rank.to_string()).style_spec(min_style) - } else { - Cell::new(&rank.to_string()).style_spec(mid_style) - }, - ])); - } - table.printstd(); - - Ok(()) -} diff --git a/bin/tau/tau-cli/src/view.rs b/bin/tau/tau-cli/src/view.rs new file mode 100644 index 000000000..77a8192ec --- /dev/null +++ b/bin/tau/tau-cli/src/view.rs @@ -0,0 +1,125 @@ +use prettytable::{cell, format, row, table, Cell, Row, Table}; + +use darkfi::Result; + +use super::{ + filter::apply_filter, + primitives::{Comment, TaskEvent, TaskInfo}, + util::timestamp_to_date, +}; + +pub fn print_list_of_task(tasks: &mut Vec, filters: Vec) -> Result<()> { + let mut table = Table::new(); + + table.set_format( + format::FormatBuilder::new() + .padding(1, 1) + .separators( + &[format::LinePosition::Title], + format::LineSeparator::new('─', ' ', ' ', ' '), + ) + .build(), + ); + + table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]); + + for filter in filters { + apply_filter(tasks, filter); + } + + tasks.sort_by(|a, b| b.rank.partial_cmp(&a.rank).unwrap()); + + let mut min_rank = 0.0; + let mut max_rank = 0.0; + if tasks.first().is_some() { + max_rank = tasks.first().unwrap().rank; + } + if tasks.last().is_some() { + min_rank = tasks.last().unwrap().rank; + } + + for task in tasks { + let state = task.events.last().unwrap_or(&TaskEvent::default()).action.clone(); + + let (max_style, min_style, mid_style, gen_style) = if state == "open" { + ("bFC", "Fb", "Fc", "") + } else { + ("iFYBd", "iFYBd", "iFYBd", "iFYBd") + }; + + let rank = task.rank.to_string(); + + table.add_row(Row::new(vec![ + Cell::new(&task.id.to_string()).style_spec(gen_style), + Cell::new(&task.title).style_spec(gen_style), + Cell::new(&task.project.join(", ")).style_spec(gen_style), + Cell::new(&task.assign.join(", ")).style_spec(gen_style), + Cell::new(×tamp_to_date(task.due.unwrap_or(0), "date")).style_spec(gen_style), + if task.rank == max_rank { + Cell::new(&rank).style_spec(max_style) + } else if task.rank == min_rank { + Cell::new(&rank).style_spec(min_style) + } else { + Cell::new(&rank).style_spec(mid_style) + }, + ])); + } + table.printstd(); + + Ok(()) +} + +pub fn print_task_info(taskinfo: TaskInfo) -> Result<()> { + let current_state = &taskinfo.events.last().unwrap_or(&TaskEvent::default()).action.clone(); + let due = timestamp_to_date(taskinfo.due.unwrap_or(0), "date"); + let created_at = timestamp_to_date(taskinfo.created_at, "datetime"); + let mut table = table!([Bd => "ref_id", &taskinfo.ref_id], + ["id", &taskinfo.id.to_string()], + [Bd =>"owner", &taskinfo.owner], + [Bd =>"title", &taskinfo.title], + ["desc", &taskinfo.desc.to_string()], + [Bd =>"assign", taskinfo.assign.join(", ")], + ["project", taskinfo.project.join(", ")], + [Bd =>"due", due], + ["rank", &taskinfo.rank.to_string()], + [Bd =>"created_at", created_at], + ["current_state", current_state]); + + table.set_format( + format::FormatBuilder::new() + .padding(1, 1) + .separators( + &[format::LinePosition::Title], + format::LineSeparator::new('─', ' ', ' ', ' '), + ) + .build(), + ); + table.set_titles(row!["Name", "Value"]); + + table.printstd(); + + let mut event_table = table!(["events", &events_as_string(taskinfo.events)]); + event_table.set_format(*format::consts::FORMAT_NO_COLSEP); + + event_table.printstd(); + + Ok(()) +} + +pub fn comments_as_string(comments: Vec) -> String { + let mut comments_str = String::new(); + for comment in comments { + comments_str.push_str(&comment.to_string()); + comments_str.push('\n'); + } + comments_str +} + +pub fn events_as_string(events: Vec) -> String { + let mut events_str = String::new(); + for event in events { + events_str.push_str(&event.to_string()); + events_str.push('\n'); + } + events_str +} diff --git a/bin/tau/taud/src/jsonrpc.rs b/bin/tau/taud/src/jsonrpc.rs index 0f9f70b44..07910f17a 100644 --- a/bin/tau/taud/src/jsonrpc.rs +++ b/bin/tau/taud/src/jsonrpc.rs @@ -34,7 +34,7 @@ struct BaseTaskInfo { assign: Vec, project: Vec, due: Option, - rank: f32, + rank: Option, } #[async_trait] @@ -54,10 +54,9 @@ impl RequestHandler for JsonRpcInterface { Some("add") => self.add(req.params).await, Some("list") => self.list(req.params).await, Some("update") => self.update(req.params).await, - Some("get_state") => self.get_state(req.params).await, Some("set_state") => self.set_state(req.params).await, Some("set_comment") => self.set_comment(req.params).await, - Some("get_by_id") => self.get_by_id(req.params).await, + Some("get_task_by_id") => self.get_task_by_id(req.params).await, Some(_) | None => { return JsonResult::Err(jsonerr(ErrorCode::MethodNotFound, None, req.id)) } @@ -101,7 +100,7 @@ impl JsonRpcInterface { &task.desc, &self.nickname, task.due, - task.rank, + task.rank.unwrap_or(0.0), &self.dataset_path, )?; new_task.set_project(&task.project); @@ -141,23 +140,6 @@ impl JsonRpcInterface { Ok(json!(true)) } - // RPCAPI: - // Get task's state. - // --> {"jsonrpc": "2.0", "method": "get_state", "params": [task_id], "id": 1} - // <-- {"jsonrpc": "2.0", "result": "state", "id": 1} - async fn get_state(&self, params: Value) -> TaudResult { - debug!(target: "tau", "JsonRpc::get_state() params {}", params); - let args = params.as_array().unwrap(); - - if args.len() != 1 { - return Err(TaudError::InvalidData("len of params should be 1".into())) - } - - let task: TaskInfo = self.load_task_by_id(&args[0])?; - - Ok(json!(task.get_state())) - } - // RPCAPI: // Set state for a task and returns `true` upon success. // --> {"jsonrpc": "2.0", "method": "set_state", "params": [task_id, state], "id": 1} @@ -203,10 +185,10 @@ impl JsonRpcInterface { // RPCAPI: // Get a task by id. - // --> {"jsonrpc": "2.0", "method": "get_by_id", "params": [task_id], "id": 1} + // --> {"jsonrpc": "2.0", "method": "get_task_by_id", "params": [task_id], "id": 1} // <-- {"jsonrpc": "2.0", "result": "task", "id": 1} - async fn get_by_id(&self, params: Value) -> TaudResult { - debug!(target: "tau", "JsonRpc::get_by_id() params {}", params); + async fn get_task_by_id(&self, params: Value) -> TaudResult { + debug!(target: "tau", "JsonRpc::get_task_by_id() params {}", params); let args = params.as_array().unwrap(); if args.len() != 1 { @@ -227,51 +209,72 @@ impl JsonRpcInterface { task.ok_or(TaudError::InvalidId) } - fn check_params_for_update(&self, task_id: &Value, data: &Value) -> TaudResult { + fn check_params_for_update(&self, task_id: &Value, fields: &Value) -> TaudResult { let mut task: TaskInfo = self.load_task_by_id(task_id)?; - if !data.is_object() { + if !fields.is_array() { return Err(TaudError::InvalidData("Invalid task's data".into())) } - let data = data.as_object().unwrap(); + let fields = fields.as_array().unwrap(); - if data.contains_key("title") { - let title = data.get("title").unwrap().clone(); - let title: String = serde_json::from_value(title)?; - task.set_title(&title); + for field in fields { + if !field.is_object() { + return Err(TaudError::InvalidData("Invalid task's fields".into())) + } + + let field = field.as_object().unwrap(); + + if field.contains_key("title") { + let title = field.get("title").unwrap().clone(); + let title: String = serde_json::from_value(title)?; + if !title.is_empty() { + task.set_title(&title); + } + } + + if field.contains_key("desc") { + let description = field.get("description"); + if let Some(description) = description { + let description: String = serde_json::from_value(description.clone())?; + task.set_desc(&description); + } + } + + if field.contains_key("rank") { + let rank_opt = field.get("rank"); + if let Some(rank) = rank_opt { + let rank: Option = serde_json::from_value(rank.clone())?; + if rank.is_some() { + task.set_rank(rank.unwrap()); + } + } + } + + if field.contains_key("due") { + let due = field.get("due").unwrap().clone(); + let due: Option> = serde_json::from_value(due)?; + if let Some(d) = due { + task.set_due(d); + } + } + + if field.contains_key("assign") { + let assign = field.get("assign").unwrap().clone(); + let assign: Vec = serde_json::from_value(assign)?; + if !assign.is_empty() { + task.set_assign(&assign); + } + } + + if field.contains_key("project") { + let project = field.get("project").unwrap().clone(); + let project: Vec = serde_json::from_value(project)?; + if !project.is_empty() { + task.set_project(&project); + } + } } - - if data.contains_key("description") { - let description = data.get("description").unwrap().clone(); - let description: String = serde_json::from_value(description)?; - task.set_desc(&description); - } - - if data.contains_key("rank") { - let rank = data.get("rank").unwrap().clone(); - let rank: f32 = serde_json::from_value(rank)?; - task.set_rank(rank); - } - - if data.contains_key("due") { - let due = data.get("due").unwrap().clone(); - let due = serde_json::from_value(due)?; - task.set_due(Some(due)); - } - - if data.contains_key("assign") { - let assign = data.get("assign").unwrap().clone(); - let assign: Vec = serde_json::from_value(assign)?; - task.set_assign(&assign); - } - - if data.contains_key("project") { - let project = data.get("project").unwrap().clone(); - let project: Vec = serde_json::from_value(project)?; - task.set_project(&project); - } - Ok(task) } }