main.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. // TODO: This whole code needs refactoring, clean-up and comments
  2. use std::{
  3. env::{temp_dir, var},
  4. fs::{self, File},
  5. io,
  6. io::{Read, Write},
  7. ops::Index,
  8. process::Command,
  9. };
  10. use chrono::{Datelike, Local, NaiveDate, NaiveDateTime};
  11. use clap::{CommandFactory, Parser, Subcommand};
  12. use log::{debug, error};
  13. use prettytable::{cell, format, row, Table};
  14. use serde_json::{json, Value};
  15. use simplelog::{ColorChoice, TermLogger, TerminalMode};
  16. use url::Url;
  17. use darkfi::{
  18. rpc::jsonrpc::{self, JsonResult},
  19. util::cli::log_config,
  20. Error, Result,
  21. };
  22. #[derive(Subcommand)]
  23. pub enum CliTauSubCommands {
  24. /// Add a new task
  25. Add {
  26. /// Specify task title
  27. #[clap(short, long)]
  28. title: Option<String>,
  29. /// Specify task description
  30. #[clap(long)]
  31. desc: Option<String>,
  32. /// Assign task to user
  33. #[clap(short, long)]
  34. assign: Option<String>,
  35. /// Task project (can be hierarchical: crypto.zk)
  36. #[clap(short, long)]
  37. project: Option<String>,
  38. /// Due date in DDMM format: "2202" for 22 Feb
  39. #[clap(short, long)]
  40. due: Option<String>,
  41. /// Project rank
  42. #[clap(short, long)]
  43. rank: Option<u32>,
  44. },
  45. /// List open tasks
  46. List {
  47. /// Month tasks
  48. #[clap(short, long)]
  49. month: Option<String>,
  50. },
  51. /// Update/Edit an existing task by ID
  52. Update {
  53. /// Task ID
  54. #[clap(short, long)]
  55. id: Option<u64>,
  56. /// Data you want to change: "title: new title"
  57. #[clap(short, long)]
  58. data: Option<String>,
  59. },
  60. /// Set task state
  61. SetState {
  62. /// Task ID
  63. #[clap(short, long)]
  64. id: u64,
  65. /// Set task state
  66. #[clap(short, long)]
  67. state: String,
  68. },
  69. /// Get task state
  70. GetState {
  71. /// Task ID
  72. #[clap(short, long)]
  73. id: u64,
  74. },
  75. /// Set comment for a task
  76. SetComment {
  77. /// Task ID
  78. #[clap(short, long)]
  79. id: u64,
  80. /// Comment author
  81. #[clap(short, long)]
  82. author: String,
  83. /// Comment content
  84. #[clap(short, long)]
  85. content: String,
  86. },
  87. }
  88. /// Tau cli
  89. #[derive(Parser)]
  90. #[clap(name = "tau")]
  91. #[clap(author, version, about)]
  92. pub struct CliTau {
  93. /// Increase verbosity
  94. #[clap(short, parse(from_occurrences))]
  95. pub verbose: u8,
  96. #[clap(subcommand)]
  97. pub command: Option<CliTauSubCommands>,
  98. }
  99. fn due_as_timestamp(due: Option<String>) -> Option<i64> {
  100. match due {
  101. Some(du) => match du.len() {
  102. 0 => None,
  103. 4 => {
  104. let (day, month) =
  105. (du[..2].parse::<u32>().unwrap(), du[2..].parse::<u32>().unwrap());
  106. let mut year = Local::today().year();
  107. if month < Local::today().month() {
  108. year += 1;
  109. }
  110. if month == Local::today().month() && day < Local::today().day() {
  111. year += 1;
  112. }
  113. let dt = NaiveDate::from_ymd(year, month, day).and_hms(12, 0, 0);
  114. Some(dt.timestamp())
  115. }
  116. _ => {
  117. error!("due date must be of length 4 (e.g \"1503\" for 15 March)");
  118. None
  119. }
  120. },
  121. None => None,
  122. }
  123. }
  124. async fn request(r: jsonrpc::JsonRequest, url: String) -> Result<Value> {
  125. let reply: JsonResult = match jsonrpc::send_request(&Url::parse(&url)?, json!(r), None).await {
  126. Ok(v) => v,
  127. Err(e) => return Err(e),
  128. };
  129. match reply {
  130. JsonResult::Resp(r) => {
  131. debug!(target: "RPC", "<-- {}", serde_json::to_string(&r)?);
  132. Ok(r.result)
  133. }
  134. JsonResult::Err(e) => {
  135. debug!(target: "RPC", "<-- {}", serde_json::to_string(&e)?);
  136. Err(Error::JsonRpcError(e.error.message.to_string()))
  137. }
  138. JsonResult::Notif(n) => {
  139. debug!(target: "RPC", "<-- {}", serde_json::to_string(&n)?);
  140. Err(Error::JsonRpcError("Unexpected reply".to_string()))
  141. }
  142. }
  143. }
  144. // List tasks
  145. // --> {"jsonrpc": "2.0", "method": "list", "params": [month_date], "id": 1}
  146. // <-- {"jsonrpc": "2.0", "result": [task, ...], "id": 1}
  147. async fn list(url: &str, month: Option<i64>) -> Result<Value> {
  148. let req = jsonrpc::request(json!("list"), json!([month]));
  149. Ok(request(req, url.to_string()).await?)
  150. }
  151. // Update task and returns `true` upon success.
  152. // --> {"jsonrpc": "2.0", "method": "update", "params": [task_id, {"title": "new title"} ], "id": 1}
  153. // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
  154. async fn update(url: &str, id: Option<u64>, data: Value) -> Result<Value> {
  155. let req = jsonrpc::request(json!("update"), json!([id, data]));
  156. Ok(request(req, url.to_string()).await?)
  157. }
  158. // Set state for a task and returns `true` upon success.
  159. // --> {"jsonrpc": "2.0", "method": "set_state", "params": [task_id, state], "id": 1}
  160. // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
  161. async fn set_state(url: &str, id: u64, state: &str) -> Result<Value> {
  162. let req = jsonrpc::request(json!("set_state"), json!([id, state]));
  163. Ok(request(req, url.to_string()).await?)
  164. }
  165. // Get task's state.
  166. // --> {"jsonrpc": "2.0", "method": "get_state", "params": [task_id], "id": 1}
  167. // <-- {"jsonrpc": "2.0", "result": "state", "id": 1}
  168. async fn get_state(url: &str, id: u64) -> Result<Value> {
  169. let req = jsonrpc::request(json!("get_state"), json!([id]));
  170. Ok(request(req, url.to_string()).await?)
  171. }
  172. // Set comment for a task and returns `true` upon success.
  173. // --> {"jsonrpc": "2.0", "method": "set_comment", "params": [task_id, comment_author, comment_content], "id": 1}
  174. // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
  175. async fn set_comment(url: &str, id: u64, author: &str, content: &str) -> Result<Value> {
  176. let req = jsonrpc::request(json!("set_comment"), json!([id, author, content]));
  177. Ok(request(req, url.to_string()).await?)
  178. }
  179. async fn start(options: CliTau) -> Result<()> {
  180. let rpc_addr = "tcp://127.0.0.1:8875";
  181. match options.command {
  182. Some(CliTauSubCommands::Add { title, desc, assign, project, due, rank }) => {
  183. let t = if title.is_none() {
  184. print!("Title: ");
  185. io::stdout().flush()?;
  186. let mut t = String::new();
  187. io::stdin().read_line(&mut t)?;
  188. if &t[(t.len() - 1)..] == "\n" {
  189. t.pop();
  190. }
  191. Some(t)
  192. } else {
  193. title
  194. };
  195. let des = if desc.is_none() {
  196. let editor = var("EDITOR").unwrap();
  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. // fix this
  218. let assignee;
  219. let assigned;
  220. let a: Option<Vec<&str>> = if assign.is_some() {
  221. assignee = assign.unwrap();
  222. assigned = assignee.as_str();
  223. let somevec = assigned.split(',').collect();
  224. Some(somevec)
  225. } else {
  226. None
  227. };
  228. // fix this
  229. let projecte;
  230. let projectd;
  231. let p: Option<Vec<&str>> = if project.is_some() {
  232. projecte = project.unwrap();
  233. projectd = projecte.as_str();
  234. let somevec = projectd.split(',').collect();
  235. Some(somevec)
  236. } else {
  237. None
  238. };
  239. let d = due_as_timestamp(due);
  240. let r = if rank.is_none() { Some(0) } else { rank };
  241. // Add new task and returns `true` upon success.
  242. // --> {"jsonrpc": "2.0", "method": "add", "params": ["title", "desc", ["assign"], ["project"], "due", "rank"], "id": 1}
  243. // <-- {"jsonrpc": "2.0", "result": true, "id": 1}
  244. let req =
  245. jsonrpc::request(json!("add"), json!([t.as_deref(), des.as_deref(), a, p, d, r]));
  246. request(req, rpc_addr.to_string()).await?;
  247. return Ok(())
  248. }
  249. Some(CliTauSubCommands::List { month }) => {
  250. let ts = if month.is_some() {
  251. let month = month.unwrap();
  252. assert!(month.len() == 4);
  253. let (m, y) = (month[..2].parse::<u32>()?, month[2..].parse::<i32>()?);
  254. let dt = NaiveDate::from_ymd(y + 2000, m, 1).and_hms(0, 0, 0);
  255. Some(dt.timestamp())
  256. } else {
  257. None
  258. };
  259. let mut table = Table::new();
  260. table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
  261. table.set_titles(row!["ID", "Title", "Project", "Assigned", "Due", "Rank"]);
  262. let rep = list(rpc_addr, ts).await?;
  263. let tasks = rep.as_array().unwrap();
  264. for task in tasks {
  265. let project = task["project"].as_array().unwrap();
  266. let mut projects = String::new();
  267. for (i, _) in project.iter().enumerate() {
  268. if !projects.is_empty() {
  269. projects.push(',');
  270. }
  271. projects.push_str(project.index(i).as_str().unwrap());
  272. }
  273. let assign = task["assign"].as_array().unwrap();
  274. let mut asgn = String::new();
  275. for (i, _) in assign.iter().enumerate() {
  276. if !asgn.is_empty() {
  277. asgn.push(',');
  278. }
  279. asgn.push_str(assign.index(i).as_str().unwrap());
  280. }
  281. let date = if task["due"].is_u64() {
  282. let due = task["due"].as_i64().unwrap();
  283. NaiveDateTime::from_timestamp(due, 0).date().format("%A %-d %B").to_string()
  284. } else {
  285. "".to_string()
  286. };
  287. // TODO: sort lines in table by rank
  288. // TODO: the higher the rank the brighter it is
  289. table.add_row(row![
  290. task["id"],
  291. task["title"].as_str().unwrap(),
  292. projects,
  293. asgn,
  294. date,
  295. Fb->task["rank"]
  296. ]);
  297. }
  298. table.printstd();
  299. return Ok(())
  300. }
  301. Some(CliTauSubCommands::Update { id, data }) => {
  302. let data = data.unwrap();
  303. let kv: Vec<&str> = data.as_str().split(':').collect();
  304. let new_data = match kv[0].trim() {
  305. "title" | "description" => {
  306. json!({kv[0].trim(): kv[1].trim()})
  307. }
  308. "due" => {
  309. let parsed_data: Option<i64> = due_as_timestamp(Some(kv[1].trim().to_string()));
  310. json!({ kv[0].trim(): parsed_data })
  311. }
  312. "rank" => {
  313. let parsed_data: Option<u64> = Some(kv[1].trim().parse()?);
  314. json!({ kv[0].trim(): parsed_data })
  315. }
  316. _ => {
  317. let parsed_data: Vec<&str> = kv[1].trim().split(',').collect();
  318. json!({ kv[0].trim(): parsed_data })
  319. }
  320. };
  321. update(rpc_addr, id, new_data).await?;
  322. return Ok(())
  323. }
  324. Some(CliTauSubCommands::SetState { id, state }) => {
  325. set_state(rpc_addr, id, state.trim()).await?;
  326. return Ok(())
  327. }
  328. Some(CliTauSubCommands::GetState { id }) => {
  329. let state = get_state(rpc_addr, id).await?;
  330. println!("Task with id: {} is {}", id, state);
  331. return Ok(())
  332. }
  333. Some(CliTauSubCommands::SetComment { id, author, content }) => {
  334. set_comment(rpc_addr, id, author.trim(), content.trim()).await?;
  335. return Ok(())
  336. }
  337. _ => (),
  338. }
  339. error!("Please run 'tau help' to see usage.");
  340. Err(Error::MissingParams)
  341. }
  342. #[async_std::main]
  343. async fn main() -> Result<()> {
  344. let args = CliTau::parse();
  345. let matches = CliTau::command().get_matches();
  346. let verbosity_level = matches.occurrences_of("verbose");
  347. //let config_path = if args.config.is_some() {
  348. // expand_path(&args.config.clone().unwrap())?
  349. //} else {
  350. // join_config_path(&PathBuf::from("tau.toml"))?
  351. //};
  352. // Spawn config file if it's not in place already.
  353. //spawn_config(&config_path, CONFIG_FILE_CONTENTS)?;
  354. let (lvl, conf) = log_config(verbosity_level)?;
  355. TermLogger::init(lvl, conf, TerminalMode::Mixed, ColorChoice::Auto)?;
  356. start(args).await
  357. }