main.rs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2023 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use std::process::exit;
  19. use clap::{Parser, Subcommand};
  20. use log::{error, info};
  21. use simplelog::{ColorChoice, TermLogger, TerminalMode};
  22. use url::Url;
  23. use darkfi::{
  24. rpc::client::RpcClient,
  25. util::cli::{get_log_config, get_log_level},
  26. Result,
  27. };
  28. mod drawdown;
  29. mod filter;
  30. mod primitives;
  31. mod rpc;
  32. mod util;
  33. mod view;
  34. use drawdown::{drawdown, to_naivedate};
  35. use filter::{apply_filter, get_ids, no_filter_warn};
  36. use primitives::{task_from_cli, State, TaskEvent};
  37. use util::{due_as_timestamp, prompt_text};
  38. use view::{print_task_info, print_task_list};
  39. use crate::primitives::TaskInfo;
  40. const DEFAULT_PATH: &str = "~/tau_exported_tasks";
  41. #[derive(Parser)]
  42. #[clap(name = "tau", version)]
  43. #[clap(subcommand_precedence_over_arg = true)]
  44. struct Args {
  45. #[arg(short, action = clap::ArgAction::Count)]
  46. /// Increase verbosity (-vvv supported)
  47. verbose: u8,
  48. #[clap(short, long, default_value = "tcp://127.0.0.1:23330")]
  49. /// taud JSON-RPC endpoint
  50. endpoint: Url,
  51. /// Search filters (zero or more)
  52. filters: Vec<String>,
  53. #[clap(subcommand)]
  54. command: Option<TauSubcommand>,
  55. }
  56. #[derive(Subcommand)]
  57. enum TauSubcommand {
  58. /// Add a new task.
  59. ///
  60. /// Quick start:
  61. /// Adding a new task named "New task":
  62. /// tau add New task
  63. /// New task with description:
  64. /// tau add Add more info to tau desc:"some awesome description"
  65. /// New task with project and assignee:
  66. /// tau add Third task project:p2p Arusty
  67. /// Add a task with due date September 12th and rank of 4.6:
  68. /// tau add Task no. Four due:1209 rank:4.6
  69. ///
  70. /// Notice that if the command does not have "desc" key it will open
  71. /// an Editor so you can write the description there.
  72. ///
  73. /// Also note that "project" key can have multiple
  74. /// comma-separated values.
  75. /// "assign" on the other hand uses '@' character but also could be
  76. /// multiple values, but like so:
  77. /// @person1 @person2
  78. ///
  79. /// All keys example:
  80. /// tau add Improve CLI desc:"Description here" project:tau,ircd @dave @rusty due:0210 rank:2.2
  81. ///
  82. #[clap(verbatim_doc_comment)]
  83. Add {
  84. /// Pairs of key:value (e.g. desc:description @dark).
  85. values: Vec<String>,
  86. },
  87. /// Modify/Edit an existing task.
  88. Modify {
  89. #[clap(allow_hyphen_values = true)]
  90. /// Values (e.g. project:blockchain).
  91. values: Vec<String>,
  92. },
  93. /// List tasks.
  94. List,
  95. /// Start task(s).
  96. Start,
  97. /// Open task(s).
  98. Open,
  99. /// Pause task(s).
  100. Pause,
  101. /// Stop task(s).
  102. Stop,
  103. /// Set or Get comment for task(s).
  104. Comment {
  105. /// Set comment content if provided (Get comments otherwise).
  106. content: Vec<String>,
  107. },
  108. /// Get all data about selected task(s).
  109. Info,
  110. /// Switch workspace.
  111. Switch {
  112. /// Tau workspace.
  113. workspace: String,
  114. },
  115. /// Import tasks from a specified directory.
  116. Import {
  117. /// The parent directory from where you want to import tasks.
  118. path: Option<String>,
  119. },
  120. /// Export tasks to a specified directory.
  121. Export {
  122. /// The parent directory to where you want to export tasks.
  123. path: Option<String>,
  124. },
  125. /// Log drawdown.
  126. Log {
  127. /// The month in which we want to draw a heatmap (e.g. 0822 for August 2022).
  128. month: Option<String>,
  129. /// The person of which we want to draw a heatmap
  130. /// (if not provided we list all assignees).
  131. assignee: Option<String>,
  132. },
  133. }
  134. pub struct Tau {
  135. pub rpc_client: RpcClient,
  136. }
  137. #[async_std::main]
  138. async fn main() -> Result<()> {
  139. let args = Args::parse();
  140. let log_level = get_log_level(args.verbose);
  141. let log_config = get_log_config(args.verbose);
  142. TermLogger::init(log_level, log_config, TerminalMode::Mixed, ColorChoice::Auto)?;
  143. let rpc_client = RpcClient::new(args.endpoint).await?;
  144. let tau = Tau { rpc_client };
  145. let mut filters = args.filters.clone();
  146. // If IDs are provided in filter we use them to get the tasks from the daemon
  147. // then remove IDs from filter so we can do apply_filter() normally.
  148. // If not provided we use get_ids() to get them from the daemon.
  149. let ids = get_ids(&mut filters)?;
  150. let ids_clone = ids.clone();
  151. let task_ids = if ids.is_empty() { tau.get_ids().await? } else { ids };
  152. let mut tasks =
  153. if filters.contains(&"state:stop".to_string()) || filters.contains(&"all".to_string()) {
  154. tau.get_stop_tasks(None).await?
  155. } else {
  156. vec![]
  157. };
  158. for id in task_ids {
  159. tasks.push(tau.get_task_by_id(id).await?);
  160. }
  161. if ids_clone.len() == 1 && args.command.is_none() {
  162. let tsk = tasks[0].clone();
  163. print_task_info(tsk)?;
  164. return Ok(())
  165. }
  166. for filter in filters {
  167. apply_filter(&mut tasks, &filter);
  168. }
  169. // Parse subcommands
  170. match args.command {
  171. Some(sc) => match sc {
  172. TauSubcommand::Add { values } => {
  173. let mut task = task_from_cli(values)?;
  174. if task.title.is_empty() {
  175. error!("Please provide a title for the task.");
  176. exit(1);
  177. };
  178. if task.desc.is_none() {
  179. task.desc = prompt_text(TaskInfo::from(task.clone()), "description")?;
  180. };
  181. if task.clone().desc.unwrap().trim().is_empty() {
  182. error!("Abort adding the task due to empty description.");
  183. exit(1)
  184. }
  185. let title = task.clone().title;
  186. if tau.add(task).await? {
  187. println!("Created task \"{}\"", title);
  188. }
  189. Ok(())
  190. }
  191. TauSubcommand::Modify { values } => {
  192. if args.filters.is_empty() {
  193. no_filter_warn()
  194. }
  195. let base_task = task_from_cli(values)?;
  196. for task in tasks.clone() {
  197. let res = tau.update(task.id.into(), base_task.clone()).await?;
  198. if res {
  199. let tsk = tau.get_task_by_id(task.id.into()).await?;
  200. print_task_info(tsk)?;
  201. }
  202. }
  203. Ok(())
  204. }
  205. TauSubcommand::Start => {
  206. if args.filters.is_empty() {
  207. no_filter_warn()
  208. }
  209. let state = State::Start;
  210. for task in tasks {
  211. if tau.set_state(task.id.into(), &state).await? {
  212. println!("Started task: {:?}", task.id);
  213. }
  214. }
  215. Ok(())
  216. }
  217. TauSubcommand::Open => {
  218. if args.filters.is_empty() {
  219. no_filter_warn()
  220. }
  221. let state = State::Open;
  222. for task in tasks {
  223. if tau.set_state(task.id.into(), &state).await? {
  224. println!("Opened task: {:?}", task.id);
  225. }
  226. }
  227. Ok(())
  228. }
  229. TauSubcommand::Pause => {
  230. if args.filters.is_empty() {
  231. no_filter_warn()
  232. }
  233. let state = State::Pause;
  234. for task in tasks {
  235. if tau.set_state(task.id.into(), &state).await? {
  236. println!("Paused task: {:?}", task.id);
  237. }
  238. }
  239. Ok(())
  240. }
  241. TauSubcommand::Stop => {
  242. if args.filters.is_empty() {
  243. no_filter_warn()
  244. }
  245. let state = State::Stop;
  246. for task in tasks {
  247. if tau.set_state(task.id.into(), &state).await? {
  248. println!("Stopped task: {}", task.id);
  249. }
  250. }
  251. Ok(())
  252. }
  253. TauSubcommand::Comment { content } => {
  254. if args.filters.is_empty() {
  255. no_filter_warn()
  256. }
  257. for task in tasks {
  258. let comment = if content.is_empty() {
  259. prompt_text(task.clone(), "comment")?
  260. } else {
  261. Some(content.join(" "))
  262. };
  263. if comment.clone().unwrap().trim().is_empty() || comment.is_none() {
  264. error!("Abort due to empty comment.");
  265. exit(1)
  266. }
  267. let res = tau.set_comment(task.id.into(), comment.unwrap().trim()).await?;
  268. if res {
  269. let tsk = tau.get_task_by_id(task.id.into()).await?;
  270. print_task_info(tsk)?;
  271. }
  272. }
  273. Ok(())
  274. }
  275. TauSubcommand::Info => {
  276. for task in tasks {
  277. let task = tau.get_task_by_id(task.id.into()).await?;
  278. print_task_info(task)?;
  279. }
  280. Ok(())
  281. }
  282. TauSubcommand::Switch { workspace } => {
  283. if tau.switch_ws(workspace.clone()).await? {
  284. println!("You are now on \"{}\" workspace", workspace);
  285. }
  286. Ok(())
  287. }
  288. TauSubcommand::Export { path } => {
  289. let path = path.unwrap_or_else(|| DEFAULT_PATH.into());
  290. let res = tau.export_to(path.clone()).await?;
  291. if res {
  292. info!("Exported to {}", path);
  293. } else {
  294. error!("Error exporting to {}", path);
  295. }
  296. Ok(())
  297. }
  298. TauSubcommand::Import { path } => {
  299. let path = path.unwrap_or_else(|| DEFAULT_PATH.into());
  300. let res = tau.import_from(path.clone()).await?;
  301. if res {
  302. info!("Imported from {}", path);
  303. } else {
  304. error!("Error importing from {}", path);
  305. }
  306. Ok(())
  307. }
  308. TauSubcommand::Log { month, assignee } => {
  309. match month {
  310. Some(date) => {
  311. let ts =
  312. to_naivedate(date.clone())?.and_hms_opt(12, 0, 0).unwrap().timestamp();
  313. let tasks = tau.get_stop_tasks(Some(ts)).await?;
  314. drawdown(date, tasks, assignee)?;
  315. }
  316. None => {
  317. let ws = tau.get_ws().await?;
  318. let tasks = tau.get_stop_tasks(None).await?;
  319. print_task_list(tasks, ws)?;
  320. }
  321. }
  322. Ok(())
  323. }
  324. TauSubcommand::List => {
  325. let ws = tau.get_ws().await?;
  326. print_task_list(tasks, ws)
  327. }
  328. },
  329. None => {
  330. let ws = tau.get_ws().await?;
  331. print_task_list(tasks, ws)
  332. }
  333. }?;
  334. tau.close_connection().await
  335. }