From 06ffbaa2f4ea4bbf0fe472564043c9d246ae0cac Mon Sep 17 00:00:00 2001 From: veeso Date: Sat, 18 Sep 2021 15:57:05 +0200 Subject: [PATCH] Option: prompt user when about to replace an existing file caused by a file transfer --- CHANGELOG.md | 3 + docs/man.md | 1 + src/config/params.rs | 7 +- src/config/serialization.rs | 3 + src/system/config_client.rs | 31 +++++ .../activities/filetransfer/actions/find.rs | 34 ++++-- src/ui/activities/filetransfer/actions/mod.rs | 5 +- .../activities/filetransfer/actions/save.rs | 107 +++++++++++++++--- .../activities/filetransfer/lib/transfer.rs | 53 +++++++++ src/ui/activities/filetransfer/mod.rs | 4 +- src/ui/activities/filetransfer/session.rs | 10 ++ src/ui/activities/filetransfer/update.rs | 25 +++- src/ui/activities/filetransfer/view.rs | 34 ++++++ src/ui/activities/setup/mod.rs | 1 + src/ui/activities/setup/update.rs | 12 +- src/ui/activities/setup/view/setup.rs | 46 +++++++- src/ui/store.rs | 63 +++++++++++ 17 files changed, 406 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 986003b..e92aab8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ Released on ?? - Possibility to update termscp directly via GUI or CLI. - Install update via CLI running `(sudo) termscp --update`. - Install update via GUI from auth form: when the "new version message" is displayed press ``, then enter `YES` in the radio input asking whether to install the update. +- **Prompt user when about to replace existing file on a file transfer**: + - Whenever a file transfer is about to replace an existing file on local/remote host, you will be prompted if you're sure you really want to replace that file. + - You may want to disable this option. You can go to configuration and set "Prompt when replacing existing files?" to "NO" - **❗ BREAKING CHANGES ❗**: - Added a new key in themes: `misc_info_dialog`: if your theme won't load, just reload it. If you're using a customised theme, you can add to it the missing key via a text editor. Just edit the `theme.toml` in your `$CONFIG_DIR/termscp/theme.toml` and add `misc_info_dialog` (Read more in manual at Themes). - Enhancements: diff --git a/docs/man.md b/docs/man.md index 0c84387..f2efcb2 100644 --- a/docs/man.md +++ b/docs/man.md @@ -300,6 +300,7 @@ These parameters can be changed: - **Default Protocol**: the default protocol is the default value for the file transfer protocol to be used in termscp. This applies for the login page and for the address CLI argument. - **Show Hidden Files**: select whether hidden files shall be displayed by default. You will be able to decide whether to show or not hidden files at runtime pressing `A` anyway. - **Check for updates**: if set to `yes`, termscp will fetch the Github API to check if there is a new version of termscp available. +- **Prompt when replacing existing files?**: If set to `yes`, termscp will prompt for confirmation you whenever a file transfer would cause an existing file on target host to be replaced. - **Group Dirs**: select whether directories should be groupped or not in file explorers. If `Display first` is selected, directories will be sorted using the configured method but displayed before files, viceversa if `Display last` is selected. - **Remote File formatter syntax**: syntax to display file info for each file in the remote explorer. See [File explorer format](#file-explorer-format) - **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format) diff --git a/src/config/params.rs b/src/config/params.rs index 09dcd04..aba9ae2 100644 --- a/src/config/params.rs +++ b/src/config/params.rs @@ -51,7 +51,8 @@ pub struct UserInterfaceConfig { pub text_editor: PathBuf, pub default_protocol: String, pub show_hidden_files: bool, - pub check_for_updates: Option, // @! Since 0.3.3 + pub check_for_updates: Option, // @! Since 0.3.3 + pub prompt_on_file_replace: Option, // @! Since 0.7.0; Default True pub group_dirs: Option, pub file_fmt: Option, // Refers to local host (for backward compatibility) pub remote_file_fmt: Option, // @! Since 0.5.0 @@ -84,6 +85,7 @@ impl Default for UserInterfaceConfig { default_protocol: FileTransferProtocol::Sftp.to_string(), show_hidden_files: false, check_for_updates: Some(true), + prompt_on_file_replace: Some(true), group_dirs: None, file_fmt: None, remote_file_fmt: None, @@ -120,6 +122,7 @@ mod tests { text_editor: PathBuf::from("nano"), show_hidden_files: true, check_for_updates: Some(true), + prompt_on_file_replace: Some(true), group_dirs: Some(String::from("first")), file_fmt: Some(String::from("{NAME}")), remote_file_fmt: Some(String::from("{USER}")), @@ -128,6 +131,7 @@ mod tests { assert_eq!(ui.text_editor, PathBuf::from("nano")); assert_eq!(ui.show_hidden_files, true); assert_eq!(ui.check_for_updates, Some(true)); + assert_eq!(ui.prompt_on_file_replace, Some(true)); assert_eq!(ui.group_dirs, Some(String::from("first"))); assert_eq!(ui.file_fmt, Some(String::from("{NAME}"))); let cfg: UserConfig = UserConfig { @@ -145,6 +149,7 @@ mod tests { assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano")); assert_eq!(cfg.user_interface.show_hidden_files, true); assert_eq!(cfg.user_interface.check_for_updates, Some(true)); + assert_eq!(cfg.user_interface.prompt_on_file_replace, Some(true)); assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first"))); assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}"))); assert_eq!( diff --git a/src/config/serialization.rs b/src/config/serialization.rs index 57e8c1d..48ed577 100644 --- a/src/config/serialization.rs +++ b/src/config/serialization.rs @@ -201,6 +201,7 @@ mod tests { assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim")); assert_eq!(cfg.user_interface.show_hidden_files, true); assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true); + assert_eq!(cfg.user_interface.prompt_on_file_replace.unwrap(), false); assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last"))); assert_eq!( cfg.user_interface.file_fmt, @@ -244,6 +245,7 @@ mod tests { assert_eq!(cfg.user_interface.show_hidden_files, true); assert_eq!(cfg.user_interface.group_dirs, None); assert!(cfg.user_interface.check_for_updates.is_none()); + assert!(cfg.user_interface.prompt_on_file_replace.is_none()); assert!(cfg.user_interface.file_fmt.is_none()); assert!(cfg.user_interface.remote_file_fmt.is_none()); // Verify keys @@ -317,6 +319,7 @@ mod tests { text_editor = "vim" show_hidden_files = true check_for_updates = true + prompt_on_file_replace = false group_dirs = "last" file_fmt = "{NAME} {PEX}" remote_file_fmt = "{NAME} {USER}" diff --git a/src/system/config_client.rs b/src/system/config_client.rs index 2a484e2..30f1913 100644 --- a/src/system/config_client.rs +++ b/src/system/config_client.rs @@ -181,6 +181,23 @@ impl ConfigClient { self.config.user_interface.check_for_updates = Some(value); } + /// ### get_prompt_on_file_replace + /// + /// Get value of `prompt_on_file_replace` + pub fn get_prompt_on_file_replace(&self) -> bool { + self.config + .user_interface + .prompt_on_file_replace + .unwrap_or(true) + } + + /// ### set_prompt_on_file_replace + /// + /// Set new value for `prompt_on_file_replace` + pub fn set_prompt_on_file_replace(&mut self, value: bool) { + self.config.user_interface.prompt_on_file_replace = Some(value); + } + /// ### get_group_dirs /// /// Get GroupDirs value from configuration (will be converted from string) @@ -580,6 +597,20 @@ mod tests { assert_eq!(client.get_check_for_updates(), false); } + #[test] + fn test_system_config_prompt_on_file_replace() { + let tmp_dir: TempDir = TempDir::new().ok().unwrap(); + let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path()); + let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path()) + .ok() + .unwrap(); + assert_eq!(client.get_prompt_on_file_replace(), true); // Null ? + client.set_prompt_on_file_replace(true); + assert_eq!(client.get_prompt_on_file_replace(), true); + client.set_prompt_on_file_replace(false); + assert_eq!(client.get_prompt_on_file_replace(), false); + } + #[test] fn test_system_config_group_dirs() { let tmp_dir: TempDir = TempDir::new().ok().unwrap(); diff --git a/src/ui/activities/filetransfer/actions/find.rs b/src/ui/activities/filetransfer/actions/find.rs index a7b9aed..6aa3f7c 100644 --- a/src/ui/activities/filetransfer/actions/find.rs +++ b/src/ui/activities/filetransfer/actions/find.rs @@ -27,7 +27,9 @@ */ // locals use super::super::browser::FileExplorerTab; -use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload}; +use super::{ + FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferOpts, TransferPayload, +}; use std::path::PathBuf; @@ -69,7 +71,7 @@ impl FileTransferActivity { } } - pub(crate) fn action_find_transfer(&mut self, save_as: Option) { + pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) { let wrkdir: PathBuf = match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(), FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(), @@ -77,10 +79,19 @@ impl FileTransferActivity { match self.get_found_selected_entries() { SelectedEntry::One(entry) => match self.browser.tab() { FileExplorerTab::FindLocal | FileExplorerTab::Local => { - if let Err(err) = self.filetransfer_send( + let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); + if opts.check_replace + && self.config().get_prompt_on_file_replace() + && self.remote_file_exists(file_to_check.as_path()) + { + // Save pending transfer + self.set_pending_transfer( + opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), + ); + } else if let Err(err) = self.filetransfer_send( TransferPayload::Any(entry.get_realfile()), wrkdir.as_path(), - save_as, + opts.save_as, ) { self.log_and_alert( LogLevel::Error, @@ -90,10 +101,19 @@ impl FileTransferActivity { } } FileExplorerTab::FindRemote | FileExplorerTab::Remote => { - if let Err(err) = self.filetransfer_recv( + let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); + if opts.check_replace + && self.config().get_prompt_on_file_replace() + && self.local_file_exists(file_to_check.as_path()) + { + // Save pending transfer + self.set_pending_transfer( + opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), + ); + } else if let Err(err) = self.filetransfer_recv( TransferPayload::Any(entry.get_realfile()), wrkdir.as_path(), - save_as, + opts.save_as, ) { self.log_and_alert( LogLevel::Error, @@ -106,7 +126,7 @@ impl FileTransferActivity { SelectedEntry::Many(entries) => { // In case of selection: save multiple files in wrkdir/input let mut dest_path: PathBuf = wrkdir; - if let Some(save_as) = save_as { + if let Some(save_as) = opts.save_as { dest_path.push(save_as); } // Iter files diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index 9a9020e..e384951 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -25,7 +25,10 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -pub(self) use super::{FileTransferActivity, FsEntry, LogLevel, TransferPayload}; +pub(self) use super::{ + browser::FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, TransferOpts, + TransferPayload, +}; use tuirealm::{Payload, Value}; // actions diff --git a/src/ui/activities/filetransfer/actions/save.rs b/src/ui/activities/filetransfer/actions/save.rs index 4a5c238..f13437b 100644 --- a/src/ui/activities/filetransfer/actions/save.rs +++ b/src/ui/activities/filetransfer/actions/save.rs @@ -26,34 +26,85 @@ * SOFTWARE. */ // locals -use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload}; +use super::{ + super::STORAGE_PENDING_TRANSFER, FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, + SelectedEntry, TransferOpts, TransferPayload, +}; use std::path::PathBuf; impl FileTransferActivity { pub(crate) fn action_local_saveas(&mut self, input: String) { - self.action_local_send_file(Some(input)); + self.local_send_file(TransferOpts::default().save_as(input)); } pub(crate) fn action_remote_saveas(&mut self, input: String) { - self.action_remote_recv_file(Some(input)); + self.remote_recv_file(TransferOpts::default().save_as(input)); } pub(crate) fn action_local_send(&mut self) { - self.action_local_send_file(None); + self.local_send_file(TransferOpts::default()); } pub(crate) fn action_remote_recv(&mut self) { - self.action_remote_recv_file(None); + self.remote_recv_file(TransferOpts::default()); } - fn action_local_send_file(&mut self, save_as: Option) { + /// ### action_finalize_pending_transfer + /// + /// Finalize "pending" transfer. + /// The pending transfer is created after a transfer which required a user action to be completed first. + /// The name of the file to transfer, is contained in the storage at `STORAGE_PENDING_TRANSFER`. + /// NOTE: Panics if `STORAGE_PENDING_TRANSFER` is undefined + pub(crate) fn action_finalize_pending_transfer(&mut self) { + // Retrieve pending transfer + let file_name: String = self + .context_mut() + .store_mut() + .take_string(STORAGE_PENDING_TRANSFER) + .unwrap(); + // Send file + match self.browser.tab() { + FileExplorerTab::Local => self.local_send_file( + TransferOpts::default() + .save_as(file_name) + .check_replace(false), + ), + FileExplorerTab::Remote => self.remote_recv_file( + TransferOpts::default() + .save_as(file_name) + .check_replace(false), + ), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => self.action_find_transfer( + TransferOpts::default() + .save_as(file_name) + .check_replace(false), + ), + } + // Reload browsers + match self.browser.tab() { + FileExplorerTab::Local => self.reload_remote_dir(), + FileExplorerTab::Remote => self.reload_local_dir(), + FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {} + } + } + + fn local_send_file(&mut self, opts: TransferOpts) { let wrkdir: PathBuf = self.remote().wrkdir.clone(); match self.get_local_selected_entries() { SelectedEntry::One(entry) => { - if let Err(err) = self.filetransfer_send( + let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); + if opts.check_replace + && self.config().get_prompt_on_file_replace() + && self.remote_file_exists(file_to_check.as_path()) + { + // Save pending transfer + self.set_pending_transfer( + opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), + ); + } else if let Err(err) = self.filetransfer_send( TransferPayload::Any(entry.get_realfile()), wrkdir.as_path(), - save_as, + opts.save_as, ) { { self.log_and_alert( @@ -67,7 +118,7 @@ impl FileTransferActivity { SelectedEntry::Many(entries) => { // In case of selection: save multiple files in wrkdir/input let mut dest_path: PathBuf = wrkdir; - if let Some(save_as) = save_as { + if let Some(save_as) = opts.save_as { dest_path.push(save_as); } // Iter files @@ -90,14 +141,23 @@ impl FileTransferActivity { } } - fn action_remote_recv_file(&mut self, save_as: Option) { + fn remote_recv_file(&mut self, opts: TransferOpts) { let wrkdir: PathBuf = self.local().wrkdir.clone(); match self.get_remote_selected_entries() { SelectedEntry::One(entry) => { - if let Err(err) = self.filetransfer_recv( + let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref()); + if opts.check_replace + && self.config().get_prompt_on_file_replace() + && self.local_file_exists(file_to_check.as_path()) + { + // Save pending transfer + self.set_pending_transfer( + opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), + ); + } else if let Err(err) = self.filetransfer_recv( TransferPayload::Any(entry.get_realfile()), wrkdir.as_path(), - save_as, + opts.save_as, ) { { self.log_and_alert( @@ -111,7 +171,7 @@ impl FileTransferActivity { SelectedEntry::Many(entries) => { // In case of selection: save multiple files in wrkdir/input let mut dest_path: PathBuf = wrkdir; - if let Some(save_as) = save_as { + if let Some(save_as) = opts.save_as { dest_path.push(save_as); } // Iter files @@ -133,4 +193,25 @@ impl FileTransferActivity { SelectedEntry::None => {} } } + + /// ### set_pending_transfer + /// + /// Set pending transfer into storage + pub(crate) fn set_pending_transfer(&mut self, file_name: &str) { + self.mount_radio_replace(file_name); + // Put pending transfer in store + self.context_mut() + .store_mut() + .set_string(STORAGE_PENDING_TRANSFER, file_name.to_string()); + } + + /// ### file_to_check + /// + /// Get file to check for path + pub(crate) fn file_to_check(e: &FsEntry, alt: Option<&String>) -> PathBuf { + match alt { + Some(s) => PathBuf::from(s), + None => PathBuf::from(e.get_name()), + } + } } diff --git a/src/ui/activities/filetransfer/lib/transfer.rs b/src/ui/activities/filetransfer/lib/transfer.rs index 2816f51..be8b75e 100644 --- a/src/ui/activities/filetransfer/lib/transfer.rs +++ b/src/ui/activities/filetransfer/lib/transfer.rs @@ -29,6 +29,8 @@ use bytesize::ByteSize; use std::fmt; use std::time::Instant; +// -- States and progress + /// ### TransferStates /// /// TransferStates contains the states related to the transfer process @@ -195,6 +197,45 @@ impl ProgressStates { } } +// -- Options + +/// ## TransferOpts +/// +/// Defines the transfer options for transfer actions +pub struct TransferOpts { + /// Save file as + pub save_as: Option, + /// Whether to check if file is being replaced + pub check_replace: bool, +} + +impl Default for TransferOpts { + fn default() -> Self { + Self { + save_as: None, + check_replace: true, + } + } +} + +impl TransferOpts { + /// ### save_as + /// + /// Define the name of the file to be saved + pub fn save_as>(mut self, n: S) -> Self { + self.save_as = Some(n.as_ref().to_string()); + self + } + + /// ### check_replace + /// + /// Set whether to check if the file being transferred will "replace" an existing one + pub fn check_replace(mut self, opt: bool) -> Self { + self.check_replace = opt; + self + } +} + #[cfg(test)] mod test { @@ -265,4 +306,16 @@ mod test { states.reset(); assert_eq!(states.aborted(), false); } + + #[test] + fn transfer_opts() { + let opts = TransferOpts::default(); + assert_eq!(opts.check_replace, true); + assert!(opts.save_as.is_none()); + let opts = TransferOpts::default() + .check_replace(false) + .save_as("omar.txt"); + assert_eq!(opts.save_as.as_deref().unwrap(), "omar.txt"); + assert_eq!(opts.check_replace, false); + } } diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index d768ed8..74065ef 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -44,7 +44,7 @@ use crate::host::Localhost; use crate::system::config_client::ConfigClient; pub(self) use lib::browser; use lib::browser::Browser; -use lib::transfer::TransferStates; +use lib::transfer::{TransferOpts, TransferStates}; pub(self) use session::TransferPayload; // Includes @@ -57,6 +57,7 @@ use tuirealm::View; // -- Storage keys const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH"; +const STORAGE_PENDING_TRANSFER: &str = "FILETRANSFER_PENDING_TRANSFER"; // -- components @@ -80,6 +81,7 @@ const COMPONENT_INPUT_OPEN_WITH: &str = "INPUT_OPEN_WITH"; const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME"; const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS"; const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE"; +const COMPONENT_RADIO_REPLACE: &str = "RADIO_REPLACE"; // NOTE: used for file transfers, to choose whether to replace files const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT"; const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT"; const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING"; diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index cb4a0ff..3948e31 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -1187,4 +1187,14 @@ impl FileTransferActivity { } } } + + // -- file exist + + pub(crate) fn local_file_exists(&mut self, p: &Path) -> bool { + self.host.file_exists(p) + } + + pub(crate) fn remote_file_exists(&mut self, p: &Path) -> bool { + self.client.stat(p).is_ok() + } } diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 716f762..c18d143 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -27,14 +27,14 @@ */ // locals use super::{ - actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel, + actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel, TransferOpts, COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE, - COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING, - COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP, + COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_REPLACE, + COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP, }; use crate::fs::explorer::FileSorting; use crate::fs::FsEntry; @@ -358,7 +358,7 @@ impl Update for FileTransferActivity { } (COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_SPACE => { // Get entry - self.action_find_transfer(None); + self.action_find_transfer(TransferOpts::default()); // Reload files match self.browser.tab() { // NOTE: swapped by purpose @@ -583,7 +583,7 @@ impl Update for FileTransferActivity { FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()), FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => { // Get entry - self.action_find_transfer(Some(input.to_string())); + self.action_find_transfer(TransferOpts::default().save_as(input)); } } self.umount_saveas(); @@ -653,6 +653,21 @@ impl Update for FileTransferActivity { } } (COMPONENT_RADIO_DELETE, _) => None, + // -- replace + (COMPONENT_RADIO_REPLACE, key) + if key == &MSG_KEY_ESC + || key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) => + { + self.umount_radio_replace(); + None + } + (COMPONENT_RADIO_REPLACE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => { + // Choice is 'YES' + self.umount_radio_replace(); + self.action_finalize_pending_transfer(); + None + } + (COMPONENT_RADIO_REPLACE, _) => None, // -- disconnect (COMPONENT_RADIO_DISCONNECT, key) if key == &MSG_KEY_ESC diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index ac97d99..cbe2655 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -308,6 +308,14 @@ impl FileTransferActivity { self.view.render(super::COMPONENT_RADIO_DELETE, f, popup); } } + if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_REPLACE) { + if props.visible { + let popup = draw_area_in(f.size(), 50, 10); + f.render_widget(Clear, popup); + // make popup + self.view.render(super::COMPONENT_RADIO_REPLACE, f, popup); + } + } if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) { if props.visible { let popup = draw_area_in(f.size(), 30, 10); @@ -698,6 +706,32 @@ impl FileTransferActivity { self.view.umount(super::COMPONENT_RADIO_DELETE); } + pub(super) fn mount_radio_replace(&mut self, file_name: &str) { + let warn_color = self.theme().misc_warn_dialog; + self.view.mount( + super::COMPONENT_RADIO_REPLACE, + Box::new(Radio::new( + RadioPropsBuilder::default() + .with_color(warn_color) + .with_inverted_color(Color::Black) + .with_borders(Borders::ALL, BorderType::Plain, warn_color) + .with_title( + format!("File '{}' already exists. Overwrite file?", file_name), + Alignment::Center, + ) + .with_options(&[String::from("Yes"), String::from("No")]) + .with_value(0) + .rewind(true) + .build(), + )), + ); + self.view.active(super::COMPONENT_RADIO_REPLACE); + } + + pub(super) fn umount_radio_replace(&mut self) { + self.view.umount(super::COMPONENT_RADIO_REPLACE); + } + pub(super) fn mount_file_info(&mut self, file: &FsEntry) { let mut texts: TableBuilder = TableBuilder::default(); // Abs path diff --git a/src/ui/activities/setup/mod.rs b/src/ui/activities/setup/mod.rs index ec2b4ff..7514141 100644 --- a/src/ui/activities/setup/mod.rs +++ b/src/ui/activities/setup/mod.rs @@ -54,6 +54,7 @@ const COMPONENT_INPUT_TEXT_EDITOR: &str = "INPUT_TEXT_EDITOR"; const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL"; const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES"; const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES"; +const COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE: &str = "RADIO_PROMPT_ON_FILE_REPLACE"; const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS"; const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT"; const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT"; diff --git a/src/ui/activities/setup/update.rs b/src/ui/activities/setup/update.rs index ffe5600..b43f795 100644 --- a/src/ui/activities/setup/update.rs +++ b/src/ui/activities/setup/update.rs @@ -43,8 +43,8 @@ use super::{ COMPONENT_INPUT_REMOTE_FILE_FMT, COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY, COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES, - COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, - COMPONENT_TEXT_HELP, + COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE, + COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP, }; use crate::ui::keymap::*; use crate::utils::parser::parse_color; @@ -87,6 +87,10 @@ impl SetupActivity { None } (COMPONENT_RADIO_UPDATES, key) if key == &MSG_KEY_DOWN => { + self.view.active(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE); + None + } + (COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, key) if key == &MSG_KEY_DOWN => { self.view.active(COMPONENT_RADIO_GROUP_DIRS); None } @@ -112,6 +116,10 @@ impl SetupActivity { None } (COMPONENT_RADIO_GROUP_DIRS, key) if key == &MSG_KEY_UP => { + self.view.active(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE); + None + } + (COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, key) if key == &MSG_KEY_UP => { self.view.active(COMPONENT_RADIO_UPDATES); None } diff --git a/src/ui/activities/setup/view/setup.rs b/src/ui/activities/setup/view/setup.rs index 6ff051b..f5d3f70 100644 --- a/src/ui/activities/setup/view/setup.rs +++ b/src/ui/activities/setup/view/setup.rs @@ -109,6 +109,19 @@ impl SetupActivity { .build(), )), ); + self.view.mount( + super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, + Box::new(Radio::new( + RadioPropsBuilder::default() + .with_color(Color::LightCyan) + .with_inverted_color(Color::Black) + .with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan) + .with_title("Prompt when replacing existing files?", Alignment::Left) + .with_options(&[String::from("Yes"), String::from("No")]) + .rewind(true) + .build(), + )), + ); self.view.mount( super::COMPONENT_RADIO_GROUP_DIRS, Box::new(Radio::new( @@ -178,6 +191,7 @@ impl SetupActivity { Constraint::Length(3), // Protocol tab Constraint::Length(3), // Hidden files Constraint::Length(3), // Updates tab + Constraint::Length(3), // Prompt file replace Constraint::Length(3), // Group dirs Constraint::Length(3), // Local Format input Constraint::Length(3), // Remote Format input @@ -193,12 +207,17 @@ impl SetupActivity { .render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]); self.view .render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]); + self.view.render( + super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, + f, + ui_cfg_chunks[4], + ); self.view - .render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[4]); + .render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[5]); self.view - .render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[5]); + .render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[6]); self.view - .render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[6]); + .render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[7]); // Popups if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) { if props.visible { @@ -282,6 +301,20 @@ impl SetupActivity { let props = RadioPropsBuilder::from(props).with_value(updates).build(); let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props); } + // File replace + if let Some(props) = self + .view + .get_props(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE) + { + let updates: usize = match self.config().get_prompt_on_file_replace() { + true => 0, + false => 1, + }; + let props = RadioPropsBuilder::from(props).with_value(updates).build(); + let _ = self + .view + .update(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, props); + } // Group dirs if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) { let dirs: usize = match self.config().get_group_dirs() { @@ -344,6 +377,13 @@ impl SetupActivity { let check: bool = matches!(opt, 0); self.config_mut().set_check_for_updates(check); } + if let Some(Payload::One(Value::Usize(opt))) = self + .view + .get_state(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE) + { + let check: bool = matches!(opt, 0); + self.config_mut().set_prompt_on_file_replace(check); + } if let Some(Payload::One(Value::Str(fmt))) = self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT) { diff --git a/src/ui/store.rs b/src/ui/store.rs index 5d49d0f..9d8cb17 100644 --- a/src/ui/store.rs +++ b/src/ui/store.rs @@ -67,6 +67,7 @@ impl Store { } // -- getters + /// ### get_string /// /// Get string from store @@ -168,6 +169,58 @@ impl Store { pub fn set(&mut self, key: &str) { self.store.insert(key.to_string(), StoreState::Flag); } + + // -- Consumers + + /// ### take_string + /// + /// Take string from store + pub fn take_string(&mut self, key: &str) -> Option { + match self.store.remove(key) { + Some(StoreState::Str(s)) => Some(s), + _ => None, + } + } + + /// ### take_signed + /// + /// Take signed from store + pub fn take_signed(&mut self, key: &str) -> Option { + match self.store.remove(key) { + Some(StoreState::Signed(i)) => Some(i), + _ => None, + } + } + + /// ### take_unsigned + /// + /// Take unsigned from store + pub fn take_unsigned(&mut self, key: &str) -> Option { + match self.store.remove(key) { + Some(StoreState::Unsigned(u)) => Some(u), + _ => None, + } + } + + /// ### get_float + /// + /// Take float from store + pub fn take_float(&mut self, key: &str) -> Option { + match self.store.remove(key) { + Some(StoreState::Float(f)) => Some(f), + _ => None, + } + } + + /// ### get_boolean + /// + /// Take boolean from store + pub fn take_boolean(&mut self, key: &str) -> Option { + match self.store.remove(key) { + Some(StoreState::Boolean(b)) => Some(b), + _ => None, + } + } } #[cfg(test)] @@ -184,20 +237,30 @@ mod tests { // Test string store.set_string("test", String::from("hello")); assert_eq!(*store.get_string("test").as_ref().unwrap(), "hello"); + assert_eq!(store.take_string("test").unwrap(), "hello".to_string()); + assert_eq!(store.take_string("test"), None); // Test isize store.set_signed("number", 3005); assert_eq!(store.get_signed("number").unwrap(), 3005); + assert_eq!(store.take_signed("number").unwrap(), 3005); + assert_eq!(store.take_signed("number"), None); store.set_signed("number", -123); assert_eq!(store.get_signed("number").unwrap(), -123); // Test usize store.set_unsigned("unumber", 1024); assert_eq!(store.get_unsigned("unumber").unwrap(), 1024); + assert_eq!(store.take_unsigned("unumber").unwrap(), 1024); + assert_eq!(store.take_unsigned("unumber"), None); // Test float store.set_float("float", 3.33); assert_eq!(store.get_float("float").unwrap(), 3.33); + assert_eq!(store.take_float("float").unwrap(), 3.33); + assert_eq!(store.take_float("float"), None); // Test boolean store.set_boolean("bool", true); assert_eq!(store.get_boolean("bool").unwrap(), true); + assert_eq!(store.take_boolean("bool").unwrap(), true); + assert_eq!(store.take_boolean("bool"), None); // Test flag store.set("myflag"); assert_eq!(store.isset("myflag"), true);