util.rs 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. use std::{
  2. env::{temp_dir, var},
  3. fs::{self, File},
  4. io::{self, Read, Write},
  5. ops::Index,
  6. process::Command,
  7. };
  8. use chrono::{Datelike, Local, NaiveDate, NaiveDateTime};
  9. use clap::{Parser, Subcommand};
  10. use log::error;
  11. use serde::{Deserialize, Serialize};
  12. use serde_json::Value;
  13. use darkfi::{util::cli::UrlConfig, Error, Result};
  14. pub const CONFIG_FILE_CONTENTS: &[u8] = include_bytes!("../../taud_config.toml");
  15. #[derive(Clone, Debug, Serialize, Deserialize)]
  16. pub struct TauConfig {
  17. /// The address where taud should bind its RPC socket
  18. pub rpc_listen: UrlConfig,
  19. }
  20. #[derive(Subcommand)]
  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(Parser)]
  102. #[clap(name = "tau")]
  103. #[clap(author, version, about)]
  104. pub struct CliTau {
  105. /// Increase verbosity
  106. #[clap(short, parse(from_occurrences))]
  107. pub verbose: u8,
  108. /// Sets a custom config file
  109. #[clap(short, long)]
  110. pub config: Option<String>,
  111. #[clap(subcommand)]
  112. pub command: Option<CliTauSubCommands>,
  113. }
  114. pub fn due_as_timestamp(due: &str) -> Option<i64> {
  115. if due.len() == 4 {
  116. let (day, month) = (due[..2].parse::<u32>().unwrap(), due[2..].parse::<u32>().unwrap());
  117. let mut year = Local::today().year();
  118. if month < Local::today().month() {
  119. year += 1;
  120. }
  121. if month == Local::today().month() && day < Local::today().day() {
  122. year += 1;
  123. }
  124. let dt = NaiveDate::from_ymd(year, month, day).and_hms(12, 0, 0);
  125. return Some(dt.timestamp())
  126. }
  127. if due.len() > 4 {
  128. error!("due date must be of length 4 (e.g \"1503\" for 15 March)");
  129. }
  130. None
  131. }
  132. pub fn set_title() -> Result<String> {
  133. print!("Title: ");
  134. io::stdout().flush()?;
  135. let mut t = String::new();
  136. io::stdin().read_line(&mut t)?;
  137. if t.is_empty() {
  138. error!("You can't have a task without a title");
  139. return Err(Error::OperationFailed)
  140. }
  141. if &t[(t.len() - 1)..] == "\n" {
  142. t.pop();
  143. }
  144. Ok(t)
  145. }
  146. pub fn desc_in_editor() -> Result<Option<String>> {
  147. // Create a temporary file with some comments inside
  148. let mut file_path = temp_dir();
  149. file_path.push("temp_file");
  150. File::create(&file_path)?;
  151. fs::write(
  152. &file_path,
  153. "\n# Write task description above this line\n# These lines will be removed\n",
  154. )?;
  155. // Calling env var {EDITOR} on temp file
  156. let editor = match var("EDITOR") {
  157. Ok(t) => t,
  158. Err(e) => {
  159. error!("EDITOR {}", e);
  160. return Err(Error::OperationFailed)
  161. }
  162. };
  163. Command::new(editor).arg(&file_path).status()?;
  164. // Whatever has been written in temp file, will be read here
  165. let mut lines = String::new();
  166. File::open(file_path)?.read_to_string(&mut lines)?;
  167. // Store only non-comment lines
  168. let mut description = String::new();
  169. for line in lines.split('\n') {
  170. if !line.starts_with('#') {
  171. description.push_str(line);
  172. description.push('\n');
  173. }
  174. }
  175. description.pop();
  176. Ok(Some(description))
  177. }
  178. pub fn get_comments(rep: Value) -> Result<String> {
  179. let task: Value = serde_json::from_value(rep)?;
  180. let comments: Vec<Value> = serde_json::from_value(task["comments"].clone())?;
  181. let mut result = String::new();
  182. for comment in comments {
  183. result.push_str(comment["author"].as_str().ok_or(Error::OperationFailed)?);
  184. result.push_str(": ");
  185. result.push_str(comment["content"].as_str().ok_or(Error::OperationFailed)?);
  186. result.push('\n');
  187. }
  188. result.pop();
  189. Ok(result)
  190. }
  191. pub fn get_events(rep: Value) -> Result<String> {
  192. let task: Value = serde_json::from_value(rep)?;
  193. let events: Vec<Value> = serde_json::from_value(task["events"].clone())?;
  194. let mut ev = String::new();
  195. for event in events {
  196. ev.push_str("State changed to ");
  197. ev.push_str(event["action"].as_str().ok_or(Error::OperationFailed)?);
  198. ev.push_str(" at ");
  199. ev.push_str(&timestamp_to_date(event["timestamp"].clone(), "datetime"));
  200. ev.push('\n');
  201. }
  202. ev.pop();
  203. Ok(ev)
  204. }
  205. pub fn timestamp_to_date(timestamp: Value, dt: &str) -> String {
  206. let result = if timestamp.is_u64() {
  207. let timestamp = timestamp.as_i64().unwrap();
  208. match dt {
  209. "date" => {
  210. NaiveDateTime::from_timestamp(timestamp, 0).date().format("%A %-d %B").to_string()
  211. }
  212. "datetime" => {
  213. NaiveDateTime::from_timestamp(timestamp, 0).format("%H:%M %A %-d %B").to_string()
  214. }
  215. _ => "".to_string(),
  216. }
  217. } else {
  218. "".to_string()
  219. };
  220. result
  221. }
  222. pub fn get_from_task(task: Value, value: &str) -> Result<String> {
  223. let vec_values: Vec<Value> = serde_json::from_value(task[value].clone())?;
  224. let mut result = String::new();
  225. for (i, _) in vec_values.iter().enumerate() {
  226. if !result.is_empty() {
  227. result.push(',');
  228. }
  229. result.push_str(vec_values.index(i).as_str().unwrap());
  230. }
  231. Ok(result)
  232. }