/* This file is part of DarkFi (https://dark.fi) * * Copyright (C) 2020-2023 Dyne.org foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ use std::{process::exit, sync::Arc}; use clap::{Parser, Subcommand}; use log::{error, info}; use simplelog::{ColorChoice, TermLogger, TerminalMode}; use smol::Executor; use url::Url; use darkfi::{ rpc::client::RpcClient, util::cli::{get_log_config, get_log_level}, Result, }; mod drawdown; mod filter; mod primitives; mod rpc; mod util; mod view; use drawdown::{drawdown, to_naivedate}; use filter::{apply_filter, get_ids, no_filter_warn}; use primitives::{task_from_cli, State, TaskEvent}; use util::{due_as_timestamp, prompt_text}; use view::{print_task_info, print_task_list}; use taud::task_info::TaskInfo; const DEFAULT_PATH: &str = "~/tau_exported_tasks"; #[derive(Parser)] #[clap(name = "tau", version)] #[clap(subcommand_precedence_over_arg = true)] struct Args { #[arg(short, action = clap::ArgAction::Count)] /// Increase verbosity (-vvv supported) verbose: u8, #[clap(short, long, default_value = "tcp://127.0.0.1:23330")] /// taud JSON-RPC endpoint endpoint: Url, /// Search filters (zero or more) filters: Vec, #[clap(subcommand)] command: Option, } #[derive(Subcommand)] enum TauSubcommand { /// Add a new task. /// /// Quick start: /// Adding a new task named "New task": /// tau add New task /// New task with description: /// tau add Add more info to tau desc:"some awesome description" /// New task with project and assignee: /// tau add Third task project:p2p Arusty /// Add a task with due date September 12th and rank of 4.6: /// tau add Task no. Four due:1209 rank:4.6 /// /// Notice that if the command does not have "desc" key it will open /// an Editor so you can write the description there. /// /// Also note that "project" key can have multiple /// comma-separated values. /// "assign" on the other hand uses '@' character but also could be /// multiple values, but like so: /// @person1 @person2 /// /// All keys example: /// tau add Improve CLI desc:"Description here" project:tau,darkirc @dave @rusty due:0210 rank:2.2 /// #[clap(verbatim_doc_comment)] Add { /// Pairs of key:value (e.g. desc:description @dark). values: Vec, }, /// Modify/Edit an existing task. Modify { #[clap(allow_hyphen_values = true)] /// Values (e.g. project:blockchain). values: Vec, }, /// List tasks. List, /// Start task(s). Start, /// Open task(s). Open, /// Pause task(s). Pause, /// Stop task(s). Stop, /// Set or Get comment for task(s). Comment { /// Set comment content if provided (Get comments otherwise). content: Vec, }, /// Get all data about selected task(s). Info, /// Switch workspace. Switch { /// Tau workspace. workspace: String, }, /// Import tasks from a specified directory. Import { /// The parent directory from where you want to import tasks. path: Option, }, /// Export tasks to a specified directory. Export { /// The parent directory to where you want to export tasks. path: Option, }, /// Log drawdown. Log { /// The month in which we want to draw a heatmap (e.g. 0822 for August 2022). month: Option, /// The person of which we want to draw a heatmap /// (if not provided we list all assignees). assignee: Option, }, } pub struct Tau { pub rpc_client: RpcClient, } fn main() -> Result<()> { let args = Args::parse(); let log_level = get_log_level(args.verbose); let log_config = get_log_config(args.verbose); TermLogger::init(log_level, log_config, TerminalMode::Mixed, ColorChoice::Auto)?; let executor = Arc::new(Executor::new()); smol::block_on(executor.run(async { let rpc_client = RpcClient::new(args.endpoint, executor.clone()).await?; let tau = Tau { rpc_client }; let mut filters = args.filters.clone(); // If IDs are provided in filter we use them to get the tasks from the daemon // then remove IDs from filter so we can do apply_filter() normally. // If not provided we use get_ids() to get them from the daemon. let ids = get_ids(&mut filters)?; let ids_clone = ids.clone(); let task_ids = if ids.is_empty() { tau.get_ids().await? } else { ids }; let mut tasks = if filters.contains(&"state:stop".to_string()) || filters.contains(&"all".to_string()) { tau.get_stop_tasks(None).await? } else { vec![] }; for id in task_ids { tasks.push(tau.get_task_by_id(id).await?); } if ids_clone.len() == 1 && args.command.is_none() { let tsk = tasks[0].clone(); print_task_info(tsk)?; return Ok(()) } for filter in filters { apply_filter(&mut tasks, &filter); } // Parse subcommands match args.command { Some(sc) => match sc { TauSubcommand::Add { values } => { let mut task = task_from_cli(values)?; if task.title.is_empty() { error!("Please provide a title for the task."); exit(1); }; if task.desc.is_none() { task.desc = prompt_text(TaskInfo::from(task.clone()), "description")?; }; if task.clone().desc.unwrap().trim().is_empty() { error!("Abort adding the task due to empty description."); exit(1) } let title = task.clone().title; let task_id = tau.add(task).await?; if task_id > 0 { println!("Created task {} \"{}\"", task_id, title); } Ok(()) } TauSubcommand::Modify { values } => { if args.filters.is_empty() { no_filter_warn() } let base_task = task_from_cli(values)?; for task in tasks.clone() { let res = tau.update(task.id, base_task.clone()).await?; if res { let tsk = tau.get_task_by_id(task.id).await?; print_task_info(tsk)?; } } Ok(()) } TauSubcommand::Start => { if args.filters.is_empty() { no_filter_warn() } let state = State::Start; for task in tasks { if tau.set_state(task.id, &state).await? { println!("Started task: {:?}", task.id); } } Ok(()) } TauSubcommand::Open => { if args.filters.is_empty() { no_filter_warn() } let state = State::Open; for task in tasks { if tau.set_state(task.id, &state).await? { println!("Opened task: {:?}", task.id); } } Ok(()) } TauSubcommand::Pause => { if args.filters.is_empty() { no_filter_warn() } let state = State::Pause; for task in tasks { if tau.set_state(task.id, &state).await? { println!("Paused task: {:?}", task.id); } } Ok(()) } TauSubcommand::Stop => { if args.filters.is_empty() { no_filter_warn() } let state = State::Stop; for task in tasks { if tau.set_state(task.id, &state).await? { println!("Stopped task: {}", task.id); } } Ok(()) } TauSubcommand::Comment { content } => { if args.filters.is_empty() { no_filter_warn() } for task in tasks { let comment = if content.is_empty() { prompt_text(task.clone(), "comment")? } else { Some(content.join(" ")) }; if comment.clone().unwrap().trim().is_empty() || comment.is_none() { error!("Abort due to empty comment."); exit(1) } let res = tau.set_comment(task.id, comment.unwrap().trim()).await?; if res { let tsk = tau.get_task_by_id(task.id).await?; print_task_info(tsk)?; } } Ok(()) } TauSubcommand::Info => { for task in tasks { let task = tau.get_task_by_id(task.id).await?; print_task_info(task)?; } Ok(()) } TauSubcommand::Switch { workspace } => { if tau.switch_ws(workspace.clone()).await? { println!("You are now on \"{}\" workspace", workspace); } else { println!("Workspace \"{}\" is not configured", workspace); } Ok(()) } TauSubcommand::Export { path } => { let path = path.unwrap_or_else(|| DEFAULT_PATH.into()); let res = tau.export_to(path.clone()).await?; if res { info!("Exported to {}", path); } else { error!("Error exporting to {}", path); } Ok(()) } TauSubcommand::Import { path } => { let path = path.unwrap_or_else(|| DEFAULT_PATH.into()); let res = tau.import_from(path.clone()).await?; if res { info!("Imported from {}", path); } else { error!("Error importing from {}", path); } Ok(()) } TauSubcommand::Log { month, assignee } => { match month { Some(date) => { let ts = to_naivedate(date.clone())? .and_hms_opt(12, 0, 0) .unwrap() .timestamp(); let tasks = tau.get_stop_tasks(Some(ts.try_into().unwrap())).await?; drawdown(date, tasks, assignee)?; } None => { let ws = tau.get_ws().await?; let tasks = tau.get_stop_tasks(None).await?; print_task_list(tasks, ws)?; } } Ok(()) } TauSubcommand::List => { let ws = tau.get_ws().await?; print_task_list(tasks, ws) } }, None => { let ws = tau.get_ws().await?; print_task_list(tasks, ws) } }?; tau.close_connection().await; Ok(()) })) }