From 24a7f2bc31e011fa3af6988623fb3d1d43e36280 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 19 Nov 2024 20:17:58 -0700 Subject: [PATCH] Select result of operation, fixes #500 --- src/app.rs | 85 +++++++++----- src/dialog.rs | 2 +- src/operation/mod.rs | 224 ++++++++++++++++++++++--------------- src/operation/recursive.rs | 15 ++- src/tab.rs | 28 ++--- 5 files changed, 215 insertions(+), 139 deletions(-) diff --git a/src/app.rs b/src/app.rs index 7388ec1..3d44d78 100644 --- a/src/app.rs +++ b/src/app.rs @@ -64,7 +64,7 @@ use crate::{ localize::LANGUAGE_SORTER, menu, mime_app, mime_icon, mounter::{MounterAuth, MounterItem, MounterItems, MounterKey, MounterMessage, MOUNTERS}, - operation::{Controller, Operation, ReplaceResult}, + operation::{Controller, Operation, OperationSelection, ReplaceResult}, spawn_detached::spawn_detached, tab::{self, HeadingOptions, ItemMetadata, Location, Tab, HOVER_DURATION}, }; @@ -308,7 +308,7 @@ pub enum Message { PasteContents(PathBuf, ClipboardPaste), PendingCancel(u64), PendingCancelAll, - PendingComplete(u64), + PendingComplete(u64, OperationSelection), PendingDismiss, PendingError(u64, String), PendingPause(u64, bool), @@ -335,7 +335,7 @@ pub enum Message { Location, Option, Vec, - Option, + Option>, ), TabView(Option, tab::View), ToggleContextPage(ContextPage), @@ -655,7 +655,7 @@ impl App { &mut self, location: Location, activate: bool, - selection_path: Option, + selection_paths: Option>, ) -> (Entity, Task) { let mut tab = Tab::new(location.clone(), self.config.tab); tab.mode = match self.mode { @@ -683,7 +683,7 @@ impl App { Task::batch([ self.update_title(), self.update_watcher(), - self.rescan_tab(entity, location, selection_path), + self.rescan_tab(entity, location, selection_paths), ]), ) } @@ -692,9 +692,9 @@ impl App { &mut self, location: Location, activate: bool, - selection_path: Option, + selection_paths: Option>, ) -> Task { - self.open_tab_entity(location, activate, selection_path).1 + self.open_tab_entity(location, activate, selection_paths).1 } fn operation(&mut self, operation: Operation) { @@ -717,13 +717,38 @@ impl App { } } + fn rescan_operation_selection(&mut self, op_sel: OperationSelection) -> Task { + log::info!("rescan_operation_selection {:?}", op_sel); + let entity = self.tab_model.active(); + let Some(tab) = self.tab_model.data::(entity) else { + return Task::none(); + }; + let Some(ref items) = tab.items_opt() else { + return Task::none(); + }; + for item in items.iter() { + if item.selected { + if let Some(path) = item.path_opt() { + if op_sel.selected.contains(path) || op_sel.ignored.contains(path) { + // Ignore if path in selected or ignored paths + continue; + } + } + + // Return if there is a previous selection not matching + return Task::none(); + } + } + self.rescan_tab(entity, tab.location.clone(), Some(op_sel.selected)) + } + fn rescan_tab( &mut self, entity: Entity, location: Location, - selection_path: Option, + selection_paths: Option>, ) -> Task { - log::info!("rescan_tab {entity:?} {location:?} {selection_path:?}"); + log::info!("rescan_tab {entity:?} {location:?} {selection_paths:?}"); let icon_sizes = self.config.tab.icon_sizes; Task::perform( async move { @@ -734,7 +759,7 @@ impl App { location, parent_item_opt, items, - selection_path, + selection_paths, )), Err(err) => { log::warn!("failed to rescan: {}", err); @@ -2264,7 +2289,7 @@ impl Application for App { Some(self.open_tab( Location::Path(parent.to_path_buf()), true, - Some(path), + Some(vec![path]), )) } else { None @@ -2381,9 +2406,9 @@ impl Application for App { self.progress_operations.remove(&id); } } - Message::PendingComplete(id) => { - let mut commands = Vec::with_capacity(3); - + Message::PendingComplete(id, op_sel) => { + let mut commands = Vec::with_capacity(4); + // Show toast for some operations if let Some((op, _, _)) = self.pending_operations.remove(&id) { if let Some(description) = op.toast() { if let Operation::Delete { ref paths } = op { @@ -2412,6 +2437,8 @@ impl Application for App { } // Potentially show a notification commands.push(self.update_notification()); + // Rescan and select based on operation + commands.push(self.rescan_operation_selection(op_sel)); // Manually rescan any trash tabs after any operation is completed commands.push(self.rescan_trash()); // if search is active, update "search" tab view @@ -2579,7 +2606,7 @@ impl Application for App { } } Message::RestoreFromTrash(entity_opt) => { - let mut paths = Vec::new(); + let mut trash_items = Vec::new(); let entity = entity_opt.unwrap_or_else(|| self.tab_model.active()); if let Some(tab) = self.tab_model.data_mut::(entity) { if let Some(items) = tab.items_opt() { @@ -2587,7 +2614,7 @@ impl Application for App { if item.selected { match &item.metadata { ItemMetadata::Trash { entry, .. } => { - paths.push(entry.clone()); + trash_items.push(entry.clone()); } _ => { //TODO: error on trying to restore non-trash file? @@ -2597,8 +2624,8 @@ impl Application for App { } } } - if !paths.is_empty() { - self.operation(Operation::Restore { paths }); + if !trash_items.is_empty() { + self.operation(Operation::Restore { items: trash_items }); } } Message::SearchActivate => { @@ -2735,14 +2762,14 @@ impl Application for App { config_set!(favorites, favorites); commands.push(self.update_config()); } - tab::Command::ChangeLocation(tab_title, tab_path, selection_path) => { + tab::Command::ChangeLocation(tab_title, tab_path, selection_paths) => { self.activate_nav_model_location(&tab_path); self.tab_model.text_set(entity, tab_title); commands.push(Task::batch([ self.update_title(), self.update_watcher(), - self.rescan_tab(entity, tab_path, selection_path), + self.rescan_tab(entity, tab_path, selection_paths), ])); } tab::Command::DropFiles(to, from) => { @@ -2816,14 +2843,14 @@ impl Application for App { }; return self.open_tab(location, true, None); } - Message::TabRescan(entity, location, parent_item_opt, items, selection_path) => { + Message::TabRescan(entity, location, parent_item_opt, items, selection_paths) => { match self.tab_model.data_mut::(entity) { Some(tab) => { if location == tab.location { tab.parent_item_opt = parent_item_opt; tab.set_items(items); - if let Some(selection_path) = selection_path { - tab.select_path(selection_path); + if let Some(selection_paths) = selection_paths { + tab.select_paths(selection_paths); } } } @@ -2882,8 +2909,8 @@ impl Application for App { Message::UndoTrashStart(paths) }); } - Message::UndoTrashStart(paths) => { - self.operation(Operation::Restore { paths }); + Message::UndoTrashStart(items) => { + self.operation(Operation::Restore { items }); } Message::WindowClose => { if let Some(window_id) = self.window_id_opt.take() { @@ -4420,8 +4447,12 @@ impl Application for App { stream::channel(16, move |msg_tx| async move { let msg_tx = Arc::new(tokio::sync::Mutex::new(msg_tx)); match pending_operation.perform(id, &msg_tx, cancelled).await { - Ok(()) => { - let _ = msg_tx.lock().await.send(Message::PendingComplete(id)).await; + Ok(result_paths) => { + let _ = msg_tx + .lock() + .await + .send(Message::PendingComplete(id, result_paths)) + .await; } Err(err) => { let _ = msg_tx diff --git a/src/dialog.rs b/src/dialog.rs index 794b63a..8a4ca2f 100644 --- a/src/dialog.rs +++ b/src/dialog.rs @@ -1381,7 +1381,7 @@ impl Application for App { tab::Command::Action(action) => { commands.push(self.update(Message::from(action.message()))); } - tab::Command::ChangeLocation(_tab_title, _tab_path, _selection_path) => { + tab::Command::ChangeLocation(_tab_title, _tab_path, _selection_paths) => { commands.push(Task::batch([self.update_watcher(), self.rescan_tab()])); } tab::Command::Iced(iced_command) => { diff --git a/src/operation/mod.rs b/src/operation/mod.rs index 730540b..f6016fb 100644 --- a/src/operation/mod.rs +++ b/src/operation/mod.rs @@ -324,55 +324,6 @@ pub enum ReplaceResult { Cancel, } -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -pub enum Operation { - /// Compress files - Compress { - paths: Vec, - to: PathBuf, - archive_type: ArchiveType, - }, - /// Copy items - Copy { - paths: Vec, - to: PathBuf, - }, - /// Move items to the trash - Delete { - paths: Vec, - }, - /// Empty the trash - EmptyTrash, - /// Uncompress files - Extract { - paths: Vec, - to: PathBuf, - }, - /// Move items - Move { - paths: Vec, - to: PathBuf, - }, - NewFile { - path: PathBuf, - }, - NewFolder { - path: PathBuf, - }, - Rename { - from: PathBuf, - to: PathBuf, - }, - /// Restore a path from the trash - Restore { - paths: Vec, - }, - /// Set executable and launch - SetExecutableAndLaunch { - path: PathBuf, - }, -} - async fn copy_or_move( paths: Vec, to: PathBuf, @@ -380,9 +331,9 @@ async fn copy_or_move( id: u64, msg_tx: &Arc>>, controller: Controller, -) -> Result<(), String> { +) -> Result { let msg_tx = msg_tx.clone(); - tokio::task::spawn_blocking(move || -> Result<(), String> { + tokio::task::spawn_blocking(move || -> Result { log::info!( "{} {:?} to {:?}", if moving { "Move" } else { "Copy" }, @@ -446,7 +397,7 @@ async fn copy_or_move( context.recursive_copy_or_move(from_to_pairs, moving)?; - Ok(()) + Ok(context.op_sel) }) .await .map_err(err_str)? @@ -553,6 +504,63 @@ fn paths_parent_name<'a>(paths: &'a Vec) -> Cow<'a, str> { file_name(parent) } +#[derive(Clone, Debug, Default)] +pub struct OperationSelection { + // Paths to ignore if they are already selected + pub ignored: Vec, + // Paths to select + pub selected: Vec, +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum Operation { + /// Compress files + Compress { + paths: Vec, + to: PathBuf, + archive_type: ArchiveType, + }, + /// Copy items + Copy { + paths: Vec, + to: PathBuf, + }, + /// Move items to the trash + Delete { + paths: Vec, + }, + /// Empty the trash + EmptyTrash, + /// Uncompress files + Extract { + paths: Vec, + to: PathBuf, + }, + /// Move items + Move { + paths: Vec, + to: PathBuf, + }, + NewFile { + path: PathBuf, + }, + NewFolder { + path: PathBuf, + }, + Rename { + from: PathBuf, + to: PathBuf, + }, + /// Restore a path from the trash + Restore { + items: Vec, + }, + /// Set executable and launch + SetExecutableAndLaunch { + path: PathBuf, + }, +} + impl Operation { pub fn pending_text(&self, percent: i32, state: ControllerState) -> String { let progress = || match state { @@ -610,7 +618,7 @@ impl Operation { Self::Rename { from, to } => { fl!("renaming", from = file_name(from), to = file_name(to)) } - Self::Restore { paths } => fl!("restoring", items = paths.len(), progress = progress()), + Self::Restore { items } => fl!("restoring", items = items.len(), progress = progress()), Self::SetExecutableAndLaunch { path } => { fl!("setting-executable-and-launching", name = file_name(path)) } @@ -661,7 +669,7 @@ impl Operation { parent = parent_name(path) ), Self::Rename { from, to } => fl!("renamed", from = file_name(from), to = file_name(to)), - Self::Restore { paths } => fl!("restored", items = paths.len()), + Self::Restore { items } => fl!("restored", items = items.len()), Self::SetExecutableAndLaunch { path } => { fl!("set-executable-and-launched", name = file_name(path)) } @@ -701,7 +709,7 @@ impl Operation { id: u64, msg_tx: &Arc>>, controller: Controller, - ) -> Result<(), String> { + ) -> Result { let _ = msg_tx .lock() .await @@ -709,18 +717,23 @@ impl Operation { .await; //TODO: IF ERROR, RETURN AN Operation THAT CAN UNDO THE CURRENT STATE - match self { + let paths = match self { Self::Compress { paths, to, archive_type, } => { let msg_tx = msg_tx.clone(); - tokio::task::spawn_blocking(move || -> Result<(), String> { + tokio::task::spawn_blocking(move || -> Result { let Some(relative_root) = to.parent() else { return Err(format!("path {:?} has no parent directory", to)); }; + let op_sel = OperationSelection { + ignored: paths.clone(), + selected: vec![to.clone()], + }; + let mut paths = paths; for path in paths.clone().iter() { if path.is_dir() { @@ -844,14 +857,14 @@ impl Operation { } } - Ok(()) + Ok(op_sel) }) .await .map_err(err_str)? - .map_err(err_str)?; + .map_err(err_str)? } Self::Copy { paths, to } => { - copy_or_move(paths, to, false, id, msg_tx, controller).await?; + copy_or_move(paths, to, false, id, msg_tx, controller).await? } Self::Delete { paths } => { let total = paths.len(); @@ -873,6 +886,7 @@ impl Operation { .map_err(err_str)?; //TODO: items_opt allows for easy restore } + OperationSelection::default() } Self::EmptyTrash => { #[cfg(any( @@ -908,11 +922,13 @@ impl Operation { .await .map_err(err_str)??; } + OperationSelection::default() } Self::Extract { paths, to } => { let msg_tx = msg_tx.clone(); - tokio::task::spawn_blocking(move || -> Result<(), String> { + tokio::task::spawn_blocking(move || -> Result { let total_paths = paths.len(); + let mut op_sel = OperationSelection::default(); for (i, path) in paths.iter().enumerate() { controller.check()?; @@ -937,6 +953,9 @@ impl Operation { } } + op_sel.ignored.push(path.clone()); + op_sel.selected.push(new_dir.clone()); + let msg_tx = msg_tx.clone(); let controller = controller.clone(); let mime = mime_for_path(&path); @@ -968,7 +987,7 @@ impl Operation { .map(io::BufReader::new) .map(bzip2::read::BzDecoder::new) .map(tar::Archive::new) - .and_then(|mut archive| archive.unpack(new_dir)) + .and_then(|mut archive| archive.unpack(&new_dir)) .map_err(err_str)? } #[cfg(feature = "liblzma")] @@ -977,7 +996,7 @@ impl Operation { .map(io::BufReader::new) .map(liblzma::read::XzDecoder::new) .map(tar::Archive::new) - .and_then(|mut archive| archive.unpack(new_dir)) + .and_then(|mut archive| archive.unpack(&new_dir)) .map_err(err_str)? } _ => Err(format!("unsupported mime type {:?}", mime))?, @@ -985,38 +1004,50 @@ impl Operation { } } - Ok(()) + Ok(op_sel) }) .await .map_err(err_str)? - .map_err(err_str)?; + .map_err(err_str)? } Self::Move { paths, to } => { - copy_or_move(paths, to, true, id, msg_tx, controller).await?; + copy_or_move(paths, to, true, id, msg_tx, controller).await? } Self::NewFolder { path } => { - controller.check()?; - - tokio::task::spawn_blocking(|| fs::create_dir(path)) - .await - .map_err(err_str)? - .map_err(err_str)?; + tokio::task::spawn_blocking(move || -> Result { + controller.check()?; + fs::create_dir(&path).map_err(err_str)?; + Ok(OperationSelection { + ignored: Vec::new(), + selected: vec![path], + }) + }) + .await + .map_err(err_str)?? } Self::NewFile { path } => { - controller.check()?; - - tokio::task::spawn_blocking(|| fs::File::create(path)) - .await - .map_err(err_str)? - .map_err(err_str)?; + tokio::task::spawn_blocking(move || -> Result { + controller.check()?; + fs::File::create(&path).map_err(err_str)?; + Ok(OperationSelection { + ignored: Vec::new(), + selected: vec![path], + }) + }) + .await + .map_err(err_str)?? } Self::Rename { from, to } => { - controller.check()?; - - tokio::task::spawn_blocking(|| fs::rename(from, to)) - .await - .map_err(err_str)? - .map_err(err_str)?; + tokio::task::spawn_blocking(move || -> Result { + controller.check()?; + fs::rename(&from, &to).map_err(err_str)?; + Ok(OperationSelection { + ignored: vec![from], + selected: vec![to], + }) + }) + .await + .map_err(err_str)?? } #[cfg(target_os = "macos")] Self::Restore { .. } => { @@ -1024,9 +1055,10 @@ impl Operation { return Err("Restoring from trash is not supported on macos".to_string()); } #[cfg(not(target_os = "macos"))] - Self::Restore { paths } => { - let total = paths.len(); - for (i, path) in paths.into_iter().enumerate() { + Self::Restore { items } => { + let total = items.len(); + let mut paths = Vec::with_capacity(total); + for (i, item) in items.into_iter().enumerate() { controller.check()?; let _ = msg_tx @@ -1038,11 +1070,17 @@ impl Operation { )) .await; - tokio::task::spawn_blocking(|| trash::os_limited::restore_all([path])) + paths.push(item.original_path()); + + tokio::task::spawn_blocking(|| trash::os_limited::restore_all([item])) .await .map_err(err_str)? .map_err(err_str)?; } + OperationSelection { + ignored: Vec::new(), + selected: paths, + } } Self::SetExecutableAndLaunch { path } => { tokio::task::spawn_blocking(move || -> Result<(), String> { @@ -1070,8 +1108,9 @@ impl Operation { .await .map_err(err_str)? .map_err(err_str)?; + OperationSelection::default() } - } + }; let _ = msg_tx .lock() @@ -1079,7 +1118,7 @@ impl Operation { .send(Message::PendingProgress(id, 100.0)) .await; - Ok(()) + Ok(paths) } } @@ -1112,7 +1151,10 @@ mod tests { const BUF_SIZE: usize = 8; /// Simple wrapper around `[Operation::Copy]` - pub async fn operation_copy(paths: Vec, to: PathBuf) -> Result<(), String> { + pub async fn operation_copy( + paths: Vec, + to: PathBuf, + ) -> Result { let id = fastrand::u64(0..u64::MAX); let (tx, mut rx) = mpsc::channel(BUF_SIZE); let paths_clone = paths.clone(); diff --git a/src/operation/recursive.rs b/src/operation/recursive.rs index ef1e7bf..b6f64a4 100644 --- a/src/operation/recursive.rs +++ b/src/operation/recursive.rs @@ -7,13 +7,14 @@ use std::{ }; use walkdir::WalkDir; -use super::{copy_unique_path, Controller, ReplaceResult}; +use super::{copy_unique_path, Controller, OperationSelection, ReplaceResult}; pub struct Context { buf: Vec, controller: Controller, on_progress: Box, on_replace: Box ReplaceResult + 'static>, + pub(crate) op_sel: OperationSelection, replace_result_opt: Option, } @@ -24,6 +25,7 @@ impl Context { controller, on_progress: Box::new(|_op, _progress| {}), on_replace: Box::new(|_op| ReplaceResult::Cancel), + op_sel: OperationSelection::default(), replace_result_opt: None, } } @@ -88,6 +90,8 @@ impl Context { } ops.push(op); } + + self.op_sel.ignored.push(from_parent); } // Add cleanup ops after standard ops, in reverse @@ -106,12 +110,19 @@ impl Context { total_bytes: None, }; (self.on_progress)(&op, &progress); - if !op.run(self, progress).map_err(|err| { + if op.run(self, progress).map_err(|err| { format!( "failed to {:?} {:?} to {:?}: {}", op.kind, op.from, op.to, err ) })? { + // The from path is ignored in the operation selection if it is a top level item + if self.op_sel.ignored.contains(&op.from) { + // So add the to path to the selection + self.op_sel.selected.push(op.to.clone()); + } + } else { + // Cancelled return Ok(false); } } diff --git a/src/tab.rs b/src/tab.rs index cedb092..1c5b4c3 100644 --- a/src/tab.rs +++ b/src/tab.rs @@ -43,7 +43,7 @@ use mime_guess::{mime, Mime}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use std::{ - cell::{Cell, RefCell}, + cell::Cell, cmp::Ordering, collections::HashMap, fmt::{self, Display}, @@ -1015,7 +1015,7 @@ pub enum Command { Action(Action), AddNetworkDrive, AddToSidebar(PathBuf), - ChangeLocation(String, Location, Option), + ChangeLocation(String, Location, Option>), DropFiles(PathBuf, ClipboardPaste), EmptyTrash, #[cfg(feature = "desktop")] @@ -1663,7 +1663,6 @@ pub struct Tab { scrollable_id: widget::Id, select_focus: Option, select_range: Option<(usize, usize)>, - cached_selected: RefCell>, clicked: Option, selected_clicked: bool, last_right_click: Option, @@ -1751,7 +1750,6 @@ impl Tab { scrollable_id: widget::Id::unique(), select_focus: None, select_range: None, - cached_selected: RefCell::new(None), clicked: None, dnd_hovered: None, selected_clicked: false, @@ -1821,7 +1819,6 @@ impl Tab { } pub fn select_all(&mut self) { - *self.cached_selected.borrow_mut() = None; if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { if !self.config.show_hidden && item.hidden { @@ -1834,7 +1831,6 @@ impl Tab { } pub fn select_none(&mut self) -> bool { - *self.cached_selected.borrow_mut() = None; self.select_focus = None; let mut had_selection = false; if let Some(ref mut items) = self.items_opt { @@ -1849,7 +1845,6 @@ impl Tab { } pub fn select_name(&mut self, name: &str) { - *self.cached_selected.borrow_mut() = None; if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { item.selected = item.name == name; @@ -1857,18 +1852,20 @@ impl Tab { } } - pub fn select_path(&mut self, path: PathBuf) { - let location = Location::Path(path); - *self.cached_selected.borrow_mut() = None; + pub fn select_paths(&mut self, paths: Vec) { if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { - item.selected = item.location_opt.as_ref() == Some(&location); + item.selected = false; + if let Some(path) = item.path_opt() { + if paths.contains(path) { + item.selected = true; + } + } } } } fn select_position(&mut self, row: usize, col: usize, mod_shift: bool) -> bool { - *self.cached_selected.borrow_mut() = None; let mut start = (row, col); let mut end = (row, col); if mod_shift { @@ -1919,7 +1916,6 @@ impl Tab { } pub fn select_rect(&mut self, rect: Rectangle, mod_ctrl: bool, mod_shift: bool) { - *self.cached_selected.borrow_mut() = None; if let Some(ref mut items) = self.items_opt { for item in items.iter_mut() { let was_overlapped = item.overlaps_drag_rect; @@ -1996,7 +1992,6 @@ impl Tab { } fn select_first_pos_opt(&self) -> Option<(usize, usize)> { - *self.cached_selected.borrow_mut() = None; let items = self.items_opt.as_ref()?; let mut first = None; for item in items.iter() { @@ -2026,7 +2021,6 @@ impl Tab { } fn select_last_pos_opt(&self) -> Option<(usize, usize)> { - *self.cached_selected.borrow_mut() = None; let items = self.items_opt.as_ref()?; let mut last = None; for item in items.iter() { @@ -2231,7 +2225,6 @@ impl Tab { l.iter() .any(|(e_i, e)| Some(e_i) == click_i_opt.as_ref() && e.selected) }); - *self.cached_selected.borrow_mut() = None; if let Some(ref mut items) = self.items_opt { for (i, item) in items.iter_mut().enumerate() { if Some(i) == click_i_opt { @@ -2680,7 +2673,6 @@ impl Tab { } Message::RightClick(click_i_opt) => { self.update(Message::Click(click_i_opt), modifiers); - *self.cached_selected.borrow_mut() = None; if let Some(ref mut items) = self.items_opt { if !click_i_opt.map_or(false, |click_i| { items.get(click_i).map_or(false, |x| x.selected) @@ -2978,7 +2970,7 @@ impl Tab { } else if location != self.location { if location.path_opt().map_or(true, |path| path.is_dir()) { let prev_path = if let Some(path) = self.location.path_opt() { - Some(path.to_path_buf()) + Some(vec![path.to_path_buf()]) } else { None };