Przeglądaj źródła

tau-cli: Code cleanup.

parazyd 4 lat temu
rodzic
commit
f2fe803812

+ 1 - 8
Cargo.lock

@@ -3974,22 +3974,15 @@ checksum = "d7fa7e55043acb85fca6b3c01485a2eeb6b69c5d21002e273c79e465f43b7ac1"
 name = "tau"
 version = "0.3.0"
 dependencies = [
- "async-channel",
- "async-executor",
  "async-std",
- "async-trait",
  "chrono",
+ "clap 3.1.18",
  "darkfi",
- "futures",
  "log 0.4.17",
  "prettytable-rs",
- "rand",
  "serde",
  "serde_json",
  "simplelog",
- "smol",
- "structopt",
- "structopt-toml",
  "url",
 ]
 

+ 11 - 19
bin/tau/tau-cli/Cargo.toml

@@ -1,29 +1,21 @@
 [package]
 name = "tau"
 version = "0.3.0"
+homepage = "https://dark.fi"
+description = "Command-line client for taud"
+authors = ["darkfi <dev@dark.fi>"]
+repository = "https://github.com/darkrenaissance/darkfi"
+license = "AGPL-3.0-only"
 edition = "2021"
 
 [dependencies]
-darkfi = { path = "../../../", features = ["rpc"]}
-
-# Async
-smol = "1.2.5"
-futures = "0.3.21"
 async-std = {version = "1.11.0", features = ["attributes"]}
-async-trait = "0.1.53"
-async-channel = "1.6.1"
-async-executor = "1.4.1"
-
-# Misc
-log = "0.4.17"
-simplelog = "0.12.0"
-url = "2.2.2"
 chrono = "0.4.19"
+clap = {version = "3.1.18", features = ["derive"]}
+darkfi = { path = "../../../", features = ["rpc"]}
+log = "0.4.17"
 prettytable-rs = "0.8.0"
-rand = "0.8.5"
-
-# Encoding and parsing
-serde_json = "1.0.81"
 serde = {version = "1.0.137", features = ["derive"]}
-structopt = "0.3.26"
-structopt-toml = "0.5.0"
+serde_json = "1.0.81"
+simplelog = "0.12.0"
+url = "2.2.2"

+ 9 - 14
bin/tau/tau-cli/src/filter.rs

@@ -1,16 +1,16 @@
 use chrono::{Datelike, NaiveDate, NaiveDateTime};
 use serde_json::Value;
 
-use super::primitives::{TaskEvent, TaskInfo};
+use crate::{primitives::TaskInfo, TaskEvent};
 
-// Helper function to check task's state
+/// 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() {
+pub fn apply_filter(tasks: &mut Vec<TaskInfo>, filter: &str) {
+    match filter {
         "open" => tasks.retain(|task| check_task_state(task, "open")),
         "pause" => tasks.retain(|task| check_task_state(task, "pause")),
 
@@ -19,7 +19,6 @@ pub fn apply_filter(tasks: &mut Vec<TaskInfo>, filter: String) {
                 (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();
@@ -31,9 +30,8 @@ pub fn apply_filter(tasks: &mut Vec<TaskInfo>, filter: String) {
         _ 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 let Some(value) = Value::from(kv[1]).as_str() {
+                    tasks.retain(|task| task.assign.contains(&value.to_string()))
                 }
             }
         }
@@ -41,19 +39,16 @@ pub fn apply_filter(tasks: &mut Vec<TaskInfo>, filter: String) {
         _ 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 let Some(value) = Value::from(kv[1]).as_str() {
+                    tasks.retain(|task| task.project.contains(&value.to_string()))
                 }
             }
         }
 
         _ 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
@@ -67,5 +62,5 @@ pub fn apply_filter(tasks: &mut Vec<TaskInfo>, filter: String) {
         }
 
         _ => {}
-    };
+    }
 }

+ 0 - 72
bin/tau/tau-cli/src/jsonrpc.rs

