util.rs 12 KB

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