diff options
author | qkzk <qkzk@users.noreply.github.com> | 2022-10-29 22:35:18 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-29 22:35:18 +0200 |
commit | ddf39e6675420fdc83363b757caaac647abff6d4 (patch) | |
tree | 4d5391439dc1749d7a0dc2d591588ea309122e52 | |
parent | fae46d52d6b7e4b48bf9174251db78c2c394eafe (diff) | |
parent | 26ec7fe77dcc56975232bd7c2579827eb1212c05 (diff) |
Merge pull request #34 from qkzk/copy_progress
Copy progress
-rw-r--r-- | Cargo.lock | 81 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | readme.md | 6 | ||||
-rw-r--r-- | src/actioner.rs | 20 | ||||
-rw-r--r-- | src/copy_move.rs | 88 | ||||
-rw-r--r-- | src/fileinfo.rs | 4 | ||||
-rw-r--r-- | src/fm_error.rs | 7 | ||||
-rw-r--r-- | src/help.rs | 1 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/main.rs | 5 | ||||
-rw-r--r-- | src/marks.rs | 2 | ||||
-rw-r--r-- | src/status.rs | 46 | ||||
-rw-r--r-- | src/tab.rs | 39 |
13 files changed, 245 insertions, 57 deletions
@@ -208,6 +208,20 @@ dependencies = [ ] [[package]] +name = "console" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] name = "const_fn" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -484,6 +498,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] name = "encoding" version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -587,6 +607,8 @@ dependencies = [ "clap 4.0.2", "content_inspector", "copypasta", + "fs_extra", + "indicatif", "pdf-extract", "rand", "regex", @@ -605,6 +627,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + +[[package]] name = "fuzzy-matcher" version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -682,6 +710,18 @@ dependencies = [ ] [[package]] +name = "indicatif" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfddc9561e8baf264e0e45e197fd7696320026eb10a8180340debc27b18f535b" +dependencies = [ + "console", + "number_prefix", + "unicode-width", + "vt100", +] + +[[package]] name = "itoa" version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -905,6 +945,12 @@ dependencies = [ ] [[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] name = "objc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1334,7 +1380,7 @@ dependencies = [ "timer", "tuikit", "unicode-width", - "vte", + "vte 0.9.0", ] [[package]] @@ -1502,6 +1548,16 @@ dependencies = [ ] [[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] name = "textwrap" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1702,6 +1758,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] +name = "vt100" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7541312ce0411d878458abf25175d878e8edc38f9f12ee8eed1d65870cacf540" +dependencies = [ + "itoa 1.0.3", + "log", + "unicode-width", + "vte 0.10.1", +] + +[[package]] name = "vte" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1713,6 +1781,17 @@ dependencies = [ ] [[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] name = "vte_generate_state_changes" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -17,6 +17,8 @@ chrono = "*" clap = { version = "4.0", features = ["derive"] } content_inspector = "0.2.4" copypasta = "0.8.1" +fs_extra = "1.2.0" +indicatif = { version = "0.17.1", features= ["in_memory"] } pdf-extract = "0.6.4" rand = "^0" regex = "1" @@ -86,6 +86,7 @@ - [x] display link destination - [x] copy filename/filepath to clipboard with ctrl+c & ctrl+p - [x] filters by ext / name / only dirs / all (aka no filter) +- [x] FIX: broken links aren't shown ## TODO @@ -104,12 +105,13 @@ - [ ] preview images @ranger [ueberzug-rs](https://github.com/Adit-Chauhan/Ueberzug-rs) @[termimage](https://rawcdn.githack.com/nabijaczleweli/termimage/doc/termimage/index.html) - [ ] COPY improvment - [ ] async/threaded copy -- move & delete should be quick enough - - [ ] progress bar for copy + - [x] progress bar for copy + - [x] move/copy progress displayed, nothing else +- [ ] opener crash, right on file crash when in nvim toggleterm ## BUGS - [ ] when opening a file with rifle opener into nvim and closing, the terminal hangs -- [ ] broken links aren't shown ## Sources diff --git a/src/actioner.rs b/src/actioner.rs index 07e97d96..adc509e4 100644 --- a/src/actioner.rs +++ b/src/actioner.rs @@ -91,6 +91,8 @@ impl Actioner { Event::Key(Key::Ctrl('f')) => self.ctrl_f(status), Event::Key(Key::Ctrl('c')) => self.ctrl_c(status), Event::Key(Key::Ctrl('p')) => self.ctrl_p(status), + Event::Key(Key::Ctrl('r')) => self.refresh_selected_view(status), + Event::User(_) => self.refresh_selected_view(status), _ => Ok(()), } } @@ -140,7 +142,8 @@ impl Actioner { | Mode::Newfile | Mode::Exec | Mode::Search - | Mode::Goto => { + | Mode::Goto + | Mode::Filter => { status.selected().event_move_cursor_left(); Ok(()) } @@ -152,14 +155,15 @@ impl Actioner { /// Move right in a string, move to children in normal mode. fn right(&self, status: &mut Status) -> FmResult<()> { match status.selected().mode { - Mode::Normal => status.selected().event_go_to_child(), + Mode::Normal => status.selected().event_child_or_open(), Mode::Rename | Mode::Chmod | Mode::Newdir | Mode::Newfile | Mode::Exec | Mode::Search - | Mode::Goto => { + | Mode::Goto + | Mode::Filter => { status.selected().event_move_cursor_right(); Ok(()) } @@ -176,7 +180,8 @@ impl Actioner { | Mode::Newfile | Mode::Exec | Mode::Search - | Mode::Goto => { + | Mode::Goto + | Mode::Filter => { status.selected().event_delete_char_left(); Ok(()) } @@ -195,7 +200,8 @@ impl Actioner { | Mode::Newfile | Mode::Exec | Mode::Search - | Mode::Goto => { + | Mode::Goto + | Mode::Filter => { status.selected().event_delete_chars_right(); Ok(()) } @@ -332,6 +338,10 @@ impl Actioner { Ok(()) } + fn refresh_selected_view(&self, status: &mut Status) -> FmResult<()> { + status.selected().refresh_view() + } + /// Match read key to a relevent event, depending on keybindings. /// Keybindings are read from `Config`. fn char(&self, status: &mut Status, c: char) -> FmResult<()> { diff --git a/src/copy_move.rs b/src/copy_move.rs new file mode 100644 index 00000000..1675da8d --- /dev/null +++ b/src/copy_move.rs @@ -0,0 +1,88 @@ +use std::fmt::Write; +use std::path::PathBuf; +use std::sync::Arc; +use std::thread; + +use fs_extra; +use indicatif::{InMemoryTerm, ProgressBar, ProgressDrawTarget, ProgressState, ProgressStyle}; +use tuikit::prelude::{Attr, Color, Effect, Event, Term}; + +use crate::fm_error::FmResult; + +fn setup( + action: String, + height: usize, + width: usize, +) -> FmResult<(InMemoryTerm, ProgressBar, fs_extra::dir::CopyOptions)> { + let in_mem = InMemoryTerm::new(height as u16, width as u16); + let pb = ProgressBar::with_draw_target( + Some(100), + ProgressDrawTarget::term_like(Box::new(in_mem.clone())), + ); + pb.set_style( + ProgressStyle::with_template( + "{spinner} {action} [{elapsed}] [{wide_bar}] {percent}% ({eta})", + ) + .unwrap() + .with_key("eta", |state: &ProgressState, w: &mut dyn Write| { + write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap() + }) + .with_key("action", move |_: &ProgressState, w: &mut dyn Write| { + write!(w, "{}", &action).unwrap() + }) + .progress_chars("#>-"), + ); + let options = fs_extra::dir::CopyOptions::new(); + Ok((in_mem, pb, options)) +} + +fn handle_progress_display( + in_mem: &InMemoryTerm, + pb: &ProgressBar, + term: &Arc<Term>, + process_info: fs_extra::TransitProcess, +) -> fs_extra::dir::TransitProcessResult { + pb.set_position(100 * process_info.copied_bytes / process_info.total_bytes); + let _ = term.print_with_attr( + 1, + 0, + &in_mem.to_owned().contents(), + Attr { + fg: Color::CYAN, + bg: Color::default(), + effect: Effect::REVERSE | Effect::BOLD, + }, + ); + let _ = term.present(); + fs_extra::dir::TransitProcessResult::ContinueOrAbort +} + +pub fn copy(sources: Vec<PathBuf>, dest: String, term: Arc<Term>) -> FmResult<()> { + let c_term = term.clone(); + let (height, width) = term.term_size()?; + let (in_mem, pb, options) = setup("copy".to_owned(), height, width)?; + let handle_progress = move |process_info: fs_extra::TransitProcess| { + handle_progress_display(&in_mem, &pb, &term, process_info) + }; + let _ = thread::spawn(move || { + fs_extra::copy_items_with_progress(&sources, &dest, &options, handle_progress) + .unwrap_or_default(); + let _ = c_term.send_event(Event::User(())); + }); + Ok(()) +} + +pub fn mover(sources: Vec<PathBuf>, dest: String, term: Arc<Term>) -> FmResult<()> { + let c_term = term.clone(); + let (height, width) = term.term_size()?; + let (in_mem, pb, options) = setup("move".to_owned(), height, width)?; + let handle_progress = move |process_info: fs_extra::TransitProcess| { + handle_progress_display(&in_mem, &pb, &term, process_info) + }; + let _ = thread::spawn(move || { + fs_extra::move_items_with_progress(&sources, dest, &options, handle_progress) + .unwrap_or_default(); + let _ = c_term.send_event(Event::User(())); + }); + Ok(()) +} diff --git a/src/fileinfo.rs b/src/fileinfo.rs index ed3b9017..bb676bb1 100644 --- a/src/fileinfo.rs +++ b/src/fileinfo.rs @@ -176,7 +176,7 @@ impl FileInfo { ); if let FileKind::SymbolicLink = self.file_kind { repr.push_str(" -> "); - repr.push_str(&self.read_dest().unwrap()); + repr.push_str(&self.read_dest().unwrap_or("Broken link".to_owned())); } Ok(repr) } @@ -295,7 +295,7 @@ impl PathContent { /// Select the next file, if any. pub fn select_next(&mut self) { - if self.selected < self.files.len() - 1 { + if !self.files.is_empty() && self.selected < self.files.len() - 1 { self.files[self.selected].unselect(); self.selected += 1; self.files[self.selected].select(); diff --git a/src/fm_error.rs b/src/fm_error.rs index 77fc8ccc..e861cfa2 100644 --- a/src/fm_error.rs +++ b/src/fm_error.rs @@ -2,6 +2,7 @@ use std::borrow::Borrow; use std::error::Error; use std::fmt; +use fs_extra::error::Error as FsExtraError; use tuikit::error::TuikitError; #[derive(Debug)] @@ -69,4 +70,10 @@ impl From<Box<dyn Error + Send + Sync + 'static>> for FmError { } } +impl From<FsExtraError> for FmError { + fn from(fs_extra_error: FsExtraError) -> Self { + Self::new(&fs_extra_error.to_string()) + } +} + pub type FmResult<T> = Result<T, FmError>; diff --git a/src/help.rs b/src/help.rs index b60e4b53..dd907432 100644 --- a/src/help.rs +++ b/src/help.rs @@ -22,6 +22,7 @@ o: open this file i: open in current nvim session P: preview this file Ctrl+f: fuzzy finder +Ctrl+r: refresh view Ctrl+c: copy filename to clipboard Ctrl+p: copy filepath to clipboard M: Mark current path @@ -5,6 +5,7 @@ pub mod color_cache; pub mod completion; pub mod config; pub mod content_window; +pub mod copy_move; pub mod display; pub mod event_char; pub mod fileinfo; diff --git a/src/main.rs b/src/main.rs index 6d80a0e5..ed368cd1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,12 +27,13 @@ fn reset_cursor(display: &Display) -> FmResult<()> { /// Main function. /// Init the status and display and listen to events from keyboard and mouse. /// The application is redrawn after every event. + fn main() -> FmResult<()> { let config = load_config(CONFIG_PATH); let term = Arc::new(init_term()?); let actioner = Actioner::new(&config.keybindings, term.clone()); - let mut display = Display::new(term, config.colors.clone()); - let mut status = Status::new(Args::parse(), config, display.height()?)?; + let mut display = Display::new(term.clone(), config.colors.clone()); + let mut status = Status::new(Args::parse(), config, display.height()?, term)?; while let Ok(event) = display.term.poll_event() { let _ = display.term.clear(); diff --git a/src/marks.rs b/src/marks.rs index 7a079ad2..74716d53 100644 --- a/src/marks.rs +++ b/src/marks.rs @@ -65,7 +65,7 @@ impl Marks { let mut buf = BufWriter::new(file); for (ch, path) in self.marks.iter() { - let _ = write!(buf, "{}:{}", ch, Self::path_as_string(path)?); + let _ = writeln!(buf, "{}:{}", ch, Self::path_as_string(path)?); } Ok(()) } diff --git a/src/status.rs b/src/status.rs index fbe6e79b..6e99c9cb 100644 --- a/src/status.rs +++ b/src/status.rs @@ -1,15 +1,18 @@ -use regex::Regex; -use skim::SkimItem; use std::collections::HashSet; use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::{self, Path, PathBuf}; use std::sync::Arc; +use regex::Regex; +use skim::SkimItem; +use tuikit::term::Term; + use crate::args::Args; use crate::bulkrename::Bulkrename; use crate::color_cache::ColorCache; use crate::config::Config; +use crate::copy_move::{copy, mover}; use crate::fileinfo::PathContent; use crate::fm_error::{FmError, FmResult}; use crate::last_edition::LastEdition; @@ -36,12 +39,14 @@ pub struct Status { pub marks: Marks, /// Colors for extension pub colors: ColorCache, + /// terminal + term: Arc<Term>, } impl Status { const MAX_PERMISSIONS: u32 = 0o777; - pub fn new(args: Args, config: Config, height: usize) -> FmResult<Self> { + pub fn new(args: Args, config: Config, height: usize, term: Arc<Term>) -> FmResult<Self> { Ok(Self { tabs: vec![Tab::new(args, config, height)?], index: 0, @@ -49,6 +54,7 @@ impl Status { jump_index: 0, marks: Marks::read_from_config_file(), colors: ColorCache::default(), + term, }) } @@ -249,12 +255,7 @@ impl Status { ); std::os::unix::fs::symlink(oldpath, newpath)?; } - - self.flagged.clear(); - self.selected().path_content.reset_files()?; - let len = self.tabs[self.index].path_content.files.len(); - self.tabs[self.index].window.reset(len); - self.reset_statuses() + self.clear_flags_and_reset_view() } pub fn event_bulkrename(&mut self) -> FmResult<()> { @@ -272,28 +273,17 @@ impl Status { } fn cut_or_copy_flagged_files(&mut self, cut_or_copy: CutOrCopy) -> FmResult<()> { - for oldpath in self.flagged.iter() { - let filename = oldpath - .as_path() - .file_name() - .ok_or_else(|| FmError::new("Couldn't parse the filename"))?; - let newpath = self.tabs[self.index] - .path_content - .path - .clone() - .join(filename); - Self::cut_or_copy(cut_or_copy.clone(), oldpath, newpath)? - } - self.clear_flags_and_reset_view() - } - - fn cut_or_copy(cut_or_copy: CutOrCopy, oldpath: &PathBuf, newpath: PathBuf) -> FmResult<()> { + let sources: Vec<PathBuf> = self.flagged.iter().map(|path| path.to_owned()).collect(); + let dest = self + .selected_non_mut() + .path_str() + .ok_or_else(|| FmError::new("unreadable path"))?; if let CutOrCopy::Cut = cut_or_copy { - std::fs::rename(oldpath, newpath)? + mover(sources, dest, self.term.clone())? } else { - std::fs::copy(oldpath, newpath)?; + copy(sources, dest, self.term.clone())? } - Ok(()) + self.clear_flags_and_reset_view() } fn clear_flags_and_reset_view(&mut self) -> FmResult<()> { @@ -129,7 +129,9 @@ impl Tab { } else { self.preview.len() }; - if self.line_index < max_line - ContentWindow::WINDOW_MARGIN_TOP { + if max_line >= ContentWindow::WINDOW_MARGIN_TOP + && self.line_index < max_line - ContentWindow::WINDOW_MARGIN_TOP + { self.line_index += 1; } self.window.scroll_down_one(self.line_index); @@ -231,27 +233,32 @@ impl Tab { self.input.cursor_left() } - pub fn event_go_to_child(&mut self) -> FmResult<()> { + pub fn event_child_or_open(&mut self) -> FmResult<()> { if let FileKind::Directory = self .path_content .selected_file() .ok_or_else(|| FmError::new("Empty directory"))? .file_kind { - let childpath = self - .path_content - .selected_file() - .ok_or_else(|| FmError::new("Empty directory"))? - .path - .clone(); - self.history.push(&childpath); - self.path_content = PathContent::new(childpath, self.show_hidden)?; - self.window.reset(self.path_content.files.len()); - self.line_index = 0; - self.input.cursor_start(); - return Ok(()); + self.go_to_child() + } else { + self.event_open_file() } - Err(FmError::new("File not found")) + } + + fn go_to_child(&mut self) -> FmResult<()> { + let childpath = self + .path_content + .selected_file() + .ok_or_else(|| FmError::new("Empty directory"))? + .path + .clone(); + self.history.push(&childpath); + self.path_content = PathContent::new(childpath, self.show_hidden)?; + self.window.reset(self.path_content.files.len()); + self.line_index = 0; + self.input.cursor_start(); + Ok(()) } pub fn event_move_cursor_right(&mut self) { @@ -426,7 +433,7 @@ impl Tab { .ok_or_else(|| FmError::new("not found"))? .file_kind { - self.event_go_to_child() + self.event_child_or_open() } else { self.event_open_file() } |