diff --git a/bin/tau/tau-cli/Cargo.toml b/bin/tau/tau-cli/Cargo.toml deleted file mode 100644 index 7ac370c57..000000000 --- a/bin/tau/tau-cli/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "tau" -description = "Command-line client for taud" -version = "0.4.1" -edition = "2021" -authors = ["Dyne.org foundation "] -license = "AGPL-3.0-only" -homepage = "https://dark.fi" -repository = "https://codeberg.org/darkrenaissance/darkfi" - -[dependencies] -libc = "0.2.155" -darkfi = {path = "../../../", features = ["rpc"]} -taud = {path = "../taud"} - -# Misc -chrono = "0.4.38" -colored = "2.1.0" -log = "0.4.22" -prettytable-rs = "0.10.0" -simplelog = "0.12.2" -smol = "2.0.0" -term_grid = { git = "https://github.com/Dastan-glitch/rust-term-grid.git" } -textwrap = "0.16.1" -url = "2.5.2" - -# Encoding and parsing -clap = {version = "4.4.11", features = ["derive"]} -serde = {version = "1.0.204", features = ["derive"]} -tinyjson = "2.5.1" - -[lints] -workspace = true - diff --git a/bin/tau/tau-cli/Makefile b/bin/tau/tau-cli/Makefile deleted file mode 100644 index 2b09bf887..000000000 --- a/bin/tau/tau-cli/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -.POSIX: - -# Install prefix -PREFIX = $(HOME)/.cargo - -# Cargo binary -CARGO = cargo +nightly - -SRC = \ - Cargo.toml \ - ../../../Cargo.toml \ - $(shell find src -type f) \ - $(shell find ../../../src -type f) \ - -BIN = ../../../tau - -all: $(BIN) - -$(BIN): $(SRC) - $(CARGO) build $(TARGET_PRFX)$(RUST_TARGET) --release --package tau - cp -f ../../../target/$(RUST_TARGET)/release/tau $@ - -clean: - rm -f $(BIN) - -install: all - mkdir -p $(DESTDIR)$(PREFIX)/bin - cp -f $(BIN) $(DESTDIR)$(PREFIX)/bin - chmod 755 $(DESTDIR)$(PREFIX)/bin/tau - -uninstall: - rm -f $(DESTDIR)$(PREFIX)/bin/tau - -.PHONY: all clean install uninstall diff --git a/bin/tau/tau-cli/src/drawdown.rs b/bin/tau/tau-cli/src/drawdown.rs deleted file mode 100644 index 8a760e892..000000000 --- a/bin/tau/tau-cli/src/drawdown.rs +++ /dev/null @@ -1,195 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 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::collections::HashMap; - -use chrono::{Datelike, Duration, NaiveDate, Utc}; -use colored::Colorize; -use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; - -use darkfi::{util::time::DateTime, Error, Result}; - -use crate::primitives::{TaskEvent, TaskInfo}; - -// Red -const NO_TASK_SCALE: u8 = 50; -const MIN_SCALE: usize = 90; -const MAX_SCALE: usize = 255; -const INCREASE_FACTOR: usize = 25; - -// Green -const GREEN: u8 = 40; -// Blue -const BLUE: u8 = 50; - -/// Log drawdown gets all assignees of tasks, stores a vec of stopped tasks for each -/// assignee in a hashmap, draw a heatmap of how many stopped tasks in each day of the -/// specified month and assignee. -pub fn drawdown(date: String, tasks: Vec, assignee: Option) -> Result<()> { - let mut ret = HashMap::new(); - let assignees = assignees(tasks.clone()); - - if assignee.is_none() { - println!("Assignees of this month's tasks are: {}", assignees.join(", ")); - return Ok(()) - } - - let asgn = assignee.unwrap(); - - if !assignees.contains(&asgn) { - eprintln!("Assignee {} not found, run \"tau log -h\" for more information.", asgn); - return Ok(()) - } - - for assignee in assignees { - let stopped_tasks = tasks - .clone() - .into_iter() - .filter(|task| { - if task.assign.is_empty() { - task.owner == asgn - } else { - task.assign.contains(&asgn) - } - }) - .collect::>(); - ret.insert(assignee, stopped_tasks); - } - - let mut naivedate = to_naivedate(date.clone())?; - - println!("log drawdown for {} in {}", asgn, naivedate.format("%b %Y")); - - let fdow = if naivedate.month() == 2 && !is_leap_year(naivedate.year()) { - [" ", "1 ", "8 ", "15", "22", " "] - } else { - [" ", "1 ", "8 ", "15", "22", "29"] - }; - - // Print first day of each week horizontally. - let mut dow_grid = - Grid::new(GridOptions { direction: Direction::LeftToRight, filling: Filling::Spaces(1) }); - if ret.contains_key(&asgn) { - for i in fdow { - let cell = Cell::from(i); - dow_grid.add(cell) - } - let grid_display = dow_grid.fit_into_rows(1); - print!("{}", grid_display); - } - - let mut grid = - Grid::new(GridOptions { direction: Direction::TopToBottom, filling: Filling::Spaces(1) }); - - let days_in_month = get_days_from_month(date)? as u32; - - if ret.contains_key(&asgn) { - for _ in 0..7 { - let dow = naivedate.weekday().to_string(); - let wcell = Cell::from(dow); - grid.add(wcell); - naivedate += Duration::days(1); - } - for day in 1..=days_in_month { - let owner_stopped_tasks = ret.get(&asgn).unwrap().to_owned(); - let date_tasks: Vec = owner_stopped_tasks - .into_iter() - .filter(|t| { - // last event is always state stop - let event_date = DateTime::from_timestamp( - t.events.last().unwrap_or(&TaskEvent::default()).timestamp.0, - 0, - ); - event_date.day == day - }) - .collect(); - - let red_scale = if date_tasks.is_empty() { - NO_TASK_SCALE - } else { - ((date_tasks.len() * INCREASE_FACTOR) + MIN_SCALE).clamp(MIN_SCALE, MAX_SCALE) as u8 - }; - - let cell = Cell::from("▀▀".truecolor(red_scale, GREEN, BLUE)); - grid.add(cell) - } - } - - let grid_display = grid.fit_into_rows(7); - println!("{}", grid_display); - - Ok(()) -} - -fn is_leap_year(year: i32) -> bool { - year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) -} - -fn helper_parse_func(date: String) -> Result<(u32, i32)> { - if date.len() != 4 || date.parse::().is_err() { - return Err(Error::MalformedPacket) - } - let (month, year) = (date[..2].parse::().unwrap(), date[2..].parse::().unwrap()); - let year = year + (Utc::now().year() / 100) * 100; - - Ok((month, year)) -} - -pub fn to_naivedate(date: String) -> Result { - let (month, year) = helper_parse_func(date)?; - Ok(NaiveDate::from_ymd_opt(year, month, 1).unwrap()) -} - -fn get_days_from_month(date: String) -> Result { - let (month, year) = helper_parse_func(date)?; - - Ok(NaiveDate::from_ymd_opt( - match month { - 12 => year + 1, - _ => year, - }, - match month { - 12 => 1, - _ => month + 1, - }, - 1, - ) - .unwrap() - .signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap()) - .num_days()) -} - -fn assignees(tasks: Vec) -> Vec { - let mut assignees = vec![]; - for task in tasks { - // if task is stopped with no assignee specified we give credit to the owner - if task.assign.is_empty() { - if !assignees.contains(&task.owner) { - assignees.push(task.owner) - } - } else { - for assignee in task.assign { - if !assignees.contains(&assignee) { - assignees.push(assignee) - } - } - } - } - - assignees -} diff --git a/bin/tau/tau-cli/src/filter.rs b/bin/tau/tau-cli/src/filter.rs deleted file mode 100644 index d0fba39c2..000000000 --- a/bin/tau/tau-cli/src/filter.rs +++ /dev/null @@ -1,236 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 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::{ - io::{stdin, stdout, Write}, - process::exit, -}; - -use chrono::{Datelike, Local, TimeZone, Utc}; -use log::error; - -use darkfi::Result; - -use crate::{ - primitives::{State, TaskInfo}, - util::due_as_timestamp, -}; - -pub fn apply_filter(tasks: &mut Vec, filter: &str) { - match filter { - "all" => {} - // Filter by state. - _ if filter.contains("state:") => { - let kv: Vec<&str> = filter.split(':').collect(); - //if let Some(state) = Value::from(kv[1]).as_str() { - if let Some(state) = Some(kv[1]) { - match state { - "open" => tasks.retain(|task| task.state == State::Open.to_string()), - "start" => tasks.retain(|task| task.state == State::Start.to_string()), - "pause" => tasks.retain(|task| task.state == State::Pause.to_string()), - "stop" => tasks.retain(|task| task.state == State::Stop.to_string()), - _ => { - error!("Not implemented, states are open,start,pause and stop"); - exit(1) - } - } - } - } - - // Filter by tag - _ if filter.starts_with('+') => tasks.retain(|task| task.tags.contains(&filter.into())), - - // Filter by month - _ if filter.contains("month:") => { - let kv: Vec<&str> = filter.split(':').collect(); - if kv.len() == 2 { - //if let Some(value) = Value::from(kv[1]).as_str() { - if let Some(value) = Some(kv[1]) { - if value.len() != 4 || value.parse::().is_err() { - error!( - "Please provide month date as \"MMYY\" (e.g. 0922 for September 2022)" - ); - exit(1) - } - let (month, year) = - (value[..2].parse::().unwrap(), value[2..].parse::().unwrap()); - - let year = year + (Utc::now().year() / 100) * 100; - tasks.retain(|task| { - let date = task.created_at.0; - let task_date = Utc.timestamp_nanos(date.try_into().unwrap()).date_naive(); - task_date.month() == month && task_date.year() == year - }) - } else { - error!("Please provide month date as \"MMYY\" (e.g. 0922 for September 2022)"); - exit(1) - } - } - } - - // Filter by assignee(s). - _ if filter.starts_with('@') => tasks.retain(|task| task.assign.contains(&filter.into())), - - // Filter by project(s). - _ if filter.contains("project:") => { - let kv: Vec<&str> = filter.split(':').collect(); - if kv.len() == 2 { - // OLD: if let Some(value) = Value::from(kv[1]).as_str() { - if let Some(value) = Some(kv[1]) { - if value.is_empty() { - tasks.retain(|task| task.project.is_empty()) - } else { - tasks.retain(|task| task.project.contains(&value.to_string())) - } - } - } - } - - // Filter by rank. - _ if filter.contains("rank:") => { - let kv: Vec<&str> = filter.split(':').collect(); - if kv.len() == 3 { - let value = kv[2].parse::().unwrap_or(0.0); - tasks.retain(|task| { - if filter.contains("lt") { - task.rank < Some(value) - } else if filter.contains("gt") { - task.rank > Some(value) - } else { - true - } - }) - } - tasks.retain(|task| task.rank.is_none()) - } - - // Filter by due date. - _ if filter.contains("due:") || filter.contains("due.") => { - let kv: Vec<&str> = filter.split(':').collect(); - let due_op = if filter.contains('.') { - let due_op: Vec<&str> = kv[0].split('.').collect(); - due_op[1] - } else { - "" - }; - - if kv.len() == 2 { - // OLD: let Value::from(kv[1]).as_str(); - let value = Some(kv[1]); - if let Some(value) = value { - if value.is_empty() { - tasks.retain(|task| task.due.is_none()) - } else { - let filter_date = if value == "today" { - Local::now().date_naive() - } else { - let due_date = due_as_timestamp(value).unwrap_or(0); - Utc.timestamp_nanos(due_date.try_into().unwrap()).date_naive() - }; - - tasks.retain(|task| { - let date = if let Some(due) = task.due { due.0 } else { 0 }; - let task_date = - Utc.timestamp_nanos(date.try_into().unwrap()).date_naive(); - - match due_op { - "not" => task_date != filter_date, - "after" => task_date > filter_date, - "before" => task_date < filter_date, - "" | "is" => task_date == filter_date, - _ => true, - } - }) - } - } else { - error!("Please provide due date as \"DDMM\" (e.g. 2210 for October 22nd)"); - exit(1) - } - } - } - - _ => { - println!("No matches."); - exit(1) - } - } -} - -pub fn no_filter_warn() { - let mut s = String::new(); - print!("This command has no filter, and will modify all tasks. Are you sure? (yes/no) "); - let _ = stdout().flush(); - stdin().read_line(&mut s).unwrap_or(0); - match s.trim() { - "y" | "yes" => {} - _ => { - println!("Command prevented from running."); - exit(1) - } - } -} - -pub fn get_ids(filters: &mut Vec) -> Result> { - let mut vec_ids = vec![]; - let mut matching_id = String::new(); - if let Some(index) = filters.iter().position(|t| { - t.parse::().is_ok() || !t.contains(':') && (t.contains(',') || t.contains('-')) - }) { - matching_id.push_str(&filters.remove(index)); - } - - match matching_id { - _ if matching_id.parse::().is_ok() => { - let id = matching_id.parse::().unwrap(); - vec_ids.push(id) - } - _ if !matching_id.contains(':') && - (matching_id.contains(',') || matching_id.contains('-')) => - { - let num = matching_id.replace(&[',', '-'][..], ""); - if num.parse::().is_err() { - error!("Invalid ID number"); - exit(1) - } - if matching_id.contains(',') { - let ids: Vec<&str> = matching_id.split(',').collect(); - for id in ids { - if id.contains('-') { - let range: Vec<&str> = id.split('-').collect(); - let range = - range[0].parse::().unwrap()..=range[1].parse::().unwrap(); - for rid in range { - vec_ids.push(rid) - } - } else { - vec_ids.push(id.parse::().unwrap()) - } - } - } else if matching_id.contains('-') { - let range: Vec<&str> = matching_id.split('-').collect(); - let range = range[0].parse::().unwrap()..=range[1].parse::().unwrap(); - for rid in range { - vec_ids.push(rid) - } - } - } - _ => {} - } - - Ok(vec_ids) -} diff --git a/bin/tau/tau-cli/src/main.rs b/bin/tau/tau-cli/src/main.rs deleted file mode 100644 index b024ed8b8..000000000 --- a/bin/tau/tau-cli/src/main.rs +++ /dev/null @@ -1,411 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 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(()) - })) -} diff --git a/bin/tau/tau-cli/src/primitives.rs b/bin/tau/tau-cli/src/primitives.rs deleted file mode 100644 index 2f25e94df..000000000 --- a/bin/tau/tau-cli/src/primitives.rs +++ /dev/null @@ -1,105 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 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 darkfi::{util::time::Timestamp, Result}; - -use crate::due_as_timestamp; -pub(crate) use taud::task_info::{State, TaskEvent, TaskInfo}; - -#[derive(Clone, Debug)] -pub struct BaseTask { - pub title: String, - pub tags: Vec, - pub desc: Option, - pub assign: Vec, - pub project: Vec, - pub due: Option, - pub rank: Option, -} - -impl From for TaskInfo { - fn from(value: BaseTask) -> Self { - Self { - ref_id: String::default(), - workspace: String::default(), - id: u32::default(), - title: value.title, - tags: value.tags, - desc: String::default(), - owner: String::default(), - assign: value.assign, - project: value.project, - due: value.due.map(Timestamp), - rank: value.rank, - created_at: Timestamp(u64::default()), - state: String::default(), - events: vec![], - comments: vec![], - } - } -} - -pub fn task_from_cli(values: Vec) -> Result { - let mut title = String::new(); - let mut tags = vec![]; - let mut desc = None; - let mut project = vec![]; - let mut assign = vec![]; - let mut due = None; - let mut rank = None; - - for val in values { - let field: Vec<&str> = val.split(':').collect(); - if field.len() == 1 { - if field[0].starts_with('+') || field[0].starts_with('-') { - tags.push(field[0].into()); - continue - } - if field[0].starts_with('@') { - assign.push(field[0].into()); - continue - } - title.push_str(field[0]); - title.push(' '); - continue - } - - if field.len() != 2 { - continue - } - - if field[0] == "project" { - project = field[1].split(',').map(|s| s.into()).collect(); - } - - if field[0] == "desc" { - desc = Some(field[1].into()); - } - - if field[0] == "due" { - due = due_as_timestamp(field[1]) - } - - if field[0] == "rank" { - rank = Some(field[1].parse::()?); - } - } - - let title = title.trim().into(); - Ok(BaseTask { title, tags, desc, project, assign, due, rank }) -} diff --git a/bin/tau/tau-cli/src/rpc.rs b/bin/tau/tau-cli/src/rpc.rs deleted file mode 100644 index 3f61b2580..000000000 --- a/bin/tau/tau-cli/src/rpc.rs +++ /dev/null @@ -1,193 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 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 darkfi::{rpc::jsonrpc::JsonRequest, Result}; -use log::debug; -use tinyjson::JsonValue; - -use crate::{ - primitives::{BaseTask, State, TaskInfo}, - Tau, -}; - -impl Tau { - pub async fn close_connection(&self) { - self.rpc_client.stop().await - } - - /// Add a new task. - pub async fn add(&self, task: BaseTask) -> Result { - let mut params = vec![ - JsonValue::String(task.title.clone()), - JsonValue::Array(task.tags.iter().map(|x| JsonValue::String(x.clone())).collect()), - JsonValue::String(task.desc.unwrap_or("".to_string())), - JsonValue::Array(task.assign.iter().map(|x| JsonValue::String(x.clone())).collect()), - JsonValue::Array(task.project.iter().map(|x| JsonValue::String(x.clone())).collect()), - ]; - - let due = if let Some(num) = task.due { - JsonValue::String(num.to_string()) - } else { - JsonValue::Null - }; - params.push(due); - - let rank = - if let Some(num) = task.rank { JsonValue::Number(num.into()) } else { JsonValue::Null }; - params.push(rank); - - let req = JsonRequest::new("add", params); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - Ok(*rep.get::().unwrap() as u32) - } - - /// Get current open tasks ids. - pub async fn get_ids(&self) -> Result> { - let req = JsonRequest::new("get_ids", vec![]); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - - let mut ret = vec![]; - for i in rep.get::>().unwrap() { - ret.push(*i.get::().unwrap() as u32) - } - - Ok(ret) - } - - /// Update existing task given it's ID and some params. - pub async fn update(&self, id: u32, task: BaseTask) -> Result { - let mut params = vec![ - JsonValue::String(task.title.clone()), - JsonValue::Array(task.tags.iter().map(|x| JsonValue::String(x.clone())).collect()), - JsonValue::String(task.desc.unwrap_or("".to_string())), - JsonValue::Array(task.assign.iter().map(|x| JsonValue::String(x.clone())).collect()), - JsonValue::Array(task.project.iter().map(|x| JsonValue::String(x.clone())).collect()), - ]; - - let due = if let Some(num) = task.due { - JsonValue::String(num.to_string()) - } else { - JsonValue::Null - }; - params.push(due); - - let rank = - if let Some(num) = task.rank { JsonValue::Number(num.into()) } else { JsonValue::Null }; - params.push(rank); - - let req = JsonRequest::new( - "update", - vec![JsonValue::Number(id.into()), JsonValue::Array(params)], - ); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - Ok(*rep.get::().unwrap()) - } - - /// Set the state for a task. - pub async fn set_state(&self, id: u32, state: &State) -> Result { - let req = JsonRequest::new( - "set_state", - vec![JsonValue::Number(id.into()), JsonValue::String(state.to_string())], - ); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - Ok(*rep.get::().unwrap()) - } - - /// Set a comment for a task. - pub async fn set_comment(&self, id: u32, content: &str) -> Result { - let req = JsonRequest::new( - "set_comment", - vec![JsonValue::Number(id.into()), JsonValue::String(content.to_string())], - ); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - Ok(*rep.get::().unwrap()) - } - - /// Get task data by its ID. - pub async fn get_task_by_id(&self, id: u32) -> Result { - let req = JsonRequest::new("get_task_by_id", vec![JsonValue::Number(id.into())]); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - let rep = rep.into(); - Ok(rep) - } - - /// Get month's stopped tasks. - pub async fn get_stop_tasks(&self, month: Option) -> Result> { - let param = if let Some(month) = month { - JsonValue::String(month.to_string()) - } else { - JsonValue::Null - }; - - let req = JsonRequest::new("get_stop_tasks", vec![param]); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - let rep = - rep.get::>().unwrap().iter().map(|x| (*x).clone().into()).collect(); - Ok(rep) - } - - /// Switch workspace. - pub async fn switch_ws(&self, workspace: String) -> Result { - let req = JsonRequest::new("switch_ws", vec![JsonValue::String(workspace)]); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - Ok(*rep.get::().unwrap()) - } - - /// Get current workspace. - pub async fn get_ws(&self) -> Result { - let req = JsonRequest::new("get_ws", vec![]); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - Ok(rep.get::().unwrap().clone()) - } - - /// Export tasks. - pub async fn export_to(&self, path: String) -> Result { - let req = JsonRequest::new("export", vec![JsonValue::String(path)]); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - Ok(*rep.get::().unwrap()) - } - - /// Import tasks. - pub async fn import_from(&self, path: String) -> Result { - let req = JsonRequest::new("import", vec![JsonValue::String(path)]); - let rep = self.rpc_client.request(req).await?; - - debug!("Got reply: {:?}", rep); - Ok(*rep.get::().unwrap()) - } -} diff --git a/bin/tau/tau-cli/src/util.rs b/bin/tau/tau-cli/src/util.rs deleted file mode 100644 index 5b4f39020..000000000 --- a/bin/tau/tau-cli/src/util.rs +++ /dev/null @@ -1,107 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 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::{ - env, - fs::{self, File}, - io::Write, - process::Command, -}; - -use chrono::{Datelike, Local, NaiveDate}; -use log::error; - -use darkfi::{util::time::Timestamp, Result}; - -use crate::{ - primitives::TaskInfo, - view::{comments_table, events_table, taskinfo_table}, -}; - -/// Parse due date (e.g. "1503" for 15 March) as i64 timestamp. -pub fn due_as_timestamp(due: &str) -> Option { - if due.len() != 4 || due.parse::().is_err() { - error!("Due date must be digits of length 4 (e.g. \"1503\" for 15 March)"); - return None - } - let (day, month) = (due[..2].parse::().unwrap(), due[2..].parse::().unwrap()); - - if day > 31 || month > 12 { - error!("Invalid or out-of-range date"); - return None - } - - let mut year = Local::now().year(); - - // Ensure the due date is in future - if month < Local::now().month() { - year += 1; - } - - if month == Local::now().month() && day < Local::now().day() { - year += 1; - } - - let dt = NaiveDate::from_ymd_opt(year, month, day).unwrap().and_hms_opt(12, 0, 0).unwrap(); - dt.timestamp().try_into().ok() -} - -/// Start up the preferred editor to edit a task's description. -pub fn prompt_text(task_info: TaskInfo, what: &str) -> Result> { - // Create a temporary file with some comments inside. - let mut file_path = env::temp_dir(); - let file_name = format!("tau-{}", Timestamp::current_time().0); - file_path.push(file_name); - let mut file = File::create(&file_path)?; - - writeln!(file, "\n# Write your task {what} above this line.")?; - writeln!(file, "# Lines starting with \"#\" will be removed.")?; - writeln!(file, "# An empty {what} aborts the operation.")?; - writeln!(file, "\n# ------------------------ >8 ------------------------")?; - writeln!(file, "# Do not modify or remove the line above.")?; - writeln!(file, "# Everything below it will be ignored.")?; - writeln!(file, "\n{}", taskinfo_table(task_info.clone())?)?; - writeln!(file, "{}", events_table(task_info.clone())?)?; - writeln!(file, "{}", comments_table(task_info)?)?; - - // Try $EDITOR, and if not, fallback to xdg-open. - let editor_argv0 = match env::var("EDITOR") { - Ok(v) => v, - Err(_) => "xdg-open".into(), - }; - - if let Err(e) = Command::new(editor_argv0).arg(&file_path).status() { - error!("Running $EDITOR failed, neither env or xdg-open are available"); - return Err(e.into()) - } - - // Whatever has been written in the temp file will be read here. - let content = fs::read_to_string(&file_path)?; - fs::remove_file(&file_path)?; - - let mut lines = vec![]; - for i in content.lines() { - if !i.starts_with('#') { - lines.push(i.to_string()) - } - if i == "# ------------------------ >8 ------------------------" { - break - } - } - Ok(Some(lines.join("\n"))) -} diff --git a/bin/tau/tau-cli/src/view.rs b/bin/tau/tau-cli/src/view.rs deleted file mode 100644 index 9d0c2bbf7..000000000 --- a/bin/tau/tau-cli/src/view.rs +++ /dev/null @@ -1,287 +0,0 @@ -/* This file is part of DarkFi (https://dark.fi) - * - * Copyright (C) 2020-2024 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::{cmp::Ordering, fmt::Write, str::FromStr}; - -use prettytable::{ - format::{consts::FORMAT_NO_COLSEP, FormatBuilder, LinePosition, LineSeparator}, - row, table, Cell, Row, Table, -}; -use textwrap::fill; - -use darkfi::{ - util::time::{timestamp_to_date, DateFormat}, - Result, -}; - -use crate::{ - primitives::{State, TaskInfo}, - TaskEvent, -}; - -pub fn print_task_list(tasks: Vec, ws: String) -> Result<()> { - let mut tasks = tasks; - - let mut table = Table::new(); - table.set_format( - FormatBuilder::new() - .padding(1, 1) - .separators(&[LinePosition::Title], LineSeparator::new('-', ' ', ' ', ' ')) - .build(), - ); - table.set_titles(row!["ID", "Title", "Tags", "Project", "Assigned", "Due", "Rank"]); - - // group tasks by state. - tasks.sort_by_key(|task| task.state.clone()); - - // sort tasks by there rank only if they are not stopped. - tasks.sort_by(|a, b| { - if a.state != "stop" && b.state != "stop" { - b.rank.partial_cmp(&a.rank).unwrap() - } else { - // because sort_by does not reorder equal elements - Ordering::Equal - } - }); - - let mut min_rank = None; - let mut max_rank = None; - - if let Some(first) = tasks.first() { - max_rank = first.rank; - } - - if let Some(last) = tasks.last() { - min_rank = last.rank; - } - - for task in tasks { - let state = State::from_str(&task.state.clone())?; - - let (max_style, min_style, mid_style, gen_style) = if state.is_start() { - ("bFg", "Fc", "Fg", "Fg") - } else if state.is_pause() { - ("iFYBd", "iFYBd", "iFYBd", "iFYBd") - } else if state.is_stop() { - ("Fr", "Fr", "Fr", "Fr") - } else { - ("", "", "", "") - }; - - let rank = if let Some(r) = task.rank { r.to_string() } else { "".to_string() }; - let mut print_tags = vec![]; - for tag in &task.tags { - let t = tag.replace('+', ""); - print_tags.push(t) - } - - let due_ = match task.due { - Some(ts) => ts.0, - None => 0, - }; - - table.add_row(Row::new(vec![ - Cell::new(&task.id.to_string()).style_spec(gen_style), - Cell::new(&task.title).style_spec(gen_style), - Cell::new(&print_tags.join(", ")).style_spec(gen_style), - Cell::new(&task.project.join(", ")).style_spec(gen_style), - Cell::new(&task.assign.join(", ")).style_spec(gen_style), - Cell::new(×tamp_to_date(due_, DateFormat::Date)).style_spec(gen_style), - if task.rank == max_rank { - Cell::new(&rank).style_spec(max_style) - } else if task.rank == min_rank { - Cell::new(&rank).style_spec(min_style) - } else { - Cell::new(&rank).style_spec(mid_style) - }, - ])); - } - - let workspace = format!("Workspace: {}", ws); - let mut ws_table = table!([workspace]); - ws_table.set_format( - FormatBuilder::new() - .padding(1, 1) - .separators(&[LinePosition::Bottom], LineSeparator::new('-', ' ', ' ', ' ')) - .build(), - ); - - if unsafe { libc::isatty(libc::STDOUT_FILENO) } == 1 { - ws_table.printstd(); - table.printstd(); - } else { - for row in table.row_iter() { - for cell in row.iter() { - print!("{}\t", cell.get_content()); - } - println!(); - } - } - - Ok(()) -} - -pub fn taskinfo_table(taskinfo: TaskInfo) -> Result { - let due_ = match taskinfo.due { - Some(ts) => ts.0, - None => 0, - }; - - let due = timestamp_to_date(due_, DateFormat::Date); - let created_at = timestamp_to_date(taskinfo.created_at.0, DateFormat::DateTime); - let rank = if let Some(r) = taskinfo.rank { r.to_string() } else { "".to_string() }; - - let mut table = table!( - [Bd => "ref_id", &taskinfo.ref_id], - ["workspace", &taskinfo.workspace], - [Bd =>"id", &taskinfo.id.to_string()], - ["owner", &taskinfo.owner], - [Bd =>"title", &taskinfo.title], - ["tags", &taskinfo.tags.join(", ")], - [Bd =>"desc", &taskinfo.desc.to_string()], - ["assign", taskinfo.assign.join(", ")], - [Bd =>"project", taskinfo.project.join(", ")], - ["due", due], - [Bd =>"rank", rank], - ["created_at", created_at], - [Bd =>"current_state", &taskinfo.state]); - - table.set_format( - FormatBuilder::new() - .padding(1, 1) - .separators(&[LinePosition::Title], LineSeparator::new('-', ' ', ' ', ' ')) - .build(), - ); - - table.set_titles(row!["Name", "Value"]); - Ok(table) -} - -pub fn events_table(taskinfo: TaskInfo) -> Result
{ - let (events, timestamps) = &events_as_string(taskinfo.events); - let mut events_table = table!([events, timestamps]); - events_table.set_format(*FORMAT_NO_COLSEP); - events_table.set_titles(row!["Events"]); - Ok(events_table) -} - -pub fn comments_table(taskinfo: TaskInfo) -> Result
{ - let (events, timestamps) = &comments_as_string(taskinfo.events); - let mut comments_table = table!([events, timestamps]); - comments_table.set_format(*FORMAT_NO_COLSEP); - comments_table.set_titles(row!["Comments"]); - - Ok(comments_table) -} - -pub fn print_task_info(taskinfo: TaskInfo) -> Result<()> { - let table = taskinfo_table(taskinfo.clone())?; - table.printstd(); - - let events_table = events_table(taskinfo.clone())?; - events_table.printstd(); - - let comments_table = comments_table(taskinfo)?; - comments_table.printstd(); - - println!(); - - Ok(()) -} - -pub fn events_as_string(events: Vec) -> (String, String) { - let mut events_str = String::new(); - let mut timestamps_str = String::new(); - let width = 50; - for event in events { - if event.action.as_str() == "comment" { - continue - } - writeln!(timestamps_str, "{}", event.timestamp).unwrap(); - match event.action.as_str() { - "title" => { - writeln!(events_str, "- {} changed title to {}", event.author, event.content) - .unwrap(); - } - "rank" => { - writeln!(events_str, "- {} changed rank to {}", event.author, event.content) - .unwrap(); - } - "state" => { - writeln!(events_str, "- {} changed state to {}", event.author, event.content) - .unwrap(); - } - "assign" => { - writeln!(events_str, "- {} assigned {}", event.author, event.content).unwrap(); - } - "project" => { - writeln!(events_str, "- {} changed project to {}", event.author, event.content) - .unwrap(); - } - "tags" => { - writeln!(events_str, "- {} changed tags to {}", event.author, event.content) - .unwrap(); - } - "due" => { - writeln!( - events_str, - "- {} changed due date to {}", - event.author, - timestamp_to_date(event.content.parse::().unwrap_or(0), DateFormat::Date) - ) - .unwrap(); - } - "desc" => { - // wrap long description - let ev_content = - fill(&event.content, textwrap::Options::new(width).subsequent_indent(" ")); - // skip wrapped lines to align timestamp with the first line - for _ in 1..ev_content.lines().count() { - writeln!(timestamps_str, " ").unwrap(); - } - writeln!(events_str, "- {} changed description to: {}", event.author, ev_content) - .unwrap(); - } - _ => {} - } - } - (events_str, timestamps_str) -} - -pub fn comments_as_string(events: Vec) -> (String, String) { - let mut events_str = String::new(); - let mut timestamps_str = String::new(); - let width = 50; - for event in events { - if event.action.as_str() != "comment" { - continue - } - writeln!(timestamps_str, "{}", event.timestamp).unwrap(); - if event.action.as_str() == "comment" { - // wrap long comments - let ev_content = - fill(&event.content, textwrap::Options::new(width).subsequent_indent(" ")); - // skip wrapped lines to align timestamp with the first line - for _ in 1..ev_content.lines().count() { - writeln!(timestamps_str, " ").unwrap(); - } - writeln!(events_str, "{}> {}", event.author, ev_content).unwrap(); - } - } - (events_str, timestamps_str) -} diff --git a/contrib/docker/riscv.Dockerfile b/contrib/docker/riscv.Dockerfile index a5143d530..44ed24840 100644 --- a/contrib/docker/riscv.Dockerfile +++ b/contrib/docker/riscv.Dockerfile @@ -43,8 +43,7 @@ COPY . /opt/darkfi # Cleanup existing binaries RUN rm -rf zkas bin/zkas/zkas darkfid bin/darkfid/darkfid \ darkirc bin/darkirc/darkirc lilith bin/lilith/lilith \ - tau-cli bin/tau/tau-cli/tau-cli taud bin/tau/taud/taud \ - vanityaddr bin/vanityaddr/vanityaddr + taud bin/tau/taud/taud vanityaddr bin/vanityaddr/vanityaddr # Risc-V support is highly experimental so we have to add some hack patches # at Cargo.toml where [patch.crates-io] exists. @@ -64,7 +63,6 @@ RUN make CARGO="cargo +${RUST_VER}" ${BINS} && \ (if [ -e darkfid ]; then cp -a darkfid compiled-bins/; fi;) && \ (if [ -e darkirc ]; then cp -a darkirc compiled-bins/; fi;) && \ (if [ -e lilith ]; then cp -a lilith compiled-bins/; fi;) && \ - (if [ -e "tau-cli" ]; then cp -a tau-cli compiled-bins/; fi;) && \ (if [ -e taud ]; then cp -a taud compiled-bins/; fi;) && \ (if [ -e vanityaddr ]; then cp -a vanityaddr compiled-bins/; fi;) diff --git a/contrib/docker/static.Dockerfile b/contrib/docker/static.Dockerfile index df2c5ad59..4ad7e560e 100644 --- a/contrib/docker/static.Dockerfile +++ b/contrib/docker/static.Dockerfile @@ -53,7 +53,6 @@ RUN make clean && make ${BINS} && mkdir compiled-bins && \ (if [ -e "genev-cli" ]; then cp -a genev-cli compiled-bins/; fi;) && \ (if [ -e genevd ]; then cp -a genevd compiled-bins/; fi;) && \ (if [ -e lilith ]; then cp -a lilith compiled-bins/; fi;) && \ - (if [ -e "tau-cli" ]; then cp -a tau-cli compiled-bins/; fi;) && \ (if [ -e taud ]; then cp -a taud compiled-bins/; fi;) && \ (if [ -e vanityaddr ]; then cp -a vanityaddr compiled-bins/; fi;)