bin/tau: review taskwarrior CLI and cleanup tau usage

This commit is contained in:
Dastan-glitch
2022-09-01 04:36:20 +03:00
parent e4838c4335
commit eb63d9559c
7 changed files with 348 additions and 109 deletions

View File

@@ -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()),
_ 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 + (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
})
"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)
}
}
}
}
// 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
})
} 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)
}

View File

@@ -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,
/// Set or Get comment for a task.
/// Start task(s).
Start,
/// Open task(s).
Open,
/// 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,50 +186,86 @@ 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::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::Modify { values } => {
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 base_task = task_from_cli(values)?;
for task in tasks {
tau.update(task.id.into(), base_task.clone()).await?;
}
},
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::Info { task_id } => {
let task = tau.get_task_by_id(task_id).await?;
print_task_info(task)
}
TauSubcommand::Switch { workspace } => {
tau.switch_ws(workspace).await?;
Ok(())
}
TauSubcommand::Start => {
if args.filters.is_empty() {
no_filter_warn()
}
let state = State::Start;
for task in tasks {
tau.set_state(task.id.into(), &state).await?;
}
Ok(())
}
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::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::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());
let res = tau.export_to(path.clone()).await?;
@@ -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)
}
}?;

View File

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

View File

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

View File

@@ -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 {
("", "", "", "")
};

View File

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

View File

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