main.rs 12 KB

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