Kaynağa Gözat

tau: rework on tau-cli functionalities and design

ghassmo 4 yıl önce
ebeveyn
işleme
7796f313ef

+ 55 - 159
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 <config>     Sets a custom config file
-			--rpc <rpc-listen>    JSON-RPC listen URL [default: 127.0.0.1:11055]
-
+	    -c, --config <config>     Sets a custom config file
+	        --rpc <rpc-listen>    JSON-RPC listen URL [default: 127.0.0.1:11055]
+	
 	ARGS:
-		<id>            Get task by ID
-		<filters>...    Search criteria (zero or more)
-
+	    <id>            Get task by ID
+	    <filters>...    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
-
-```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
-```
-
-
-### 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<number"  # lists all tasks that have rank lesser than "number"
-```
-
-Combined filters:
-```shell
-% tau project:project1 assign:person2 0522 open
-```
-Output:
-```text
- ID | Title  | Project  | Assigned        | Due             | Rank 
-----+--------+----------+-----------------+-----------------+------
- 1  | title1 | project1 | person1,person2 | Wednesday 4 May | 4.74 
-```
-
-
-### Update an existing task
-
-```shell
-% tau update 3 project project3 
-% tau "rank<4" # qoutes are for escaping special characters
-```
-Output:
-```text
- ID | Title     | Project  | Assigned | Due | Rank 
-----+-----------+----------+----------+-----+------
- 3  | new title | project3 |          |     | 0 
-```
-
-
-### Get/Set task state
-
-```shell
-% tau state 1 
-```
-Output:
-```text
-Task with id 1 is: "open"
-```
-
-```shell
-% tau state 1 pause
-% tau state 1 
-```
-Output:
-```text
-Task with id 1 is: "pause"
-```
-
-```shell
-% tau state 2 stop # this will deactivate the task (task is done)
-```
-
-
-### Get/Set comment
+### Example  
 
 ```shell