@@ -1,72 +0,0 @@
-use serde_json::{json, Value};
-
-use darkfi::{
-    rpc::{jsonrpc, rpcclient::RpcClient},
-    Result,
-};
-
-pub struct Rpc {
-    pub client: RpcClient,
-}
-
-impl Rpc {
-    // RPCAPI:
-    // Add new task and returns `true` upon success.
-    // --> {"jsonrpc": "2.0", "method": "add",
-    //      "params":
-    //          [{
-    //          "title": "..",
-    //          "desc": "..",
-    //          assign: [..],
-    //          project: [..],
-    //          "due": ..,
-    //          "rank": ..
-    //          }],
-    //      "id": 1
-    //      }
-    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
-    pub async fn add(&self, params: Value) -> Result<Value> {
-        let req = jsonrpc::request(json!("add"), params);
-        self.client.request(req).await
-    }
-
-    // List tasks
-    // --> {"jsonrpc": "2.0", "method": "get_ids", "params": [], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": [task_id, ...], "id": 1}
-    pub async fn get_ids(&self, params: Value) -> Result<Value> {
-        let req = jsonrpc::request(json!("get_ids"), json!(params));
-        self.client.request(req).await
-    }
-
-    // Update task and returns `true` upon success.
-    // --> {"jsonrpc": "2.0", "method": "update", "params": [task_id, {"title": "new title"} ], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
-    pub async fn update(&self, id: u64, data: Value) -> Result<Value> {
-        let req = jsonrpc::request(json!("update"), json!([id, data]));
-        self.client.request(req).await
-    }
-
-    // Set state for a task and returns `true` upon success.
-    // --> {"jsonrpc": "2.0", "method": "set_state", "params": [task_id, state], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
-    pub async fn set_state(&self, id: u64, state: &str) -> Result<Value> {
-        let req = jsonrpc::request(json!("set_state"), json!([id, state]));
-        self.client.request(req).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}
-    pub async fn set_comment(&self, id: u64, content: &str) -> Result<Value> {
-        let req = jsonrpc::request(json!("set_comment"), json!([id, content]));
-        self.client.request(req).await
-    }
-
-    // Get task by id.
-    // --> {"jsonrpc": "2.0", "method": "get_task_by_id", "params": [task_id], "id": 1}
-    // <-- {"jsonrpc": "2.0", "result": "task", "id": 1}
-    pub async fn get_task_by_id(&self, id: u64) -> Result<Value> {
-        let req = jsonrpc::request(json!("get_task_by_id"), json!([id]));
-        self.client.request(req).await
-    }
-}

+ 118 - 81
bin/tau/tau-cli/src/main.rs

@@ -1,126 +1,163 @@
+use std::process::exit;
+
+use clap::{Parser, Subcommand};
 use log::error;
-use serde_json::json;
 use simplelog::{ColorChoice, TermLogger, TerminalMode};
-use structopt_toml::StructOptToml;
 use url::Url;
 
-use darkfi::{
-    rpc::rpcclient::RpcClient,
-    util::{
-        cli::{log_config, spawn_config},
-        path::get_config_path,
-    },
-    Result,
-};
+use darkfi::{cli_desc, rpc::rpcclient::RpcClient, util::cli::log_config, Error, Result};
 
-mod cli;
 mod filter;
-mod jsonrpc;
 mod primitives;
+mod rpc;
 mod util;
 mod view;
 
-use cli::CliTauSubCommands;
-use jsonrpc::Rpc;
-use primitives::{TaskEvent, TaskInfo};
-use util::{desc_in_editor, CONFIG_FILE, CONFIG_FILE_CONTENTS};
-use view::{comments_as_string, print_list_of_task, print_task_info};
+use primitives::{task_from_cli, TaskEvent};
+use util::{desc_in_editor, due_as_timestamp};
+use view::{comments_as_string, print_task_info, print_task_list};
 
