Просмотр исходного кода

bin/tau: review taskwarrior CLI and cleanup tau usage

Dastan-glitch 3 лет назад
Родитель
Сommit
eb63d9559c

+ 177 - 16
bin/tau/tau-cli/src/filter.rs

@@ -1,44 +1,95 @@
-use chrono::{Datelike, NaiveDateTime, Utc};
+use std::{
+    io::{stdin, stdout, Write},
+    process::exit,
+};
+
+use chrono::{Datelike, Local, NaiveDateTime, Utc};
+use log::error;
 use serde_json::Value;
 
-use crate::primitives::{State, TaskInfo};
+use darkfi::Result;
+
+use crate::{
+    primitives::{State, TaskInfo},
+    util::due_as_timestamp,
+};
 
 pub fn apply_filter(tasks: &mut Vec<TaskInfo>, filter: &str) {
     match filter {
-        "open" => tasks.retain(|task| task.state == State::Open.to_string()),
-        "start" => tasks.retain(|task| task.state == State::Start.to_string()),
-        "pause" => tasks.retain(|task| task.state == State::Pause.to_string()),
+        "all" => {}
+        // Filter by state.
+        _ if filter.contains("state:") => {
+            let kv: Vec<&str> = filter.split(':').collect();
+            if let Some(state) = Value::from(kv[1]).as_str() {
+                match state {
+                    "open" => tasks.retain(|task| task.state == State::Open.to_string()),
+                    "start" => tasks.retain(|task| task.state == State::Start.to_string()),
+                    "pause" => tasks.retain(|task| task.state == State::Pause.to_string()),
+                    "stop" => tasks.retain(|task| task.state == State::Stop.to_string()),
+                    _ => {
+                        error!("Not implemented, states are open,start,pause and stop");
+                        exit(1)
+                    }
+                }
+            }
+        }
 
-        _ if filter.len() == 4 && filter.parse::<u32>().is_ok() => {
-            let (month, year) =
-                (filter[..2].parse::<u32>().unwrap(), filter[2..].parse::<i32>().unwrap());
+        // Filter by month
+        _ if filter.contains("month:") => {
+            let kv: Vec<&str> = filter.split(':').collect();
+            if kv.len() == 2 {
+                if let Some(value) = Value::from(kv[1]).as_str() {
+                    if value.len() != 4 || value.parse::<u32>().is_err() {
+                        error!(
+                            "Please provide month date as \"MMYY\" (e.g. 0922 for September 2022)"
+                        );
+                        exit(1)
+                    }
+                    let (month, year) =
+                        (value[..2].parse::<u32>().unwrap(), value[2..].parse::<i32>().unwrap());
 
-            let year = year + (Utc::today().year() / 100) * 100;
-            tasks.retain(|task| {
-                let date = task.created_at;
-                let task_date = NaiveDateTime::from_timestamp(date, 0).date();
-                task_date.month() == month && task_date.year() == year
-            })
+                    let year = year + (Utc::today().year() / 100) * 100;
+                    tasks.retain(|task| {
+                        let date = task.created_at;
+                        let task_date = NaiveDateTime::from_timestamp(date, 0).date();
+                        task_date.month() == month && task_date.year() == year
+                    })
+                } else {
+                    error!("Please provide month date as \"MMYY\" (e.g. 0922 for September 2022)");
+                    exit(1)
+                }
+            }
         }
 
+        // Filter by assignee(s).
         _ if filter.contains("assign:") => {
             let kv: Vec<&str> = filter.split(':').collect();
             if kv.len() == 2 {
                 if let Some(value) = Value::from(kv[1]).as_str() {
-                    tasks.retain(|task| task.assign.contains(&value.to_string()))
+                    if value.is_empty() {
+                        tasks.retain(|task| task.assign.is_empty())
+                    } else {
+                        tasks.retain(|task| task.assign.contains(&value.to_string()))
+                    }
                 }
             }
         }
 
+        // Filter by project(s).
         _ if filter.contains("project:") => {
             let kv: Vec<&str> = filter.split(':').collect();
             if kv.len() == 2 {
                 if let Some(value) = Value::from(kv[1]).as_str() {
-                    tasks.retain(|task| task.project.contains(&value.to_string()))
+                    if value.is_empty() {
+                        tasks.retain(|task| task.project.is_empty())
+                    } else {
+                        tasks.retain(|task| task.project.contains(&value.to_string()))
+                    }
                 }
             }
         }
 
+        // Filter by rank.
         _ if filter.contains("rank:") => {
             let kv: Vec<&str> = filter.split(':').collect();
             if kv.len() == 3 {
@@ -53,8 +104,118 @@ pub fn apply_filter(tasks: &mut Vec<TaskInfo>, filter: &str) {
                     }
                 })
             }
+            tasks.retain(|task| task.rank.is_none())
+        }
+
+        // Filter by due date.
+        _ if filter.contains("due:") || filter.contains("due.") => {
+            let kv: Vec<&str> = filter.split(':').collect();
+            let due_op = if filter.contains('.') {
+                let due_op: Vec<&str> = kv[0].split('.').collect();
+                due_op[1]
+            } else {
+                ""
+            };
+
+            if kv.len() == 2 {
+                if let Some(value) = Value::from(kv[1]).as_str() {
+                    if value.is_empty() {
+                        tasks.retain(|task| task.due.is_none())
+                    } else {
+                        let filter_date = if value == "today" {
+                            Local::today().naive_local()
+                        } else {
+                            let due_date = due_as_timestamp(value).unwrap_or(0);
+                            NaiveDateTime::from_timestamp(due_date, 0).date()
+                        };
+
+                        tasks.retain(|task| {
+                            let date = task.due.unwrap_or(0);
+                            let task_date = NaiveDateTime::from_timestamp(date, 0).date();
+
+                            match due_op {
+                                "not" => task_date != filter_date,
+                                "after" => task_date > filter_date,
+                                "before" => task_date < filter_date,
+                                "" | "is" => task_date == filter_date,
+                                _ => true,
+                            }
+                        })
+                    }
+                } else {
+                    error!("Please provide due date as \"DDMM\" (e.g. 2210 for October 22nd)");
+                    exit(1)
+                }
+            }
+        }
+
+        _ => {
+            println!("No matches.");
+            exit(1)
+        }
+    }
+}
+
+pub fn no_filter_warn() {
+    let mut s = String::new();
+    print!("This command has no filter, and will modify all tasks. Are you sure? (yes/no) ");
+    let _ = stdout().flush();
+    stdin().read_line(&mut s).unwrap_or(0);
+    match s.trim() {
+        "y" | "yes" => {}
+        _ => {
+            println!("Command prevented from running.");
+            exit(1)
         }
+    }
+}
+
+pub fn get_ids(filters: &mut Vec<String>) -> Result<Vec<u64>> {
+    let mut vec_ids = vec![];
+    let mut matching_id = String::new();
+    if let Some(index) = filters.iter().position(|t| {
+        t.parse::<u64>().is_ok() || !t.contains(':') && (t.contains(',') || t.contains('-'))
+    }) {
+        matching_id.push_str(&filters.remove(index));
+    }
 
+    match matching_id {
+        _ if matching_id.parse::<u64>().is_ok() => {
+            let id = matching_id.parse::<u64>().unwrap();
+            vec_ids.push(id)
+        }
+        _ if !matching_id.contains(':') &&
+            (matching_id.contains(',') || matching_id.contains('-')) =>
+        {
+            let num = matching_id.replace(&[',', '-'][..], "");
+            if num.parse::<u64>().is_err() {
+                error!("Invalid ID number");
+                exit(1)
+            }
+            if matching_id.contains(',') {
+                let ids: Vec<&str> = matching_id.split(',').collect();
+                for id in ids {
+                    if id.contains('-') {
+                        let range: Vec<&str> = id.split('-').collect();
+                        let range =
+                            range[0].parse::<u64>().unwrap()..=range[1].parse::<u64>().unwrap();
+                        for rid in range {
+                            vec_ids.push(rid)
+                        }
+                    } else {
+                        vec_ids.push(id.parse::<u64>().unwrap())
+                    }
+                }
+            } else if matching_id.contains('-') {
+                let range: Vec<&str> = matching_id.split('-').collect();
+                let range = range[0].parse::<u64>().unwrap()..=range[1].parse::<u64>().unwrap();
+                for rid in range {
+                    vec_ids.push(rid)
+                }
+            }
+        }
         _ => {}
     }
+
+    Ok(vec_ids)
 }

