util.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. use std::{
  2. env::{temp_dir, var},
  3. fs::{self, File},
  4. io::{self, Read, Write},
  5. net::SocketAddr,
  6. ops::Index,
  7. process::Command,
  8. };
  9. use chrono::{Datelike, Local, NaiveDate, NaiveDateTime};
  10. use clap::Subcommand;
  11. use log::error;
  12. use prettytable::{cell, format, row, Cell, Row, Table};
  13. use rand::distributions::{Alphanumeric, DistString};
  14. use serde::{Deserialize, Serialize};
  15. use serde_json::Value;
  16. use darkfi::{Error, Result};
  17. use structopt::StructOpt;
  18. use structopt_toml::StructOptToml;
  19. pub const CONFIG_FILE: &str = "taud_config.toml";
  20. pub const CONFIG_FILE_CONTENTS: &str = include_str!("../../taud_config.toml");
  21. #[derive(Subcommand, Deserialize, Debug, StructOpt)]
  22. pub enum CliTauSubCommands {
  23. /// Add a new task
  24. Add {
  25. /// Specify task title
  26. #[clap(short, long)]
  27. title: Option<String>,
  28. /// Specify task description
  29. #[clap(long)]
  30. desc: Option<String>,
  31. /// Assign task to user
  32. #[clap(short, long)]
  33. assign: Option<String>,
  34. /// Task project (can be hierarchical: crypto.zk)
  35. #[clap(short, long)]
  36. project: Option<String>,
  37. /// Due date in DDMM format: "2202" for 22 Feb
  38. #[clap(short, long)]
  39. due: Option<String>,
  40. /// Project rank single precision decimal real value: 4.8761
  41. #[clap(short, long)]
  42. rank: Option<f32>,
  43. },
  44. /// Update/Edit an existing task by ID
  45. Update {
  46. /// Task ID
  47. id: u64,
  48. /// Field's name (ex title)
  49. key: String,
  50. /// New value
  51. value: String,
  52. },
  53. /// Set task state
  54. SetState {
  55. /// Task ID
  56. id: u64,
  57. /// Set task state
  58. state: String,
  59. },
  60. /// Get task state
  61. GetState {
  62. /// Task ID
  63. id: u64,
  64. },
  65. /// Set comment for a task
  66. SetComment {
  67. /// Task ID
  68. id: u64,
  69. /// Comment author
  70. author: String,
  71. /// Comment content
  72. content: String,
  73. },
  74. /// Get task's comments
  75. GetComment {
  76. /// Task ID
  77. id: u64,
  78. },
  79. /// List open tasks
  80. List {},
  81. /// Get task by ID
  82. Get {
  83. /// Task ID
  84. id: u64,
  85. },
  86. }
  87. #[derive(Debug, Clone, Deserialize, Serialize)]
  88. pub struct TaskInfo {
  89. pub ref_id: String,
  90. pub id: u32,
  91. pub title: String,
  92. pub desc: String,
  93. pub assign: Vec<String>,
  94. pub project: Vec<String>,
  95. pub due: Option<i64>,
  96. pub rank: f32,
  97. pub created_at: i64,
  98. pub events: Vec<Value>,
  99. pub comments: Vec<Value>,
  100. }
  101. /// Tau cli
  102. #[derive(Debug, Deserialize, StructOpt, StructOptToml)]
  103. #[serde(default)]
  104. #[structopt(name = "tau")]
  105. pub struct CliTau {
  106. /// Increase verbosity
  107. #[structopt(short, parse(from_occurrences))]
  108. pub verbose: u8,
  109. /// JSON-RPC listen URL
  110. #[structopt(long = "rpc", default_value = "127.0.0.1:11055")]
  111. pub rpc_listen: SocketAddr,
  112. /// Sets a custom config file
  113. #[structopt(short, long)]
  114. pub config: Option<String>,
  115. #[structopt(subcommand)]
  116. pub command: Option<CliTauSubCommands>,
  117. #[structopt(multiple = true)]
  118. /// Search criteria (zero or more)
  119. pub filters: Vec<String>,
  120. }
  121. pub fn due_as_timestamp(due: &str) -> Option<i64> {
  122. if due.len() == 4 {
  123. let (day, month) = (due[..2].parse::<u32>().unwrap(), due[2..].parse::<u32>().unwrap());
  124. let mut year = Local::today().year();
  125. if month < Local::today().month() {
  126. year += 1;
  127. }
  128. if month == Local::today().month() && day < Local::today().day() {
  129. year += 1;
  130. }
  131. let dt = NaiveDate::from_ymd(year, month, day).and_hms(12, 0, 0);
  132. return Some(dt.timestamp())
  133. }
  134. if due.len() > 4 {
  135. error!("due date must be of length 4 (e.g \"1503\" for 15 March)");
  136. }
  137. None
  138. }
  139. pub fn set_title() -> Result<String> {
  140. print!("Title: ");
  141. io::stdout().flush()?;
  142. let mut t = String::new();
  143. io::stdin().read_line(&mut t)?;
  144. if t.is_empty() {
  145. error!("You can't have a task without a title");
  146. return Err(Error::OperationFailed)
  147. }
  148. if &t[(t.len() - 1)..] == "\n" {
  149. t.pop();
  150. }
  151. Ok(t)
  152. }
  153. pub fn desc_in_editor() -> Result<Option<String>> {
  154. // Create a temporary file with some comments inside
  155. let mut file_path = temp_dir();
  156. let file_name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
  157. file_path.push(file_name);
  158. fs::write(
  159. &file_path,
  160. "\n# Write task description above this line\n# These lines will be removed\n",
  161. )?;
  162. // Calling env var {EDITOR} on temp file
  163. let editor = match var("EDITOR") {
  164. Ok(t) => t,
  165. Err(e) => {
  166. error!("EDITOR {}", e);
  167. return Err(Error::OperationFailed)
  168. }
  169. };
  170. Command::new(editor).arg(&file_path).status()?;
  171. // Whatever has been written in temp file, will be read here
  172. let mut lines = String::new();
  173. File::open(&file_path)?.read_to_string(&mut lines)?;
  174. fs::remove_file(file_path)?;
  175. // Store only non-comment lines
  176. let mut description = String::new();
  177. for line in lines.split('\n') {
  178. if !line.starts_with('#') {
  179. description.push_str(line);
  180. description.push('\n');
  181. }
  182. }
  183. description.pop();
  184. Ok(Some(description))
  185. }
  186. pub fn get_comments(rep: Value) -> Result<String> {
  187. let task: Value = serde_json::from_value(rep)?;
  188. let comments: Vec<Value> = serde_json::from_value(task["comments"].clone())?;
  189. let mut result = String::new();
  190. for comment in comments {
  191. result.push_str(comment["author"].as_str().ok_or(Error::OperationFailed)?);
  192. result.push_str(": ");
  193. result.push_str(comment["content"].as_str().ok_or(Error::OperationFailed)?);
  194. result.push('\n');
  195. }
  196. result.pop();
  197. Ok(result)
  198. }
  199. pub fn get_events(rep: Value) -> Result<String> {
  200. let task: Value = serde_json::from_value(rep)?;
  201. let events: Vec<Value> = serde_json::from_value(task["events"].clone())?;
  202. let mut ev = String::new();
  203. for event in events {
  204. ev.push_str("State changed to ");
  205. ev.push_str(event["action"].as_str().ok_or(Error::OperationFailed)?);
  206. ev.push_str(" at ");
  207. ev.push_str(&timestamp_to_date(event["timestamp"].clone(), "datetime"));
  208. ev.push('\n');
  209. }
  210. ev.pop();
  211. Ok(ev)
  212. }
  213. pub fn timestamp_to_date(timestamp: Value, dt: &str) -> String {
  214. let timestamp = timestamp.as_i64().unwrap_or(0);
  215. if timestamp <= 0 {
  216. return "".to_string()
  217. }
  218. match dt {
  219. "date" => {
  220. NaiveDateTime::from_timestamp(timestamp, 0).date().format("%A %-d %B").to_string()
  221. }
  222. "datetime" => {
  223. NaiveDateTime::from_timestamp(timestamp, 0).format("%H:%M %A %-d %B").to_string()
  224. }
  225. _ => "".to_string(),
  226. }
  227. }
  228. pub fn get_from_task(task: Value, value: &str) -> Result<String> {
  229. let vec_values: Vec<Value> = serde_json::from_value(task[value].clone())?;
  230. let mut result = String::new();
  231. for (i, _) in vec_values.iter().enumerate() {
  232. if !result.is_empty() {
  233. result.push(',');
  234. }
  235. result.push_str(vec_values.index(i).as_str().unwrap());
  236. }
  237. Ok(result)
  238. }
  239. // Helper function to check task's state
  240. fn check_task_state(task: &Value, state: &str) -> bool {
  241. let mut default_events = serde_json::Map::new();
  242. default_events.insert("action".into(), "open".into());
  243. let default_events: Vec<Value> = vec![Value::from(default_events)];
  244. let last_event = task["events"].as_array().unwrap_or(&default_events).last().unwrap();
  245. let last_state = last_event["action"].as_str().unwrap();
  246. state == last_state
  247. }
  248. fn apply_filter(tasks: Vec<Value>, filter: String) -> Result<Vec<Value>> {
  249. let filtered_tasks: Vec<Value> = match filter.as_str() {
  250. "open" => tasks.into_iter().filter(|task| check_task_state(task, "open")).collect(),
  251. "pause" => tasks.into_iter().filter(|task| check_task_state(task, "pause")).collect(),
  252. "stop" => tasks.into_iter().filter(|task| check_task_state(task, "stop")).collect(),
  253. "month" => tasks
  254. .into_iter()
  255. .filter(|task| {
  256. let date = task["created_at"].as_i64().unwrap();
  257. let task_month = NaiveDateTime::from_timestamp(date, 0).month();
  258. let this_month = Local::today().month();
  259. task_month == this_month
  260. })
  261. .collect(),
  262. _ if filter.contains("assign:") | filter.contains("project:") => {
  263. let kv: Vec<&str> = filter.split(':').collect();
  264. let key = kv[0];
  265. let value = Value::from(kv[1]);
  266. tasks
  267. .into_iter()
  268. .filter(|task| task[key].as_array().unwrap_or(&vec![]).contains(&value))
  269. .collect()
  270. }
  271. _ if filter.contains("rank>") | filter.contains("rank<") => {
  272. let kv: Vec<&str> = if filter.contains('>') {
  273. filter.split('>').collect()
  274. } else {
  275. filter.split('<').collect()
  276. };
  277. let key = kv[0];
  278. let value = kv[1].parse::<f32>()?;
  279. tasks
  280. .into_iter()
  281. .filter(|task| {
  282. let rank = task[key].as_f64().unwrap_or(0.0) as f32;
  283. if filter.contains('>') {
  284. rank > value
  285. } else {
  286. rank < value
  287. }
  288. })
  289. .collect()
  290. }
  291. _ => tasks,
  292. };
  293. Ok(filtered_tasks)
  294. }
  295. pub fn list_tasks(rep: Value, filters: Vec<String>) -> Result<()> {
  296. let mut table = Table::new();
  297. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  298. table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]);
  299. let mut tasks: Vec<Value> = serde_json::from_value(rep)?;
  300. for filter in filters {
  301. // TODO need to use iterator or reference instead of copy
  302. tasks = apply_filter(tasks, filter)?;
  303. }
  304. tasks.sort_by(|a, b| b["rank"].as_f64().partial_cmp(&a["rank"].as_f64()).unwrap());
  305. let (max_rank, min_rank) = if !tasks.is_empty() {
  306. (
  307. serde_json::from_value(tasks[0]["rank"].clone())?,
  308. serde_json::from_value(tasks[tasks.len() - 1]["rank"].clone())?,
  309. )
  310. } else {
  311. (0.0, 0.0)
  312. };
  313. for task in tasks {
  314. let events: Vec<Value> = serde_json::from_value(task["events"].clone())?;
  315. let state = match events.last() {
  316. Some(s) => s["action"].as_str().unwrap(),
  317. None => "open",
  318. };
  319. let rank = task["rank"].as_f64().unwrap_or(0.0) as f32;
  320. let (max_style, min_style, mid_style, gen_style) = if state == "open" {
  321. ("bFC", "Fb", "Fc", "")
  322. } else {
  323. ("iFYBd", "iFYBd", "iFYBd", "iFYBd")
  324. };
  325. table.add_row(Row::new(vec![
  326. Cell::new(&task["id"].to_string()).style_spec(gen_style),
  327. Cell::new(task["title"].as_str().unwrap()).style_spec(gen_style),
  328. Cell::new(&get_from_task(task.clone(), "project")?).style_spec(gen_style),
  329. Cell::new(&get_from_task(task.clone(), "assign")?).style_spec(gen_style),
  330. Cell::new(&timestamp_to_date(task["due"].clone(), "date")).style_spec(gen_style),
  331. if rank == max_rank {
  332. Cell::new(&rank.to_string()).style_spec(max_style)
  333. } else if rank == min_rank {
  334. Cell::new(&rank.to_string()).style_spec(min_style)
  335. } else {
  336. Cell::new(&rank.to_string()).style_spec(mid_style)
  337. },
  338. ]));
  339. }
  340. table.printstd();
  341. Ok(())
  342. }