tau: rework on tau-cli functionalities and design

This commit is contained in:
ghassmo
2022-05-08 03:10:22 +03:00
parent 2d629a1c56
commit 7796f313ef
9 changed files with 539 additions and 619 deletions

View File

@@ -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
### 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<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
```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
```
### 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
bin/tau/tau-cli/src/cli.rs Normal file
View File

@@ -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)
}

View File

@@ -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
}
})
}
}
_ => {}
};
}

View File

@@ -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
}

View File

@@ -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)?;

View File

@@ -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)
}
}

View File

@@ -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
bin/tau/tau-cli/src/view.rs Normal file
View File

@@ -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
}

View File

@@ -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()))
}
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<f32> = 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<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);
}
}
}
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<String> = serde_json::from_value(assign)?;
task.set_assign(&assign);
}
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);
}
Ok(task)
}
}