-async fn start(mut options: cli::CliTau) -> Result<()> {
-    let rpc_client = Rpc { client: RpcClient::new(Url::parse(&options.rpc_listen)?).await? };
+#[derive(Parser)]
+#[clap(name = "tau", about = cli_desc!(), version)]
+#[clap(arg_required_else_help(true))]
+struct Args {
+    #[clap(short, parse(from_occurrences))]
+    /// Increase verbosity (-vvv supported)
+    verbose: u8,
 
-    let states: Vec<String> = vec!["stop".into(), "open".into(), "pause".into()];
+    #[clap(short, long, default_value = "tcp://127.0.0.1:11055")]
+    /// taud JSON-RPC endpoint
+    endpoint: Url,
 
-    match options.id {
-        Some(id) if id.len() < 4 && id.parse::<u64>().is_ok() => {
-            let task = rpc_client.get_task_by_id(id.parse::<u64>().unwrap()).await?;
-            let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
-            print_task_info(taskinfo)?;
-            return Ok(())
-        }
-        Some(id) => options.filters.push(id),
-        None => {}
-    }
+    #[clap(subcommand)]
+    command: TauSubcommand,
+}
+
+#[derive(Subcommand)]
+enum TauSubcommand {
+    /// 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 {
+        /// Search criteria (zero or more)
+        filters: Vec<String>,
+    },
+
+    /// Get task info by ID
+    Info { id: u64 },
+}
 
