util.rs 12 KB

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