+ 130 - 59
bin/tau/tau-cli/src/main.rs

@@ -1,4 +1,4 @@
-use std::{process::exit, str::FromStr};
+use std::process::exit;
 
 use clap::{Parser, Subcommand};
 use log::{error, info};
@@ -19,6 +19,7 @@ mod util;
 mod view;
 
 use drawdown::{drawdown, to_naivedate};
+use filter::{apply_filter, get_ids, no_filter_warn};
 use primitives::{task_from_cli, State, TaskEvent};
 use util::{desc_in_editor, due_as_timestamp};
 use view::{comments_as_string, print_task_info, print_task_list};
@@ -27,6 +28,7 @@ const DEFAULT_PATH: &str = "~/tau_exported_tasks";
 
 #[derive(Parser)]
 #[clap(name = "tau", version)]
+#[clap(subcommand_precedence_over_arg = true)]
 struct Args {
     #[clap(short, parse(from_occurrences))]
     /// Increase verbosity (-vvv supported)
@@ -72,32 +74,35 @@ enum TauSubcommand {
         values: Vec<String>,
     },
 
-    /// Update/Edit an existing task by ID.
-    Update {
-        /// Task ID.
-        task_id: u64,
+    /// Modify/Edit an existing task.
+    Modify {
         /// Values (e.g. project:blockchain).
         values: Vec<String>,
     },
 
-    /// Set or Get task state.
-    State {
-        /// Task ID.
-        task_id: u64,
-        /// Set task state if provided (Get state otherwise).
-        state: Option<String>,
-    },
+    /// List tasks.
+    List,
+
+    /// Start task(s).
+    Start,
+
+    /// Open task(s).
+    Open,
 
-    /// Set or Get comment for a task.
+    /// Pause task(s).
+    Pause,
+
+    /// Stop task(s).
+    Stop,
+
+    /// Set or Get comment for task(s).
     Comment {
-        /// Task ID.
-        task_id: u64,
         /// Set comment content if provided (Get comments otherwise).
         content: Vec<String>,
     },
 
-    /// Get task info by ID.
-    Info { task_id: u64 },
+    /// Get all data about selected task(s).
+    Info,
 
     /// Switch workspace.
     Switch {
@@ -120,7 +125,7 @@ enum TauSubcommand {
     /// Log drawdown.
     Log {
         /// The month in which we want to draw a heatmap (e.g. 0822 for August 2022).
-        month: String,
+        month: Option<String>,
         /// The person of which we want to draw a heatmap
         /// (if not provided we list all assignees).
         assignee: Option<String>,
@@ -142,6 +147,28 @@ async fn main() -> Result<()> {
     let rpc_client = RpcClient::new(args.endpoint).await?;
     let tau = Tau { rpc_client };
 
+    let mut filters = args.filters.clone();
+
+    // If IDs are provided in filter we use them to get the tasks from the daemon
+    // then remove IDs from filter so we can do apply_filter() normally.
+    // If not provided we use get_ids() to get them from the daemon.
+    let ids = get_ids(&mut filters)?;
+    let task_ids = if ids.is_empty() { tau.get_ids().await? } else { ids };
+
+    let mut tasks =
+        if filters.contains(&"state:stop".to_string()) || filters.contains(&"all".to_string()) {
+            tau.get_stop_tasks(None).await?
+        } else {
+            vec![]
+        };
+    for id in task_ids {
+        tasks.push(tau.get_task_by_id(id).await?);
+    }
+
+    for filter in filters {
+        apply_filter(&mut tasks, &filter);
+    }
+
     // Parse subcommands
     match args.command {
         Some(sc) => match sc {
@@ -159,49 +186,85 @@ async fn main() -> Result<()> {
                 return tau.add(task).await
             }
 
-            TauSubcommand::Update { task_id, values } => {
-                let task = task_from_cli(values)?;
-                tau.update(task_id, task).await
+            TauSubcommand::Modify { values } => {
+                if args.filters.is_empty() {
+                    no_filter_warn()
+                }
+                let base_task = task_from_cli(values)?;
+                for task in tasks {
+                    tau.update(task.id.into(), base_task.clone()).await?;
+                }
+                Ok(())
             }
 
-            TauSubcommand::State { task_id, state } => match state {
-                Some(state) => {
-                    let state = state.trim().to_lowercase();
-                    if let Ok(st) = State::from_str(&state) {
-                        tau.set_state(task_id, &st).await
-                    } else {
-                        error!("State can only be one of the following: open start stop pause",);
-                        Ok(())
-                    }
+            TauSubcommand::Start => {
+                if args.filters.is_empty() {
+                    no_filter_warn()
                 }
-                None => {
-                    let task = tau.get_task_by_id(task_id).await?;
-                    let state = State::from_str(&task.state)?;
-                    println!("Task {}: {}", task_id, state);
-                    Ok(())
+                let state = State::Start;
+                for task in tasks {
+                    tau.set_state(task.id.into(), &state).await?;
                 }
-            },
+                Ok(())
+            }
 
-            TauSubcommand::Comment { task_id, content } => {
-                if content.is_empty() {
-                    let task = tau.get_task_by_id(task_id).await?;
-                    let comments = comments_as_string(task.comments);
-                    println!("Comments {}:\n{}", task_id, comments);
-                    Ok(())
-                } else {
-                    tau.set_comment(task_id, &content.join(" ")).await
+            TauSubcommand::Open => {
+                if args.filters.is_empty() {
+                    no_filter_warn()
+                }
+                let state = State::Open;
+                for task in tasks {
+                    tau.set_state(task.id.into(), &state).await?;
                 }
+                Ok(())
             }
 
-            TauSubcommand::Info { task_id } => {
-                let task = tau.get_task_by_id(task_id).await?;
-                print_task_info(task)
+            TauSubcommand::Pause => {
+                if args.filters.is_empty() {
+                    no_filter_warn()
+                }
+                let state = State::Pause;
+                for task in tasks {
+                    tau.set_state(task.id.into(), &state).await?;
+                }
+                Ok(())
+            }
+
+            TauSubcommand::Stop => {
+                if args.filters.is_empty() {
+                    no_filter_warn()
+                }
+                let state = State::Stop;
+                for task in tasks {
+                    tau.set_state(task.id.into(), &state).await?;
+                }
+                Ok(())
+            }
+
+            TauSubcommand::Comment { content } => {
+                if args.filters.is_empty() {
+                    no_filter_warn()
+                }
+                for task in tasks {
+                    if content.is_empty() {
+                        let task = tau.get_task_by_id(task.id.into()).await?;
+                        let comments = comments_as_string(task.comments);
+                        println!("Comments {}:\n{}", task.id, comments);
+                    } else {
+                        tau.set_comment(task.id.into(), &content.join(" ")).await?;
+                    }
+                }
+                Ok(())
             }
 
-            TauSubcommand::Switch { workspace } => {
-                tau.switch_ws(workspace).await?;
+            TauSubcommand::Info => {
+                for task in tasks {
+                    let task = tau.get_task_by_id(task.id.into()).await?;
+                    print_task_info(task)?;
+                }
                 Ok(())
             }
+            TauSubcommand::Switch { workspace } => tau.switch_ws(workspace).await,
 
             TauSubcommand::Export { path } => {
                 let path = path.unwrap_or_else(|| DEFAULT_PATH.into());
@@ -230,22 +293,30 @@ async fn main() -> Result<()> {
             }
 
             TauSubcommand::Log { month, assignee } => {
-                let ts = to_naivedate(month.clone())?.and_hms(12, 0, 0).timestamp();
-                let tasks = tau.get_stop_tasks(ts).await?;
-                drawdown(month, tasks, assignee)?;
+                match month {
+                    Some(date) => {
+                        let ts = to_naivedate(date.clone())?.and_hms(12, 0, 0).timestamp();
+                        let tasks = tau.get_stop_tasks(Some(ts)).await?;
+                        drawdown(date, tasks, assignee)?;
+                    }
+                    None => {
+                        let ws = tau.get_ws().await?;
+                        let tasks = tau.get_stop_tasks(None).await?;
+                        print_task_list(tasks, ws)?;
+                    }
+                }
 
                 Ok(())
             }
+
+            TauSubcommand::List => {
+                let ws = tau.get_ws().await?;
+                print_task_list(tasks, ws)
+            }
         },
         None => {
             let ws = tau.get_ws().await?;
-            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?);
-            }
-            print_task_list(tasks, ws, args.filters)?;
-            Ok(())
+            print_task_list(tasks, ws)
         }
     }?;
 

+ 7 - 4
bin/tau/tau-cli/src/primitives.rs

@@ -18,6 +18,9 @@ impl State {
     pub const fn is_pause(&self) -> bool {
         matches!(*self, Self::Pause)
     }
+    pub const fn is_stop(&self) -> bool {
+        matches!(*self, Self::Stop)
+    }
 }
 
 impl fmt::Display for State {
@@ -46,7 +49,7 @@ impl FromStr for State {
     }
 }
 
-#[derive(serde::Serialize, serde::Deserialize, Debug)]
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
 pub struct BaseTask {
     pub title: String,
     pub desc: Option<String>,
@@ -56,7 +59,7 @@ pub struct BaseTask {
     pub rank: Option<f32>,
 }
 
-#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
 pub struct TaskInfo {
     pub ref_id: String,
     pub workspace: String,
@@ -74,7 +77,7 @@ pub struct TaskInfo {
     pub comments: Vec<Comment>,
 }
 
-#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
 pub struct TaskEvent {
     pub action: String,
     pub author: String,
@@ -99,7 +102,7 @@ impl Default for TaskEvent {
     }
 }
 
-#[derive(Clone, serde::Serialize, serde::Deserialize, Debug)]
+#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
 pub struct Comment {
     content: String,
     author: String,

+ 1 - 1
bin/tau/tau-cli/src/rpc.rs

@@ -71,7 +71,7 @@ impl Tau {
     }
 
     /// Get month's stopped tasks.
-    pub async fn get_stop_tasks(&self, month: i64) -> Result<Vec<TaskInfo>> {
+    pub async fn get_stop_tasks(&self, month: Option<i64>) -> Result<Vec<TaskInfo>> {
         let req = JsonRequest::new("get_stop_tasks", json!([month]));
         let rep = self.rpc_client.request(req).await?;
 

+ 16 - 9
bin/tau/tau-cli/src/view.rs

@@ -1,24 +1,22 @@
-use std::{fmt::Write, str::FromStr};
+use std::{cmp::Ordering, fmt::Write, str::FromStr};
 
 use prettytable::{
-    cell,
     format::{consts::FORMAT_NO_COLSEP, FormatBuilder, LinePosition, LineSeparator},
     row, table, Cell, Row, Table,
 };
+use textwrap::fill;
 
 use darkfi::{
     util::time::{timestamp_to_date, DateFormat},
     Result,
 };
-use textwrap::fill;
 
 use crate::{
-    filter::apply_filter,
     primitives::{Comment, State, TaskInfo},
     TaskEvent,
 };
 
-pub fn print_task_list(tasks: Vec<TaskInfo>, ws: String, filters: Vec<String>) -> Result<()> {
+pub fn print_task_list(tasks: Vec<TaskInfo>, ws: String) -> Result<()> {
     let mut tasks = tasks;
 
     let mut table = Table::new();
@@ -30,11 +28,18 @@ pub fn print_task_list(tasks: Vec<TaskInfo>, ws: String, filters: Vec<String>) -
     );
     table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]);
 
-    for filter in filters {
-        apply_filter(&mut tasks, &filter);
-    }
+    // group tasks by state.
+    tasks.sort_by_key(|task| task.state.clone());
 
-    tasks.sort_by(|a, b| b.rank.partial_cmp(&a.rank).unwrap());
+    // sort tasks by there rank only if they are not stopped.
+    tasks.sort_by(|a, b| {
+        if a.state != "stop" && b.state != "stop" {
+            b.rank.partial_cmp(&a.rank).unwrap()
+        } else {
+            // because sort_by does not reorder equal elements
+            Ordering::Equal
+        }
+    });
 
     let mut min_rank = None;
     let mut max_rank = None;
@@ -54,6 +59,8 @@ pub fn print_task_list(tasks: Vec<TaskInfo>, ws: String, filters: Vec<String>) -
             ("bFg", "Fc", "Fg", "Fg")
         } else if state.is_pause() {
             ("iFYBd", "iFYBd", "iFYBd", "iFYBd")
+        } else if state.is_stop() {
+            ("Fr", "Fr", "Fr", "Fr")
         } else {
             ("", "", "", "")
         };

+ 4 - 7
bin/tau/taud/src/jsonrpc.rs

@@ -238,21 +238,18 @@ impl JsonRpcInterface {
 
     // RPCAPI:
     // Get all tasks.
-    // --> {"jsonrpc": "2.0", "method": "get_all_tasks", "params": [task_id], "id": 1}
+    // --> {"jsonrpc": "2.0", "method": "get_stop_tasks", "params": [task_id], "id": 1}
     // <-- {"jsonrpc": "2.0", "result": "task", "id": 1}
     async fn get_stop_tasks(&self, params: &[Value]) -> TaudResult<Value> {
-        debug!(target: "tau", "JsonRpc::get_all_tasks() params {:?}", params);
+        debug!(target: "tau", "JsonRpc::get_stop_tasks() params {:?}", params);
 
         if params.len() != 1 {
             return Err(TaudError::InvalidData("len of params should be 1".into()))
         }
-        if !params[0].is_i64() {
-            return Err(TaudError::InvalidData("Invalid month".into()))
-        }
-        let month = Timestamp(params[0].as_i64().unwrap());
+        let month = params[0].as_i64().map(Timestamp);
         let ws = self.workspace.lock().await.clone();
 
-        let tasks = MonthTasks::load_stop_tasks(&self.dataset_path, ws, &month)?;
+        let tasks = MonthTasks::load_stop_tasks(&self.dataset_path, ws, month.as_ref())?;
 
         Ok(json!(tasks))
     }

+ 2 - 2
bin/tau/taud/src/month_tasks.rs

@@ -161,9 +161,9 @@ impl MonthTasks {
     pub fn load_stop_tasks(
         dataset_path: &Path,
         ws: String,
-        date: &Timestamp,
+        date: Option<&Timestamp>,
     ) -> TaudResult<Vec<TaskInfo>> {
-        let mt = Self::load_or_create(Some(date), dataset_path)?;
+        let mt = Self::load_or_create(date, dataset_path)?;
         Ok(mt
             .objects(dataset_path)?
             .into_iter()