-    match options.command {
-        Some(CliTauSubCommands::Add { values }) => {
-            let mut task = cli::task_from_cli_values(values)?;
+pub struct Tau {
+    pub rpc_client: RpcClient,
+}
+
+#[async_std::main]
+async fn main() -> Result<()> {
+    let args = Args::parse();
+
+    let (lvl, conf) = log_config(args.verbose.into())?;
+    TermLogger::init(lvl, conf, TerminalMode::Mixed, ColorChoice::Auto)?;
+
+    let rpc_client = RpcClient::new(args.endpoint).await?;
+    let tau = Tau { rpc_client };
+
+    // Allowed states for a task
+    let states = ["stop", "open", "pause"];
+
+    // Parse subcommands
+    match args.command {
+        TauSubcommand::Add { values } => {
+            let mut task = task_from_cli(values)?;
             if task.title.is_empty() {
-                error!("Provide a title for the task");
-                return Ok(())
+                error!("Please provide a title for the task.");
+                exit(1);
             };
 
             if task.desc.is_none() {
                 task.desc = desc_in_editor()?;
             };
 
-            rpc_client.add(json!([task])).await?;
+            return tau.add(task).await
         }
 
-        Some(CliTauSubCommands::Update { id, values }) => {
-            let task = cli::task_from_cli_values(values)?;
-            rpc_client.update(id, json!([task])).await?;
+        TauSubcommand::Update { id, values } => {
+            let task = task_from_cli(values)?;
+            tau.update(id, task).await
         }
 
-        Some(CliTauSubCommands::State { id, state }) => match state {
+        TauSubcommand::State { id, state } => match state {
             Some(state) => {
                 let state = state.trim().to_lowercase();
-                if states.contains(&state) {
-                    rpc_client.set_state(id, &state).await?;
+                if states.contains(&state.as_str()) {
+                    tau.set_state(id, &state).await
                 } else {
-                    error!("Task state could only be one of three states: open, pause or stop");
+                    error!(
+                        "Task state can only be one of the following {}: {:?}",
+                        states.len(),
+                        states
+                    );
+                    return Err(Error::OperationFailed)
                 }
             }
             None => {
-                let task = rpc_client.get_task_by_id(id).await?;
-                let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
-                let default_event = TaskEvent::default();
-                let state = &taskinfo.events.last().unwrap_or(&default_event).action;
+                let task = tau.get_task_by_id(id).await?;
+                let state = &task.events.last().unwrap_or(&TaskEvent::default()).action.clone();
                 println!("Task {}: {}", id, state);
+                Ok(())
             }
         },
 
-        Some(CliTauSubCommands::Comment { id, content }) => match content {
-            Some(content) => {
-                rpc_client.set_comment(id, content.trim()).await?;
-            }
+        TauSubcommand::Comment { id, content } => match content {
+            Some(content) => tau.set_comment(id, content.trim()).await,
             None => {
-                let task = rpc_client.get_task_by_id(id).await?;
-                let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
-                let comments = comments_as_string(taskinfo.comments);
+                let task = tau.get_task_by_id(id).await?;
+                let comments = comments_as_string(task.comments);
                 println!("Comments {}:\n{}", id, comments);
+                Ok(())
             }
         },
 
-        Some(CliTauSubCommands::List {}) | None => {
-            let task_ids = rpc_client.get_ids(json!([])).await?;
-            let mut tasks: Vec<TaskInfo> = vec![];
-            if let Some(ids) = task_ids.as_array() {
-                for id in ids {
-                    let id = if id.is_u64() { id.as_u64().unwrap() } else { continue };
-                    let task = rpc_client.get_task_by_id(id).await?;
-                    let taskinfo: TaskInfo = serde_json::from_value(task.clone())?;
-                    tasks.push(taskinfo);
-                }
+        TauSubcommand::List { filters } => {
+            let task_ids = tau.get_ids().await?;
+            let mut tasks = vec![];
+            for id in task_ids {
+                tasks.push(tau.get_task_by_id(id).await?);
             }
-
-            // let mut tasks: Vec<TaskInfo> = serde_json::from_value(tasks)?;
-            print_list_of_task(&mut tasks, options.filters)?;
+            print_task_list(tasks, filters)?;
+            Ok(())
         }
-    }
 
-    Ok(())
-}
-
-#[async_std::main]
-async fn main() -> Result<()> {
-    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 = 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)?;
+        TauSubcommand::Info { id } => {
+            let task = tau.get_task_by_id(id).await?;
+            print_task_info(task)?;
+            Ok(())
+        }
+    }?;
 
-    start(args).await
+    tau.close_connection().await
 }

+ 59 - 9
bin/tau/tau-cli/src/primitives.rs

@@ -1,8 +1,18 @@
-use serde::{Deserialize, Serialize};
+use darkfi::{util::Timestamp, Result};
 
-use darkfi::util::Timestamp;
+use crate::due_as_timestamp;
 
-#[derive(Debug, Clone, Deserialize, Serialize)]
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
+pub struct BaseTask {
+    pub title: String,
+    pub desc: Option<String>,
+    pub assign: Vec<String>,
+    pub project: Vec<String>,
+    pub due: Option<i64>,
+    pub rank: Option<f32>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
 pub struct TaskInfo {
     pub ref_id: String,
     pub id: u32,
@@ -18,7 +28,7 @@ pub struct TaskInfo {
     pub comments: Vec<Comment>,
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize)]
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
 pub struct TaskEvent {
     pub action: String,
     pub timestamp: Timestamp,
@@ -32,14 +42,11 @@ impl std::fmt::Display for TaskEvent {
 
 impl Default for TaskEvent {
     fn default() -> Self {
-        TaskEvent {
-            action: "open".to_string(),
-            timestamp: Timestamp(chrono::offset::Local::now().timestamp()),
-        }
+        Self { action: "open".into(), timestamp: Timestamp::current_time() }
     }
 }
 
-#[derive(Clone, Debug, Serialize, Deserialize)]
+#[derive(serde::Serialize, serde::Deserialize, Debug)]
 pub struct Comment {
     content: String,
     author: String,
@@ -51,3 +58,46 @@ impl std::fmt::Display for Comment {
         write!(f, "{} author: {}, content: {} ", self.timestamp, self.author, self.content)
     }
 }
+
+pub fn task_from_cli(values: Vec<String>) -> Result<BaseTask> {
+    let mut title = String::new();
+    let mut desc = None;
+    let mut project = vec![];
+    let mut assign = vec![];
+    let mut due = None;
+    let mut rank = 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] == "project" {
+            project = field[1].split(',').map(|s| s.into()).collect();
+        }
+
+        if field[0] == "desc" {
+            desc = Some(field[1].into());
+        }
+
+        if field[0] == "assign" {
+            assign = field[1].split(',').map(|s| s.into()).collect();
+        }
+
+        if field[0] == "due" {
+            due = due_as_timestamp(&field[1])
+        }
+
+        if field[0] == "rank" {
+            rank = Some(field[1].parse::<f32>()?);
+        }
+    }
+
+    Ok(BaseTask { title, desc, project, assign, due, rank })
+}

+ 90 - 0
bin/tau/tau-cli/src/rpc.rs

@@ -0,0 +1,90 @@
+use log::error;
+use serde_json::json;
+
+use darkfi::{rpc::jsonrpc, Result};
+
+use crate::{
+    primitives::{BaseTask, TaskInfo},
+    Tau,
+};
+
+impl Tau {
+    pub async fn close_connection(&self) -> Result<()> {
+        self.rpc_client.close().await
+    }
+
+    /// Add a new task.
+    pub async fn add(&self, task: BaseTask) -> Result<()> {
+        let req = jsonrpc::request(json!("add"), json!([task]));
+        let rep = self.rpc_client.request(req).await.or_else(|e| {
+            error!("Failed sending `add` request to taud: {}", e);
+            return Err(e)
+        })?;
+
+        println!("Got reply: {:?}", rep);
+        Ok(())
+    }
+
+    /// Get all task ids.
+    pub async fn get_ids(&self) -> Result<Vec<u64>> {
+        let req = jsonrpc::request(json!("get_ids"), json!([]));
+        let rep = self.rpc_client.request(req).await.or_else(|e| {
+            error!("Failed sending `get_ids` request to taud: {}", e);
+            return Err(e)
+        })?;
+
+        let mut ret = vec![];
+        for i in rep.as_array().unwrap() {
+            ret.push(i.as_u64().unwrap());
+        }
+
+        Ok(ret)
+    }
+
+    /// Update existing task given it's ID and some params.
+    pub async fn update(&self, id: u64, task: BaseTask) -> Result<()> {
+        let req = jsonrpc::request(json!("update"), json!([id, task]));
+        let rep = self.rpc_client.request(req).await.or_else(|e| {
+            error!("Failed sending `update` request to taud: {}", e);
+            return Err(e)
+        })?;
+
+        println!("Got reply: {:?}", rep);
+        Ok(())
+    }
+
+    /// Set the state for a task.
+    pub async fn set_state(&self, id: u64, state: &str) -> Result<()> {
+        let req = jsonrpc::request(json!("set_state"), json!([id, state]));
+        let rep = self.rpc_client.request(req).await.or_else(|e| {
+            error!("Failed sending `set_state` request to taud: {}", e);
+            return Err(e)
+        })?;
+
+        println!("Got reply: {:?}", rep);
+        Ok(())
+    }
+
+    /// Set a comment for a task.
+    pub async fn set_comment(&self, id: u64, content: &str) -> Result<()> {
+        let req = jsonrpc::request(json!("set_comment"), json!([id, content]));
+        let rep = self.rpc_client.request(req).await.or_else(|e| {
+            error!("Failed sending `set_comment` request to taud: {}", e);
+            return Err(e)
+        })?;
+
+        println!("Got reply: {:?}", rep);
+        Ok(())
+    }
+
+    /// Get task data by its ID.
+    pub async fn get_task_by_id(&self, id: u64) -> Result<TaskInfo> {
+        let req = jsonrpc::request(json!("get_task_by_id"), json!([id]));
+        let rep = self.rpc_client.request(req).await.or_else(|e| {
+            error!("Error sending `get_task_by_id` request: {}", e);
+            return Err(e)
+        })?;
+
+        Ok(serde_json::from_value(rep)?)
+    }
+}

+ 36 - 56
bin/tau/tau-cli/src/util.rs

@@ -1,79 +1,59 @@
-use std::{
-    env::{temp_dir, var},
-    fs::{self, File},
-    io::Read,
-    process::Command,
-};
+use std::{env, fs, process::Command};
 
 use chrono::{Datelike, Local, NaiveDate};
 use log::error;
-use rand::distributions::{Alphanumeric, DistString};
 
-use darkfi::{Error, Result};
-
-pub const CONFIG_FILE: &str = "taud_config.toml";
-pub const CONFIG_FILE_CONTENTS: &str = include_str!("../../taud_config.toml");
+use darkfi::{util::Timestamp, Result};
 
+/// Parse due date (e.g. "1503" for 15 March) as i64 timestamp.
 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());
-
-        let mut year = Local::today().year();
-
-        if month < Local::today().month() {
-            year += 1;
-        }
-
-        if month == Local::today().month() && day < Local::today().day() {
-            year += 1;
-        }
+    if due.len() != 4 {
+        error!("Due date must be of length 4 (e.g. \"1503\" for 15 March)");
+        return None
+    }
+    let (day, month) = (due[..2].parse::<u32>().unwrap(), due[2..].parse::<u32>().unwrap());
 
-        let dt = NaiveDate::from_ymd(year, month, day).and_hms(12, 0, 0);
+    let mut year = Local::today().year();
 
-        return Some(dt.timestamp())
+    if month < Local::today().month() {
+        year += 1;
     }
 
-    if due.len() > 4 {
-        error!("due date must be of length 4 (e.g \"1503\" for 15 March)");
+    if month == Local::today().month() && day < Local::today().day() {
+        year += 1;
     }
 
-    None
+    let dt = NaiveDate::from_ymd(year, month, day).and_hms(12, 0, 0);
+    Some(dt.timestamp())
 }
 
+/// Start up the preferred editor to edit a task's description.
 pub fn desc_in_editor() -> Result<Option<String>> {
-    // Create a temporary file with some comments inside
-    let mut file_path = temp_dir();
-    let file_name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
+    // Create a temporary file with some comments inside.
+    let mut file_path = env::temp_dir();
+    let file_name = format!("tau-{}", Timestamp::current_time().0);
     file_path.push(file_name);
-    fs::write(
-        &file_path,
-        "\n# Write task description above this line\n# These lines will be removed\n",
-    )?;
 
-    // Calling env var {EDITOR} on temp file
-    let editor = match var("EDITOR") {
-        Ok(t) => t,
-        Err(e) => {
-            error!("EDITOR {}", e);
-            return Err(Error::OperationFailed)
-        }
+    fs::write(&file_path, "# Write your task description here.\n")?;
+    fs::write(&file_path, "# Lines starting with \"#\" will be removed\n")?;
+
+    // Try $EDITOR, and if not, fallback to xdg-open.
+    let editor_argv0 = match env::var("EDITOR") {
+        Ok(v) => v,
+        Err(_) => "xdg-open".into(),
     };
-    Command::new(editor).arg(&file_path).status()?;
 
-    // Whatever has been written in temp file, will be read here
-    let mut lines = String::new();
-    File::open(&file_path)?.read_to_string(&mut lines)?;
-    fs::remove_file(file_path)?;
+    Command::new(editor_argv0).arg(&file_path).status()?;
 
-    // Store only non-comment lines
-    let mut description = String::new();
-    for line in lines.split('\n') {
-        if !line.starts_with('#') {
-            description.push_str(line);
-            description.push('\n');
+    // Whatever has been written in the temp file will be read here.
+    let content = fs::read_to_string(&file_path)?;
+    fs::remove_file(&file_path)?;
+
+    let mut lines = vec![];
+    for i in content.lines() {
+        if !i.starts_with('#') {
+            lines.push(format!("{}", i))
         }
     }
-    description.pop();
-
-    Ok(Some(description))
+    Ok(Some(lines.join("\n")))
 }

+ 40 - 43
bin/tau/tau-cli/src/view.rs

@@ -1,40 +1,44 @@
-use prettytable::{cell, format, row, table, Cell, Row, Table};
+use prettytable::{
+    cell,
+    format::{consts::FORMAT_NO_COLSEP, FormatBuilder, LinePosition, LineSeparator},
+    row, table, Cell, Row, Table,
+};
 
 use darkfi::{util::time::timestamp_to_date, Result};
 
-use super::{
+use crate::{
     filter::apply_filter,
-    primitives::{Comment, TaskEvent, TaskInfo},
+    primitives::{Comment, TaskInfo},
+    TaskEvent,
 };
 
-pub fn print_list_of_task(tasks: &mut Vec<TaskInfo>, filters: Vec<String>) -> Result<()> {
-    let mut table = Table::new();
+pub fn print_task_list(tasks: Vec<TaskInfo>, filters: Vec<String>) -> Result<()> {
+    let mut tasks = tasks;
 
+    let mut table = Table::new();
     table.set_format(
-        format::FormatBuilder::new()
+        FormatBuilder::new()
             .padding(1, 1)
-            .separators(
-                &[format::LinePosition::Title],
-                format::LineSeparator::new('─', ' ', ' ', ' '),
-            )
+            .separators(&[LinePosition::Title], LineSeparator::new('-', ' ', ' ', ' '))
             .build(),
     );
-
     table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]);
 
     for filter in filters {
-        apply_filter(tasks, filter);
+        apply_filter(&mut 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 let Some(first) = tasks.first() {
+        max_rank = first.rank;
     }
-    if tasks.last().is_some() {
-        min_rank = tasks.last().unwrap().rank;
+
+    if let Some(last) = tasks.last() {
+        min_rank = last.rank;
     }
 
     for task in tasks {
@@ -63,8 +67,8 @@ pub fn print_list_of_task(tasks: &mut Vec<TaskInfo>, filters: Vec<String>) -> Re
             },
         ]));
     }
-    table.printstd();
 
+    table.printstd();
     Ok(())
 }
 
@@ -72,34 +76,32 @@ 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],
-                            ["title", &taskinfo.title],
-                            [Bd =>"desc", &taskinfo.desc.to_string()],
-                            ["assign", taskinfo.assign.join(", ")],
-                            [Bd =>"project", taskinfo.project.join(", ")],
-                            ["due", due],
-                            [Bd =>"rank", &taskinfo.rank.to_string()],
-                            ["created_at", created_at],
-                            [Bd =>"current_state", current_state]);
+
+    let mut table = table!(
+        [Bd => "ref_id", &taskinfo.ref_id],
+        ["id", &taskinfo.id.to_string()],
+        [Bd => "owner", &taskinfo.owner],
+        ["title", &taskinfo.title],
+        [Bd => "desc", &taskinfo.desc.to_string()],
+        ["assign", taskinfo.assign.join(", ")],
+        [Bd => "project", taskinfo.project.join(", ")],
+        ["due", due],
+        [Bd => "rank", &taskinfo.rank.to_string()],
+        ["created_at", created_at],
+        [Bd => "current_state", current_state]);
 
     table.set_format(
-        format::FormatBuilder::new()
+        FormatBuilder::new()
             .padding(1, 1)
-            .separators(
-                &[format::LinePosition::Title],
-                format::LineSeparator::new('─', ' ', ' ', ' '),
-            )
+            .separators(&[LinePosition::Title], LineSeparator::new('-', ' ', ' ', ' '))
             .build(),
     );
-    table.set_titles(row!["Name", "Value"]);
 
+    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.set_format(*FORMAT_NO_COLSEP);
     event_table.printstd();
 
     Ok(())
@@ -108,8 +110,7 @@ pub fn print_task_info(taskinfo: TaskInfo) -> Result<()> {
 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.push_str(&format!("{}\n", comment));
     }
     comments_str.pop();
     comments_str
@@ -118,11 +119,7 @@ pub fn comments_as_string(comments: Vec<Comment>) -> String {
 pub fn events_as_string(events: Vec<TaskEvent>) -> String {
     let mut events_str = String::new();
     for event in events {
-        events_str.push_str("State changed to ");
-        events_str.push_str(&event.action.to_string());
-        events_str.push_str(" at ");
-        events_str.push_str(&event.timestamp.to_string());
-        events_str.push('\n');
+        events_str.push_str(&format!("State changed to {} at {}\n", event.action, event.timestamp));
     }
     events_str
 }