-% tau comment 1 person1 "some awesome comment"
-% tau comment 1 person2 "other awesome comment"
-% tau comment 1
-```
-Output:
-```text
-Comments on Task with id 1:
-person1: some awesome comment
-person2: other awesome comment
+$ # 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 
 ```
 
 
-### Get a task
 
-```shell
-% tau 1
-```
-Output:
-```text
- Name          | Value 
----------------+--------------------------------
- ref_id        | cGw1AI7cBSdJWIqPMU8d355wRrB0qy 
- id            | 1 
- title         | title1 
- desc          | description 
- assign        | person1,person2 
- project       | project1 
- due           | Wednesday 4 May 
- rank          | 4.74 
- created_at    | 21:28 Monday 2 May 
- current_state | pause 
- comments      | person1: some awesome comment 
-               | person2: other awesome comment 
-------------------------------------------------------
- events  State changed to pause at 21:34 Monday 2 May 
-------------------------------------------------------
-```

+ 112 - 0
bin/tau/tau-cli/src/cli.rs

@@ -0,0 +1,112 @@
+use std::net::SocketAddr;
+
+use serde::{Deserialize, Serialize};
+use structopt::StructOpt;
+use structopt_toml::StructOptToml;
+
+use darkfi::Result;
+
+use super::util::due_as_timestamp;
+
+/// 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<String>,
+    #[structopt(subcommand)]
+    pub command: Option<CliTauSubCommands>,
+    /// Get task by ID
+    pub id: Option<String>,
+    /// Search criteria (zero or more)
+    #[structopt(multiple = true)]
+    pub filters: Vec<String>,
+}
+
+#[derive(StructOpt, Deserialize, Debug)]
+pub enum CliTauSubCommands {
+    /// Add a new task
+    Add { values: Vec<String> },
+    /// Update/Edit an existing task by ID
+    Update {
+        /// Task ID
+        id: u64,
+        /// Values (ex: project:blockchain)
+        values: Vec<String>,
+    },
+    /// Set or Get task state
+    State {
+        /// Task ID
+        id: u64,
+        /// Set task state
+        state: Option<String>,
+    },
+    /// Set or Get comment for a task
+    Comment {
+        /// Task ID
+        id: u64,
+        /// Comment content
+        content: Option<String>,
+    },
+    /// List all tasks
+    List {},
+}
+
+#[derive(Serialize, Debug, Clone)]
+pub struct CliBaseTask {
+    pub title: String,
+    pub desc: Option<String>,
+    pub assign: Vec<String>,
+    pub project: Vec<String>,
+    pub due: Option<i64>,
+    pub rank: Option<f32>,
+}
+
+pub fn task_from_cli_values(values: Vec<String>) -> Result<CliBaseTask> {
+    let mut title: String = String::new();
+    let mut desc: Option<String> = None;
+    let mut project: Vec<String> = vec![];
+    let mut assign: Vec<String> = vec![];
+    let mut due: Option<i64> = None;
+    let mut rank: Option<f32> = 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::<f32>()?);
+        }
+    }
+
+    let task = CliBaseTask { title, desc, project, assign, due, rank };
+
+    Ok(task)
+}

+ 71 - 0
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<TaskInfo>, 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::<u32>().is_ok() => {
+            let (month, year) =
+                (filter[..2].parse::<u32>().unwrap(), filter[2..].parse::<i32>().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::<f32>().unwrap_or(0.0);
+
+                tasks.retain(|task| {
+                    if filter.contains("lt") {
+                        task.rank < value
+                    } else if filter.contains("gt") {
+                        task.rank > value
+                    } else {
+                        true
+                    }
+                })
+            }
+        }
+
+        _ => {}
+    };
+}

+ 3 - 11
bin/tau/tau-cli/src/jsonrpc.rs

@@ -75,14 +75,6 @@ pub async fn set_state(url: &str, id: u64, state: &str) -> Result<Value> {
     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<Value> {
-    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<Value> {
 }
 
 // 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<Value> {
-    let req = jsonrpc::request(json!("get_by_id"), json!([id]));
+pub async fn get_task_by_id(url: &str, id: u64) -> Result<Value> {
+    let req = jsonrpc::request(json!("get_task_by_id"), json!([id]));
     request(req, url.to_string()).await
 }

+ 43 - 77
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<String> = vec!["stop".into(), "open".into(), "pause".into()];
+
     match options.id {
         Some(id) if id.len() < 4 && id.parse::<u64>().is_ok() => {
-            let task = get_by_id(rpc_addr, id.parse::<u64>().unwrap()).await?;
+            let task = jsonrpc::get_task_by_id(rpc_addr, id.parse::<u64>().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::<u64>().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<String> = match assign {
-                Some(a) => a.split(',').map(|s| s.into()).collect(),
-                None => vec![],
-            };
-
-            let project: Vec<String> = 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::<f32>()?)
-                }
-                "project" | "assign" => {
-                    json!(value.split(',').collect::<Vec<&str>>())
-                }
-                _ => {
-                    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<TaskInfo> = 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)?;

+ 63 - 0
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<String>,
+    pub project: Vec<String>,
+    pub due: Option<i64>,
+    pub rank: f32,
+    pub created_at: i64,
+    pub events: Vec<TaskEvent>,
+    pub comments: Vec<Comment>,
+}
+
+#[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)
+    }
+}

+ 2 - 310
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<String>,
-        /// Specify task description
-        desc: Option<String>,
-        /// Assign task to user
-        assign: Option<String>,
-        /// Task project (can be hierarchical: crypto.zk)
-        project: Option<String>,
-        /// Due date in DDMM format: "2202" for 22 Feb
-        due: Option<String>,
-        /// Project rank single precision decimal real value: 4.8761
-        rank: Option<f32>,
-    },
-    /// 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<String>,
-    },
-    /// Set or Get comment for a task
-    Comment {
-        /// Task ID
-        id: u64,
-        /// Comment content
-        content: Option<String>,
-    },
-    /// 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<String>,
-    pub project: Vec<String>,
-    pub due: Option<i64>,
-    pub rank: f32,
-    pub created_at: i64,
-    pub events: Vec<Value>,
-    pub comments: Vec<Value>,
-}
-
-/// 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<String>,
-    #[structopt(subcommand)]
-    pub command: Option<CliTauSubCommands>,
-    /// Get task by ID
-    pub id: Option<String>,
-    /// Search criteria (zero or more)
-    #[structopt(multiple = true)]
-    pub filters: Vec<String>,
-}
-
 pub fn due_as_timestamp(due: &str) -> Option<i64> {
     if due.len() == 4 {
         let (day, month) = (due[..2].parse::<u32>().unwrap(), due[2..].parse::<u32>().unwrap());
@@ -130,24 +40,6 @@ pub fn due_as_timestamp(due: &str) -> Option<i64> {
     None
 }
 
-pub fn set_title() -> Result<String> {
-    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<Option<String>> {
     // Create a temporary file with some comments inside
     let mut file_path = temp_dir();
@@ -186,71 +78,7 @@ pub fn desc_in_editor() -> Result<Option<String>> {
     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", &current_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<String> {
-    let task: Value = serde_json::from_value(rep)?;
-
-    let comments: Vec<Value> = 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<String> {
-    let task: Value = serde_json::from_value(rep)?;
-
-    let events: Vec<Value> = 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(&timestamp_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<String> {
-    let vec_values: Vec<Value> = 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<Value>, 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::<u32>().is_ok() => {
-            let (month, year) =
-                (filter[..2].parse::<u32>().unwrap(), filter[2..].parse::<i32>().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::<f32>().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<String>) -> 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<Value> = 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<Value> = 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(&timestamp_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(())
-}

+ 125 - 0
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<TaskInfo>, filters: Vec<String>) -> 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(&timestamp_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<Comment>) -> 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<TaskEvent>) -> String {
+    let mut events_str = String::new();
+    for event in events {
+        events_str.push_str(&event.to_string());
+        events_str.push('\n');
+    }
+    events_str
+}

+ 60 - 57
bin/tau/taud/src/jsonrpc.rs

@@ -34,7 +34,7 @@ struct BaseTaskInfo {
     assign: Vec<String>,
     project: Vec<String>,
     due: Option<Timestamp>,
-    rank: f32,
+    rank: Option<f32>,
 }
 
 #[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<Value> {
-        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<Value> {
-        debug!(target: "tau", "JsonRpc::get_by_id() params {}", params);
+    async fn get_task_by_id(&self, params: Value) -> TaudResult<Value> {
+        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<TaskInfo> {
+    fn check_params_for_update(&self, task_id: &Value, fields: &Value) -> TaudResult<TaskInfo> {
         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()))
+            }
 
-        if data.contains_key("description") {
-            let description = data.get("description").unwrap().clone();
-            let description: String = serde_json::from_value(description)?;
-            task.set_desc(&description);
-        }
+            let field = field.as_object().unwrap();
 
-        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 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 data.contains_key("due") {
-            let due = data.get("due").unwrap().clone();
-            let due = serde_json::from_value(due)?;
-            task.set_due(Some(due));
-        }
+            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 data.contains_key("assign") {
-            let assign = data.get("assign").unwrap().clone();
-            let assign: Vec<String> = serde_json::from_value(assign)?;
-            task.set_assign(&assign);
-        }
+            if field.contains_key("rank") {
+                let rank_opt = field.get("rank");
+                if let Some(rank) = rank_opt {
+                    let rank: Option<f32> = serde_json::from_value(rank.clone())?;
+                    if rank.is_some() {
+                        task.set_rank(rank.unwrap());
+                    }
+                }
+            }
 
-        if data.contains_key("project") {
-            let project = data.get("project").unwrap().clone();
-            let project: Vec<String> = serde_json::from_value(project)?;
-            task.set_project(&project);
-        }
+            if field.contains_key("due") {
+                let due = field.get("due").unwrap().clone();
+                let due: Option<Option<Timestamp>> = 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<String> = 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<String> = serde_json::from_value(project)?;
+                if !project.is_empty() {
+                    task.set_project(&project);
+                }
+            }
+        }
         Ok(task)
     }
 }