main.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. use std::{
  2. env::{temp_dir, var},
  3. fs::{self, File},
  4. io,
  5. io::{Read, Write},
  6. ops::Index,
  7. process::Command,
  8. };
  9. use chrono::{Datelike, Local, NaiveDate, NaiveDateTime};
  10. use clap::{CommandFactory, Parser, Subcommand};
  11. use log::{debug, error};
  12. use prettytable::{cell, format, row, Table};
  13. use serde_json::{json, Value};
  14. use simplelog::{ColorChoice, TermLogger, TerminalMode};
  15. use url::Url;
  16. use darkfi::{
  17. rpc::jsonrpc::{self, JsonResult},
  18. util::cli::log_config,
  19. Error, Result,
  20. };
  21. #[derive(Subcommand)]
  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
  41. #[clap(short, long)]
  42. rank: Option<u32>,
  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. }
  82. /// Tau cli
  83. #[derive(Parser)]
  84. #[clap(name = "tau")]
  85. #[clap(author, version, about)]
  86. pub struct CliTau {
  87. /// Increase verbosity
  88. #[clap(short, parse(from_occurrences))]
  89. pub verbose: u8,
  90. #[clap(subcommand)]
  91. pub command: Option<CliTauSubCommands>,
  92. }
  93. fn due_as_timestamp(due: &str) -> Option<i64> {
  94. if due.len() == 4 {
  95. let (day, month) = (due[..2].parse::<u32>().unwrap(), due[2..].parse::<u32>().unwrap());
  96. let mut year = Local::today().year();
  97. if month < Local::today().month() {
  98. year += 1;
  99. }
  100. if month == Local::today().month() && day < Local::today().day() {
  101. year += 1;
  102. }
  103. let dt = NaiveDate::from_ymd(year, month, day).and_hms(12, 0, 0);
  104. return Some(dt.timestamp())
  105. }
  106. if due.len() > 4 {
  107. error!("due date must be of length 4 (e.g \"1503\" for 15 March)");
  108. }
  109. None
  110. }
  111. async fn request(r: jsonrpc::JsonRequest, url: String) -> Result<Value> {
  112. let reply: JsonResult = match jsonrpc::send_request(&Url::parse(&url)?, json!(r), None).await {
  113. Ok(v) => v,
  114. Err(e) => return Err(e),
  115. };
  116. match reply {
  117. JsonResult::Resp(r) => {
  118. debug!(target: "RPC", "<-- {}", serde_json::to_string(&r)?);
  119. Ok(r.result)
  120. }
  121. JsonResult::Err(e) => {
  122. debug!(target: "RPC", "<-- {}", serde_json::to_string(&e)?);
  123. Err(Error::JsonRpcError(e.error.message.to_string()))
  124. }
  125. JsonResult::Notif(n) => {
  126. debug!(target: "RPC", "<-- {}", serde_json::to_string(&n)?);
  127. Err(Error::JsonRpcError("Unexpected reply".to_string()))
  128. }
  129. }
  130. }
  131. // Add new task and returns `true` upon success.
  132. // --> {"jsonrpc": "2.0", "method": "add", "params": ["title", "desc", ["assign"], ["project"], "due", "rank"], "id": 1}
  133. // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
  134. async fn add(url: &str, params: Value) -> Result<Value> {
  135. let req = jsonrpc::request(json!("add"), params);
  136. request(req, url.to_string()).await
  137. }
  138. // List tasks
  139. // --> {"jsonrpc": "2.0", "method": "list", "params": [], "id": 1}
  140. // <-- {"jsonrpc": "2.0", "result": [task, ...], "id": 1}
  141. async fn list(url: &str, params: Value) -> Result<Value> {
  142. let req = jsonrpc::request(json!("list"), json!(params));
  143. request(req, url.to_string()).await
  144. }
  145. // Update task and returns `true` upon success.
  146. // --> {"jsonrpc": "2.0", "method": "update", "params": [task_id, {"title": "new title"} ], "id": 1}
  147. // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
  148. async fn update(url: &str, id: u64, data: Value) -> Result<Value> {
  149. let req = jsonrpc::request(json!("update"), json!([id, data]));
  150. request(req, url.to_string()).await
  151. }
  152. // Set state for a task and returns `true` upon success.
  153. // --> {"jsonrpc": "2.0", "method": "set_state", "params": [task_id, state], "id": 1}
  154. // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
  155. async fn set_state(url: &str, id: u64, state: &str) -> Result<Value> {
  156. let req = jsonrpc::request(json!("set_state"), json!([id, state]));
  157. request(req, url.to_string()).await
  158. }
  159. // Get task's state.
  160. // --> {"jsonrpc": "2.0", "method": "get_state", "params": [task_id], "id": 1}
  161. // <-- {"jsonrpc": "2.0", "result": "state", "id": 1}
  162. async fn get_state(url: &str, id: u64) -> Result<Value> {
  163. let req = jsonrpc::request(json!("get_state"), json!([id]));
  164. request(req, url.to_string()).await
  165. }
  166. // Set comment for a task and returns `true` upon success.
  167. // --> {"jsonrpc": "2.0", "method": "set_comment", "params": [task_id, comment_author, comment_content], "id": 1}
  168. // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
  169. async fn set_comment(url: &str, id: u64, author: &str, content: &str) -> Result<Value> {
  170. let req = jsonrpc::request(json!("set_comment"), json!([id, author, content]));
  171. request(req, url.to_string()).await
  172. }
  173. async fn start(options: CliTau) -> Result<()> {
  174. let rpc_addr = "tcp://127.0.0.1:8875";
  175. match options.command {
  176. Some(CliTauSubCommands::Add { title, desc, assign, project, due, rank }) => {
  177. let title = if title.is_none() {
  178. print!("Title: ");
  179. io::stdout().flush()?;
  180. let mut t = String::new();
  181. io::stdin().read_line(&mut t)?;
  182. if &t[(t.len() - 1)..] == "\n" {
  183. t.pop();
  184. }
  185. Some(t)
  186. } else {
  187. title
  188. };
  189. let desc = if desc.is_none() {
  190. let editor = match var("EDITOR") {
  191. Ok(t) => t,
  192. Err(e) => {
  193. error!("EDITOR {}", e);
  194. return Err(Error::BadOperationType)
  195. }
  196. };
  197. let mut file_path = temp_dir();
  198. file_path.push("temp_file");
  199. File::create(&file_path)?;
  200. fs::write(
  201. &file_path,
  202. "\n# Write task description above this line\n# These lines will be removed\n",
  203. )?;
  204. Command::new(editor).arg(&file_path).status()?;
  205. let mut lines = String::new();
  206. File::open(file_path)?.read_to_string(&mut lines)?;
  207. let mut description = String::new();
  208. for line in lines.split('\n') {
  209. if !line.starts_with('#') {
  210. description.push_str(line)
  211. }
  212. }
  213. Some(description)
  214. } else {
  215. desc
  216. };
  217. let assign: Vec<String> = match assign {
  218. Some(a) => a.split(',').map(|s| s.into()).collect(),
  219. None => vec![],
  220. };
  221. let project: Vec<String> = match project {
  222. Some(p) => p.split(',').map(|s| s.into()).collect(),
  223. None => vec![],
  224. };
  225. let due = match due {
  226. Some(d) => due_as_timestamp(&d),
  227. None => None,
  228. };
  229. let rank = rank.unwrap_or(0);
  230. add(rpc_addr, json!([title, desc, assign, project, due, rank])).await?;
  231. }
  232. Some(CliTauSubCommands::List {}) => {
  233. let rep = list(rpc_addr, json!([])).await?;
  234. let mut table = Table::new();
  235. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  236. table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]);
  237. let mut tasks = rep.as_array().unwrap().to_owned();
  238. tasks.sort_by(|a, b| b["rank"].as_u64().cmp(&a["rank"].as_u64()));
  239. let max_rank = if !tasks.is_empty() { tasks[0]["rank"].as_u64().unwrap() } else { 0 };
  240. for task in tasks {
  241. let project = task["project"].as_array().unwrap();
  242. let mut projects = String::new();
  243. for (i, _) in project.iter().enumerate() {
  244. if !projects.is_empty() {
  245. projects.push(',');
  246. }
  247. projects.push_str(project.index(i).as_str().unwrap());
  248. }
  249. let assign = task["assign"].as_array().unwrap();
  250. let mut asgn = String::new();
  251. for (i, _) in assign.iter().enumerate() {
  252. if !asgn.is_empty() {
  253. asgn.push(',');
  254. }
  255. asgn.push_str(assign.index(i).as_str().unwrap());
  256. }
  257. let date = if task["due"].is_u64() {
  258. let due = task["due"].as_i64().unwrap();
  259. NaiveDateTime::from_timestamp(due, 0).date().format("%A %-d %B").to_string()
  260. } else {
  261. "".to_string()
  262. };
  263. if task["rank"].as_u64().unwrap() == max_rank {
  264. table.add_row(row![
  265. task["id"],
  266. task["title"].as_str().unwrap(),
  267. projects,
  268. asgn,
  269. date,
  270. bFC->task["rank"]
  271. ]);
  272. } else {
  273. table.add_row(row![
  274. task["id"],
  275. task["title"].as_str().unwrap(),
  276. projects,
  277. asgn,
  278. date,
  279. Fb->task["rank"]
  280. ]);
  281. }
  282. }
  283. table.printstd();
  284. }
  285. Some(CliTauSubCommands::Update { id, key, value }) => {
  286. let value = value.as_str().trim();
  287. let updated_value: Value = match key.as_str() {
  288. "due" => {
  289. json!(due_as_timestamp(value))
  290. }
  291. "rank" => {
  292. json!(value.parse::<u64>()?)
  293. }
  294. "project" | "assign" => {
  295. json!(value.split(',').collect::<Vec<&str>>())
  296. }
  297. _ => {
  298. json!(value)
  299. }
  300. };
  301. update(rpc_addr, id, json!({ key: updated_value })).await?;
  302. }
  303. Some(CliTauSubCommands::SetState { id, state }) => {
  304. set_state(rpc_addr, id, state.trim()).await?;
  305. }
  306. Some(CliTauSubCommands::GetState { id }) => {
  307. let state = get_state(rpc_addr, id).await?;
  308. println!("Task with id: {} is {}", id, state);
  309. }
  310. Some(CliTauSubCommands::SetComment { id, author, content }) => {
  311. set_comment(rpc_addr, id, author.trim(), content.trim()).await?;
  312. }
  313. Some(CliTauSubCommands::GetComment { id }) => {
  314. let rep = list(rpc_addr, json!([])).await?;
  315. let tasks = rep.as_array().unwrap();
  316. if tasks.iter().any(|x| x["id"].as_u64().unwrap() == id) {
  317. let index: usize = (id - 1).try_into().unwrap();
  318. let comments = tasks[index]["comments"].as_array().unwrap();
  319. let mut cmnt = String::new();
  320. for comment in comments {
  321. cmnt.push_str(comment["author"].as_str().unwrap());
  322. cmnt.push_str(": ");
  323. cmnt.push_str(comment["content"].as_str().unwrap());
  324. cmnt.push('\n');
  325. }
  326. println!("Comments on Task with id {}:\n{}", id, cmnt);
  327. }
  328. }
  329. _ => {
  330. error!("Please run 'tau help' to see usage.");
  331. return Err(Error::MissingParams)
  332. }
  333. }
  334. Ok(())
  335. }
  336. #[async_std::main]
  337. async fn main() -> Result<()> {
  338. let args = CliTau::parse();
  339. let matches = CliTau::command().get_matches();
  340. let verbosity_level = matches.occurrences_of("verbose");
  341. let (lvl, conf) = log_config(verbosity_level)?;
  342. TermLogger::init(lvl, conf, TerminalMode::Mixed, ColorChoice::Auto)?;
  343. start(args).await
  344. }