├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── demo.gif └── src ├── app.rs ├── avgspeed.rs ├── copy.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ppcp" 3 | version = "0.1.0" 4 | authors = ["Nikita Bilous "] 5 | license = "MIT" 6 | description = "Tool for copying files in console with neat progress bars" 7 | repository = "https://github.com/acidnik/ppcp" 8 | readme = "README.md" 9 | edition = "2021" 10 | 11 | [dependencies] 12 | clap = {version="4.4",features=["cargo"]} 13 | walkdir = "2.2.7" 14 | indicatif = "0.17.7" 15 | pathdiff = "0.2.1" 16 | path_abs = "0.5.0" 17 | thiserror = "1.0.56" 18 | anyhow = "1.0.79" 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Nikita Bilous 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ppcp 2 | ==== 3 | 4 | Command-line tool for copying files and directories with progress bar 5 | 6 | ![](https://github.com/acidnik/ppcp/raw/master/demo.gif) 7 | 8 | WARNING 9 | ======= 10 | 11 | This is an early stage software. Do not use it for anything serious. Please send feedback via github issues 12 | 13 | USAGE 14 | ===== 15 | ``` 16 | # copy file to dir 17 | ppcp 18 | 19 | # copy file to file 20 | ppcp 21 | 22 | # copy dir to dir. directory /path/to/dest/dir will be created 23 | ppcp 24 | 25 | # copy multiple files/dirs 26 | ppcp 27 | ``` 28 | 29 | Error handling 30 | -------------- 31 | Currently, ppcp will panic on any error. TODO is to add a dialog asking abort/skip/skip all/retry/overwrite and command-line option for default actions 32 | 33 | Alternatives 34 | ------------ 35 | ``` 36 | rsync -P 37 | ``` 38 | https://code.lm7.fr/mcy/gcp 39 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acidnik/ppcp/258f91eade20e9bbfb48fa022b0f028e056556ac/demo.gif -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use clap::ArgMatches; 2 | use indicatif::*; 3 | use std::ops::{Deref, DerefMut}; 4 | use std::path::PathBuf; 5 | use std::sync::mpsc::*; 6 | use std::thread; 7 | use std::time::*; 8 | 9 | use crate::avgspeed::*; 10 | use crate::copy::*; 11 | use anyhow::Result; 12 | 13 | /// utility to track changes of variable 14 | #[derive(Default, Clone)] 15 | pub struct TrackChange { 16 | val: T, 17 | changed: bool, 18 | } 19 | 20 | impl TrackChange { 21 | pub fn new(val: T) -> Self { 22 | TrackChange { 23 | val, 24 | changed: false, 25 | } 26 | } 27 | pub fn changed(&mut self) -> bool { 28 | let r = self.changed; 29 | self.changed = false; 30 | r 31 | } 32 | pub fn set(&mut self, val: T) { 33 | if val == self.val { 34 | return; 35 | } 36 | self.changed = true; 37 | self.val = val; 38 | } 39 | } 40 | impl Deref for TrackChange { 41 | type Target = T; 42 | fn deref(&self) -> &T { 43 | &self.val 44 | } 45 | } 46 | impl DerefMut for TrackChange { 47 | fn deref_mut(&mut self) -> &mut T { 48 | self.changed = true; // XXX not checking prev value 49 | &mut self.val 50 | } 51 | } 52 | 53 | pub struct OperationStats { 54 | files_done: u32, 55 | bytes_done: u64, 56 | files_total: TrackChange, 57 | bytes_total: TrackChange, 58 | current_total: TrackChange, 59 | current_done: u64, 60 | current_path: TrackChange, 61 | current_start: Instant, 62 | } 63 | 64 | impl Default for OperationStats { 65 | fn default() -> Self { 66 | OperationStats { 67 | files_done: 0, 68 | bytes_done: 0, 69 | files_total: TrackChange::new(0), 70 | bytes_total: TrackChange::new(0), 71 | current_total: TrackChange::new(0), 72 | current_done: 0, 73 | current_path: TrackChange::new(PathBuf::new()), 74 | current_start: Instant::now(), 75 | } 76 | } 77 | } 78 | 79 | struct SourceWalker {} 80 | 81 | impl SourceWalker { 82 | fn run(tx: Sender<(PathBuf, PathBuf, u64, std::fs::Permissions, bool)>, sources: Vec) { 83 | thread::spawn(move || { 84 | for src in sources { 85 | // let src = PathAbs::new(&src).unwrap().as_path().to_owned(); 86 | let src = src.canonicalize().unwrap(); 87 | for entry in walkdir::WalkDir::new(src.clone()) { 88 | match entry { 89 | Ok(entry) => { 90 | if entry.file_type().is_file() || entry.path_is_symlink() { 91 | let m = entry.metadata().unwrap(); 92 | let size = m.len(); 93 | let perm = m.permissions(); 94 | let is_link = m.file_type().is_symlink(); 95 | tx.send((src.clone(), entry.into_path(), size, perm, is_link)) 96 | .expect("send"); 97 | } 98 | } 99 | Err(_) => { 100 | // TODO 101 | } 102 | } 103 | } 104 | } 105 | }); 106 | } 107 | } 108 | 109 | pub struct App { 110 | pb_curr: ProgressBar, 111 | pb_files: ProgressBar, 112 | pb_bytes: ProgressBar, 113 | pb_name: ProgressBar, 114 | last_update: Instant, 115 | avg_speed: AvgSpeed, 116 | } 117 | 118 | impl App { 119 | pub fn new() -> Self { 120 | let pb_name = ProgressBar::with_draw_target(Some(10_u64), ProgressDrawTarget::stdout()); 121 | // \u{00A0} (nbsp) to make indicatif draw lines as wide as possible 122 | // otherwise it leaves leftovers from prev lines at the end of lines 123 | pb_name.set_style( 124 | ProgressStyle::default_spinner() 125 | .template("{spinner} {wide_msg} \u{00A0}") 126 | .unwrap(), 127 | ); 128 | let pb_curr = ProgressBar::new(10); 129 | pb_curr.set_style(ProgressStyle::default_bar() 130 | .template("current {bar:40.} {bytes:>10} / {total_bytes:<10} {elapsed:>5} ETA {eta} {wide_msg} \u{00A0}").unwrap() 131 | ); 132 | let pb_files = ProgressBar::with_draw_target(Some(10_u64), ProgressDrawTarget::stdout()); 133 | pb_files.set_style( 134 | ProgressStyle::default_bar() 135 | .template("files {bar:40} {pos:>10} / {len:<10} {wide_msg} \u{00A0}") 136 | .unwrap(), 137 | ); 138 | let pb_bytes = ProgressBar::with_draw_target(Some(10), ProgressDrawTarget::stdout()); 139 | pb_bytes.set_style(ProgressStyle::default_bar() 140 | .template("bytes {bar:40} {bytes:>10} / {total_bytes:<10} {elapsed:>5} ETA {eta} {wide_msg} \u{00A0}").unwrap() 141 | // .progress_chars("=> ") 142 | ); 143 | let multi_pb = MultiProgress::new(); 144 | let pb_name = multi_pb.add(pb_name); 145 | let pb_curr = multi_pb.add(pb_curr); 146 | let pb_files = multi_pb.add(pb_files); 147 | let pb_bytes = multi_pb.add(pb_bytes); 148 | multi_pb.set_move_cursor(true); 149 | 150 | App { 151 | pb_curr, 152 | pb_files, 153 | pb_bytes, 154 | pb_name, 155 | last_update: Instant::now(), 156 | avg_speed: AvgSpeed::new(), 157 | } 158 | } 159 | 160 | // fn error_ask(&self, err: String) -> OperationControl { 161 | // OperationControl::Skip // TODO 162 | // } 163 | 164 | fn update_progress(&mut self, stats: &mut OperationStats) { 165 | // return; 166 | if Instant::now().duration_since(self.last_update) < Duration::from_millis(97) { 167 | return; 168 | } 169 | self.last_update = Instant::now(); 170 | self.pb_name.tick(); // spin the spinner 171 | if stats.current_path.changed() { 172 | self.pb_name 173 | .set_message(format!("{}", stats.current_path.display())); 174 | self.pb_curr.set_length(*stats.current_total); 175 | stats.current_start = Instant::now(); // This is inaccurate. Init current_start in copy worker and send instant with path? 176 | self.pb_curr.reset_elapsed(); 177 | self.pb_curr.reset_eta(); 178 | } 179 | self.pb_curr.set_position(stats.current_done); 180 | self.avg_speed.add(stats.bytes_done); 181 | self.pb_curr 182 | .set_message(format!("{}/s", HumanBytes(self.avg_speed.get()))); 183 | 184 | if stats.files_total.changed() { 185 | self.pb_files.set_length(*stats.files_total); 186 | } 187 | self.pb_files.set_position(u64::from(stats.files_done)); 188 | 189 | if stats.bytes_total.changed() { 190 | self.pb_bytes.set_length(*stats.bytes_total); 191 | } 192 | self.pb_bytes.set_position(stats.bytes_done); 193 | } 194 | 195 | pub fn run(&mut self, matches: &ArgMatches) -> Result<()> { 196 | // for sending errors, progress info and other events from worker to ui: 197 | let (worker_tx, worker_rx) = channel::(); 198 | // TODO for sending user input (retry/skip/abort) to worker: 199 | let (_user_tx, user_rx) = channel::(); 200 | // fs walker sends files to operation 201 | let (src_tx, src_rx) = channel(); 202 | 203 | let operation = OperationCopy::new(matches, user_rx, worker_tx, src_rx)?; 204 | 205 | let search_path = operation.search_path(); 206 | assert!(!search_path.is_empty()); 207 | SourceWalker::run(src_tx, search_path); 208 | 209 | let mut stats: OperationStats = Default::default(); 210 | 211 | let start = Instant::now(); 212 | 213 | while let Ok(event) = worker_rx.recv() { 214 | match event { 215 | WorkerEvent::Stat(StatsChange::FileDone) => { stats.files_done += 1 } 216 | WorkerEvent::Stat(StatsChange::BytesTotal(n)) => { 217 | *stats.bytes_total += n; 218 | *stats.files_total += 1; 219 | }, 220 | WorkerEvent::Stat(StatsChange::Current(p, chunk, done, todo)) => { 221 | stats.current_path.set(p); 222 | stats.current_total.set(todo); 223 | stats.current_done = done; 224 | stats.bytes_done += u64::from(chunk); 225 | } 226 | // WorkerEvent::Status(OperationStatus::Error(err)) => { 227 | // let answer = self.error_ask(err); 228 | // user_tx.send(answer).expect("send"); 229 | // }, 230 | // _ => {}, 231 | } 232 | self.update_progress(&mut stats); 233 | } 234 | self.pb_curr.finish(); 235 | self.pb_files.finish(); 236 | self.pb_bytes.finish(); 237 | self.pb_name.finish(); 238 | let ela = Instant::now().duration_since(start); 239 | println!( 240 | "copied {} files ({}) in {} {}/s", 241 | *stats.files_total, 242 | HumanBytes(*stats.bytes_total), 243 | HumanDuration(ela), 244 | HumanBytes(get_speed(*stats.bytes_total, &ela)) 245 | ); 246 | Ok(()) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/avgspeed.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | use std::ops::*; 3 | use std::time::{Duration, Instant}; 4 | 5 | /// moving (rolling) average 6 | pub struct RollingAverage { 7 | hist: VecDeque, 8 | sum: T, 9 | size: usize, 10 | } 11 | 12 | impl RollingAverage 13 | // moments like this I miss dating duck typing 14 | where 15 | T: AddAssign 16 | + SubAssign 17 | + Div 18 | + std::convert::From 19 | + std::convert::From<::Output> 20 | + Copy, 21 | { 22 | pub fn new(size: usize) -> Self { 23 | RollingAverage { 24 | hist: VecDeque::with_capacity(size), 25 | sum: 0_u64.into(), 26 | size, 27 | } 28 | } 29 | pub fn add(&mut self, val: T) { 30 | self.hist.push_back(val); 31 | self.sum += val; 32 | if self.hist.len() > self.size { 33 | self.sum -= self.hist.pop_front().unwrap(); 34 | } 35 | } 36 | pub fn get(&self) -> T { 37 | (self.sum / (self.hist.len() as u64).into()).into() 38 | } 39 | } 40 | 41 | pub struct AvgSpeed { 42 | avg: RollingAverage, 43 | prev_bytes: u64, 44 | last_chunk: Instant, 45 | } 46 | 47 | impl AvgSpeed { 48 | pub fn new() -> Self { 49 | AvgSpeed { 50 | avg: RollingAverage::new(100), 51 | prev_bytes: 0, 52 | last_chunk: Instant::now(), 53 | } 54 | } 55 | pub fn add(&mut self, total_bytes: u64) { 56 | let db = total_bytes - self.prev_bytes; 57 | self.avg.add(get_speed( 58 | db, 59 | &Instant::now().duration_since(self.last_chunk), 60 | )); 61 | self.last_chunk = Instant::now(); 62 | self.prev_bytes = total_bytes; 63 | } 64 | pub fn get(&self) -> u64 { 65 | self.avg.get() 66 | } 67 | } 68 | 69 | pub fn get_speed(x: u64, ela: &Duration) -> u64 { 70 | if *ela >= Duration::from_nanos(1) && x < std::u64::MAX / 1_000_000_000 { 71 | x * 1_000_000_000 / ela.as_nanos() as u64 72 | } else if *ela >= Duration::from_micros(1) && x < std::u64::MAX / 1_000_000 { 73 | x * 1_000_000 / ela.as_micros() as u64 74 | } else if *ela >= Duration::from_millis(1) && x < std::u64::MAX / 1_000 { 75 | x * 1_000 / ela.as_millis() as u64 76 | } else if *ela >= Duration::from_secs(1) { 77 | x / ela.as_secs() 78 | } else { 79 | // what the hell are you? 80 | std::u64::MAX 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/copy.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::ArgMatches; 3 | use std::collections::HashSet; 4 | use std::fs::{self, *}; 5 | use std::io::{self, *}; 6 | use std::path::PathBuf; 7 | use std::sync::mpsc::{channel, Receiver, Sender}; 8 | use std::thread; 9 | use std::time::Duration; 10 | use thiserror::Error; 11 | 12 | #[derive(Clone, PartialEq, Debug)] 13 | pub enum StatsChange { 14 | FileDone, 15 | BytesTotal(u64), 16 | Current(PathBuf, u32, u64, u64), 17 | } 18 | 19 | #[derive(Clone, PartialEq, Debug)] 20 | pub enum OperationStatus { 21 | // Running, 22 | // Error(String), 23 | // Done, 24 | } 25 | 26 | pub enum OperationControl { 27 | // Abort, 28 | // Skip, 29 | // Retry, 30 | // SkipAll, 31 | } 32 | 33 | #[derive(Debug)] 34 | pub enum WorkerEvent { 35 | Stat(StatsChange), 36 | // Status(OperationStatus), 37 | } 38 | 39 | pub trait Operation { 40 | fn search_path(&self) -> Vec; 41 | } 42 | 43 | pub struct OperationCopy { 44 | sources: Vec, 45 | } 46 | 47 | impl Operation for OperationCopy { 48 | fn search_path(&self) -> Vec { 49 | self.sources.clone() 50 | } 51 | } 52 | 53 | #[derive(Error, Debug)] 54 | pub enum OperationError { 55 | #[error("Arguments missing")] 56 | ArgumentsMissing, 57 | #[error("Can not copy directory {src:?} to file {dest:?}")] 58 | DirOverFile { src: String, dest: String }, 59 | } 60 | 61 | impl OperationCopy { 62 | pub fn new( 63 | matches: &ArgMatches, 64 | _user_rx: Receiver, 65 | worker_tx: Sender, 66 | src_rx: Receiver<(PathBuf, PathBuf, u64, Permissions, bool)>, 67 | ) -> Result { 68 | let source: Vec = matches 69 | .get_many::("source") 70 | .ok_or(OperationError::ArgumentsMissing)? 71 | .cloned() 72 | .collect(); 73 | 74 | let dest: &PathBuf = matches 75 | .get_one::("destination") 76 | .ok_or(OperationError::ArgumentsMissing)?; 77 | 78 | let dest_parent = dest 79 | .parent() 80 | .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "dest.parent?"))? 81 | .to_owned(); 82 | if !dest_parent.exists() { 83 | fs::create_dir_all(&dest_parent)?; 84 | } 85 | let (dest_is_file, dest_dir) = if !dest.exists() { 86 | // if dest not exists - consider it a dir 87 | // cp /path/to/dir . -> must create dir and set it as dest 88 | // cp /dir1 /dir2 /file . -> cp /dir1/* ./dir; cp /dir2/* ./dir2; cp /file ./ 89 | (false, dest.clone()) 90 | } else { 91 | let meta = fs::symlink_metadata(dest)?; 92 | if meta.is_file() { 93 | // cp /path/to/file.txt ./here/file.txt: dest_dir = ./here 94 | (true, dest_parent) 95 | } else { 96 | // cp /path/to/dir ./here/foo -> copy to/dir/* ./here/foo 97 | (false, dest.clone()) 98 | } 99 | }; 100 | for src in source.iter() { 101 | let meta = fs::symlink_metadata(src)?; 102 | if dest_is_file && meta.is_dir() { 103 | Err(OperationError::DirOverFile { 104 | src: src.display().to_string(), 105 | dest: dest.display().to_string(), 106 | })? 107 | } 108 | } 109 | if !dest_is_file && !dest_dir.exists() { 110 | fs::create_dir_all(&dest_dir)? 111 | } 112 | let dest_dir = dest_dir.canonicalize()?; 113 | 114 | let (q_tx, q_rx) = channel::<(PathBuf, PathBuf, u64, Permissions, bool)>(); // source_path, source_file, total, 115 | let (d_tx, d_rx) = channel::<(PathBuf, u32, u64, u64)>(); // src_path, chunk, done, total 116 | CopyWorker::run(dest_dir, d_tx, q_rx); 117 | // MockCopyWorker::run(dest_dir, d_tx, q_rx); 118 | 119 | { 120 | let worker_tx = worker_tx.clone(); 121 | thread::spawn(move || { 122 | for (p, chunk, done, todo) in d_rx.iter() { 123 | worker_tx 124 | .send(WorkerEvent::Stat(StatsChange::Current( 125 | p, chunk, done, todo, 126 | ))) 127 | .expect("send"); 128 | if done >= todo { 129 | worker_tx 130 | .send(WorkerEvent::Stat(StatsChange::FileDone)) 131 | .expect("send"); 132 | } 133 | } 134 | }); 135 | } 136 | 137 | thread::spawn(move || { 138 | // let mut question = "".to_string(); 139 | // let mut skip_all = true; 140 | while let Ok((src, path, size, perm, is_link)) = src_rx.recv() { 141 | worker_tx 142 | .send(WorkerEvent::Stat(StatsChange::BytesTotal(size))) 143 | .expect("send"); 144 | 145 | q_tx.send((src, path, size, perm, is_link)).expect("send"); 146 | } 147 | }); 148 | Ok(OperationCopy { sources: source }) 149 | } 150 | } 151 | 152 | struct CopyWorker {} 153 | 154 | impl CopyWorker { 155 | fn run( 156 | dest: PathBuf, 157 | tx: Sender<(PathBuf, u32, u64, u64)>, 158 | rx: Receiver<(PathBuf, PathBuf, u64, Permissions, bool)>, 159 | ) { 160 | thread::spawn(move || { 161 | let mut mkdird = HashSet::new(); 162 | for (src, p, sz, perm, is_link) in rx.iter() { 163 | let r = if src.is_file() { 164 | p.file_name().unwrap().into() 165 | } else { 166 | // cp /dir1 d/ 167 | // src = /dir1 p = /dir1/inner/inner2/f.txt 168 | // dest_dir = d/dir1/inner/inner2/f.txt 169 | // diff(/dir1 /dir1/inner/inner2/f.txt) = inner/inner2/f.txt 170 | let p_parent: PathBuf = src.file_name().unwrap().into(); 171 | p_parent.join(pathdiff::diff_paths(&p, &src).unwrap()) 172 | }; 173 | let dest_file = dest.join(r.clone()); 174 | let dest_dir = dest_file.parent().unwrap().to_owned(); 175 | if !mkdird.contains(&dest_dir) { 176 | // TODO : this will make dir foo/bar/baz and then foo/bar again 177 | fs::create_dir_all(&dest_dir).unwrap(); 178 | mkdird.insert(dest_dir.clone()); 179 | } 180 | 181 | if is_link { 182 | let link_dest = std::fs::read_link(&p).unwrap(); 183 | std::os::unix::fs::symlink(&link_dest, &dest_file).unwrap_or_else(|err| { 184 | eprintln!("Error creating symlink: {}", err); 185 | }); // FIXME 186 | tx.send((p, sz as u32, sz, sz)).unwrap(); 187 | continue; 188 | } 189 | 190 | let fwh = File::create(&dest_file).unwrap(); 191 | fwh.set_permissions(perm).unwrap_or(()); // works on unix fs only 192 | 193 | let mut fr = BufReader::new(File::open(&p).unwrap()); 194 | let mut fw = BufWriter::new(fwh); 195 | let mut buf = vec![0; 10_000_000]; 196 | let mut s: u64 = 0; 197 | loop { 198 | match fr.read(&mut buf) { 199 | Ok(ds) => { 200 | s += ds as u64; 201 | if ds == 0 { 202 | break; 203 | } 204 | fw.write_all(&buf[..ds]).unwrap(); 205 | tx.send((p.clone(), ds as u32, s, sz)).unwrap(); 206 | } 207 | Err(e) => { 208 | println!("{:?}", e); 209 | break; 210 | } 211 | } 212 | } 213 | } 214 | }); 215 | } 216 | } 217 | 218 | struct _MockCopyWorker {} 219 | 220 | impl _MockCopyWorker { 221 | fn _run( 222 | _dest: PathBuf, 223 | tx: Sender<(PathBuf, u32, u64, u64)>, 224 | rx: Receiver<(PathBuf, PathBuf, u64)>, 225 | ) { 226 | let chunk = 1_048_576; 227 | thread::spawn(move || { 228 | for (_src, p, sz) in rx.iter() { 229 | let mut s = 0; 230 | while s < sz { 231 | let ds = if s + chunk > sz { sz - s } else { chunk }; 232 | s += ds; 233 | let delay = Duration::from_micros(ds / chunk * 100_000); 234 | tx.send((p.clone(), ds as u32, s, sz)).unwrap(); 235 | thread::sleep(delay); 236 | } 237 | } 238 | }); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{command, value_parser, ArgAction}; 2 | use std::error; 3 | use std::path::PathBuf; 4 | mod app; 5 | mod avgspeed; 6 | mod copy; 7 | use clap::Arg; 8 | fn main() -> Result<(), Box> { 9 | let matches = command!() 10 | .version("0.0.1") 11 | .author("Nikita Bilous ") 12 | .about("Copy files in console with progress bar") 13 | .arg( 14 | Arg::new("source") 15 | .required(true) 16 | .num_args(1..) 17 | .action(ArgAction::Append) 18 | .index(1) 19 | .value_parser(value_parser!(PathBuf)), 20 | ) 21 | .arg( 22 | Arg::new("destination") 23 | .required(true) 24 | .index(2) 25 | // .last(true) 26 | .value_parser(value_parser!(PathBuf)), 27 | ) 28 | .get_matches(); 29 | 30 | let mut app = app::App::new(); 31 | app.run(&matches)?; 32 | Ok(()) 33 | } 34 | --------------------------------------------------------------------------------