mirror of
https://github.com/veeso/termscp.git
synced 2025-12-06 17:15:35 -08:00
Compare commits
2 Commits
f4156a5059
...
75943f2b93
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75943f2b93 | ||
|
|
085ab721f9 |
@@ -47,13 +47,15 @@
|
|||||||
Released on 20/09/2025
|
Released on 20/09/2025
|
||||||
|
|
||||||
- [Issue 331](https://github.com/veeso/termscp/issues/331): Added new `import-ssh-hosts` CLI subcommand to import all the hosts from the ssh config as bookmarks.
|
- [Issue 331](https://github.com/veeso/termscp/issues/331): Added new `import-ssh-hosts` CLI subcommand to import all the hosts from the ssh config as bookmarks.
|
||||||
|
- [Issue 335](https://github.com/veeso/termscp/issues/335): Changed file overwrite behaviour
|
||||||
|
- Now the user can choose for each file whether to overwrite, skip or overwrite all/skip all.
|
||||||
|
- [Issue 354](https://github.com/veeso/termscp/issues/354):
|
||||||
|
- Removed error popup message if failed to check for updates.
|
||||||
|
- Prevent long timeouts when checking for updates if the network is down or the DNS is not working.
|
||||||
- [Issue 356](https://github.com/veeso/termscp/issues/356): Fixed SSH auth issue not trying with the password if any RSA key was found.
|
- [Issue 356](https://github.com/veeso/termscp/issues/356): Fixed SSH auth issue not trying with the password if any RSA key was found.
|
||||||
- [Issue 334](https://github.com/veeso/termscp/issues/334): SMB support for MacOS with vendored build of libsmbclient.
|
- [Issue 334](https://github.com/veeso/termscp/issues/334): SMB support for MacOS with vendored build of libsmbclient.
|
||||||
- [Issue 337](https://github.com/veeso/termscp/issues/337): Migrated to libssh.org on Linux and MacOS for better ssh agent support.
|
- [Issue 337](https://github.com/veeso/termscp/issues/337): Migrated to libssh.org on Linux and MacOS for better ssh agent support.
|
||||||
- [Issue 361](https://github.com/veeso/termscp/issues/361): Report a message while calculating total size of files to transfer.
|
- [Issue 361](https://github.com/veeso/termscp/issues/361): Report a message while calculating total size of files to transfer.
|
||||||
- [Issue 354](https://github.com/veeso/termscp/issues/354):
|
|
||||||
- Removed error popup message if failed to check for updates.
|
|
||||||
- Prevent long timeouts when checking for updates if the network is down or the DNS is not working.
|
|
||||||
|
|
||||||
## 0.18.0
|
## 0.18.0
|
||||||
|
|
||||||
|
|||||||
8
Cargo.lock
generated
8
Cargo.lock
generated
@@ -1272,7 +1272,7 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users",
|
"redox_users",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2032,7 +2032,7 @@ dependencies = [
|
|||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.57.0",
|
"windows-core 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3553,9 +3553,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remotefs-ssh"
|
name = "remotefs-ssh"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d29de4702886ae0e4433904d4f6f9b2ed2961d31d76a7e7bef58305097238eb6"
|
checksum = "5ca8b65fbd60801ac03973a41196b030fb04d1ce1af33d3c58c5bad94d46eb27"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"lazy-regex",
|
"lazy-regex",
|
||||||
|
|||||||
@@ -99,24 +99,16 @@ impl FileTransferActivity {
|
|||||||
// Iter files
|
// Iter files
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
FileExplorerTab::FindHostBridge | FileExplorerTab::HostBridge => {
|
||||||
if self.config().get_prompt_on_file_replace() {
|
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
||||||
// Check which file would be replaced
|
entries,
|
||||||
let existing_files: Vec<&File> = entries
|
) = self.get_files_to_transfer_with_overwrites(
|
||||||
.iter()
|
entries,
|
||||||
.filter(|(x, dest_path)| {
|
super::save::CheckFileExists::Remote,
|
||||||
self.remote_file_exists(
|
)
|
||||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
else {
|
||||||
)
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
})
|
return;
|
||||||
.map(|(x, _)| x)
|
};
|
||||||
.collect();
|
|
||||||
// Check whether to replace files
|
|
||||||
if !existing_files.is_empty()
|
|
||||||
&& !self.should_replace_files(existing_files)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = self.filetransfer_send(
|
if let Err(err) = self.filetransfer_send(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
@@ -131,24 +123,16 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
if self.config().get_prompt_on_file_replace() {
|
let super::save::TransferFilesWithOverwritesResult::FilesToTransfer(
|
||||||
// Check which file would be replaced
|
entries,
|
||||||
let existing_files: Vec<&File> = entries
|
) = self.get_files_to_transfer_with_overwrites(
|
||||||
.iter()
|
entries,
|
||||||
.filter(|(x, dest_path)| {
|
super::save::CheckFileExists::HostBridge,
|
||||||
self.host_bridge_file_exists(
|
)
|
||||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
else {
|
||||||
)
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
})
|
return;
|
||||||
.map(|(x, _)| x)
|
};
|
||||||
.collect();
|
|
||||||
// Check whether to replace files
|
|
||||||
if !existing_files.is_empty()
|
|
||||||
&& !self.should_replace_files(existing_files)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = self.filetransfer_recv(
|
if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
|
|||||||
@@ -10,6 +10,37 @@ use super::{
|
|||||||
TransferPayload,
|
TransferPayload,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum GetFileToReplaceResult {
|
||||||
|
Replace(Vec<(File, PathBuf)>),
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of getting files to transfer with overwrites.
|
||||||
|
///
|
||||||
|
/// - FilesToTransfer: files to transfer.
|
||||||
|
/// - Cancel: user cancelled the operation.
|
||||||
|
pub(crate) enum TransferFilesWithOverwritesResult {
|
||||||
|
FilesToTransfer(Vec<(File, PathBuf)>),
|
||||||
|
Cancel,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decides whether to check file existence on host bridge or remote side.
|
||||||
|
pub(crate) enum CheckFileExists {
|
||||||
|
HostBridge,
|
||||||
|
Remote,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Options for all files replacement.
|
||||||
|
///
|
||||||
|
/// - ReplaceAll: user wants to replace all files.
|
||||||
|
/// - SkipAll: user wants to skip all files.
|
||||||
|
/// - Unset: no option set yet.
|
||||||
|
enum AllOpts {
|
||||||
|
ReplaceAll,
|
||||||
|
SkipAll,
|
||||||
|
Unset,
|
||||||
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_saveas(&mut self, input: String) {
|
pub(crate) fn action_local_saveas(&mut self, input: String) {
|
||||||
self.local_send_file(TransferOpts::default().save_as(Some(input)));
|
self.local_send_file(TransferOpts::default().save_as(Some(input)));
|
||||||
@@ -60,22 +91,12 @@ impl FileTransferActivity {
|
|||||||
dest_path.push(save_as);
|
dest_path.push(save_as);
|
||||||
}
|
}
|
||||||
// Iter files
|
// Iter files
|
||||||
if self.config().get_prompt_on_file_replace() {
|
let TransferFilesWithOverwritesResult::FilesToTransfer(entries) =
|
||||||
// Check which file would be replaced
|
self.get_files_to_transfer_with_overwrites(entries, CheckFileExists::Remote)
|
||||||
let existing_files: Vec<&File> = entries
|
else {
|
||||||
.iter()
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
.filter(|(x, dest_path)| {
|
return;
|
||||||
self.remote_file_exists(
|
};
|
||||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(|(x, _)| x)
|
|
||||||
.collect();
|
|
||||||
// Check whether to replace files
|
|
||||||
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = self.filetransfer_send(
|
if let Err(err) = self.filetransfer_send(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
@@ -128,23 +149,13 @@ impl FileTransferActivity {
|
|||||||
if let Some(save_as) = opts.save_as {
|
if let Some(save_as) = opts.save_as {
|
||||||
dest_path.push(save_as);
|
dest_path.push(save_as);
|
||||||
}
|
}
|
||||||
// Iter files
|
let TransferFilesWithOverwritesResult::FilesToTransfer(entries) = self
|
||||||
if self.config().get_prompt_on_file_replace() {
|
.get_files_to_transfer_with_overwrites(entries, CheckFileExists::HostBridge)
|
||||||
// Check which file would be replaced
|
else {
|
||||||
let existing_files: Vec<&File> = entries
|
debug!("User cancelled file transfer due to overwrites");
|
||||||
.iter()
|
return;
|
||||||
.filter(|(x, dest_path)| {
|
};
|
||||||
self.host_bridge_file_exists(
|
|
||||||
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(|(x, _)| x)
|
|
||||||
.collect();
|
|
||||||
// Check whether to replace files
|
|
||||||
if !existing_files.is_empty() && !self.should_replace_files(existing_files) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Err(err) = self.filetransfer_recv(
|
if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::TransferQueue(entries),
|
TransferPayload::TransferQueue(entries),
|
||||||
dest_path.as_path(),
|
dest_path.as_path(),
|
||||||
@@ -172,11 +183,17 @@ impl FileTransferActivity {
|
|||||||
self.mount_radio_replace(&file_name);
|
self.mount_radio_replace(&file_name);
|
||||||
// Wait for answer
|
// Wait for answer
|
||||||
trace!("Asking user whether he wants to replace file {}", file_name);
|
trace!("Asking user whether he wants to replace file {}", file_name);
|
||||||
if self.wait_for_pending_msg(&[
|
if matches!(
|
||||||
Msg::PendingAction(PendingActionMsg::CloseReplacePopups),
|
self.wait_for_pending_msg(&[
|
||||||
Msg::PendingAction(PendingActionMsg::TransferPendingFile),
|
Msg::PendingAction(PendingActionMsg::ReplaceCancel),
|
||||||
]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile)
|
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite),
|
||||||
{
|
Msg::PendingAction(PendingActionMsg::ReplaceSkip),
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceSkipAll),
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll),
|
||||||
|
]),
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)
|
||||||
|
| Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll)
|
||||||
|
) {
|
||||||
trace!("User wants to replace file");
|
trace!("User wants to replace file");
|
||||||
self.umount_radio_replace();
|
self.umount_radio_replace();
|
||||||
true
|
true
|
||||||
@@ -187,28 +204,76 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set pending transfer for many files into storage and mount radio
|
/// Get files to replace
|
||||||
pub(crate) fn should_replace_files(&mut self, files: Vec<&File>) -> bool {
|
fn get_files_to_replace(&mut self, files: Vec<(File, PathBuf)>) -> GetFileToReplaceResult {
|
||||||
let file_names: Vec<String> = files.iter().map(|x| x.name()).collect();
|
// keep only files the user want to replace
|
||||||
self.mount_radio_replace_many(file_names.as_slice());
|
let mut files_to_replace = vec![];
|
||||||
// Wait for answer
|
let mut all_opts = AllOpts::Unset;
|
||||||
trace!(
|
for (file, p) in files {
|
||||||
"Asking user whether he wants to replace files {:?}",
|
// Check for all opts
|
||||||
file_names
|
match all_opts {
|
||||||
);
|
AllOpts::ReplaceAll => {
|
||||||
if self.wait_for_pending_msg(&[
|
trace!(
|
||||||
Msg::PendingAction(PendingActionMsg::CloseReplacePopups),
|
"User wants to replace all files, including file {}",
|
||||||
Msg::PendingAction(PendingActionMsg::TransferPendingFile),
|
file.name()
|
||||||
]) == Msg::PendingAction(PendingActionMsg::TransferPendingFile)
|
);
|
||||||
{
|
files_to_replace.push((file, p));
|
||||||
trace!("User wants to replace files");
|
continue;
|
||||||
|
}
|
||||||
|
AllOpts::SkipAll => {
|
||||||
|
trace!(
|
||||||
|
"User wants to skip all files, including file {}",
|
||||||
|
file.name()
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
AllOpts::Unset => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_name = file.name();
|
||||||
|
self.mount_radio_replace(&file_name);
|
||||||
|
|
||||||
|
// Wait for answer
|
||||||
|
match self.wait_for_pending_msg(&[
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceCancel),
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite),
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceSkip),
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceSkipAll),
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll),
|
||||||
|
]) {
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceCancel) => {
|
||||||
|
trace!("The user cancelled the replace operation");
|
||||||
|
self.umount_radio_replace();
|
||||||
|
return GetFileToReplaceResult::Cancel;
|
||||||
|
}
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceOverwrite) => {
|
||||||
|
trace!("User wants to replace file {}", file_name);
|
||||||
|
files_to_replace.push((file, p));
|
||||||
|
}
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll) => {
|
||||||
|
trace!(
|
||||||
|
"User wants to replace all files from now on, including file {}",
|
||||||
|
file_name
|
||||||
|
);
|
||||||
|
files_to_replace.push((file, p));
|
||||||
|
all_opts = AllOpts::ReplaceAll;
|
||||||
|
}
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceSkip) => {
|
||||||
|
trace!("The user skipped file {}", file_name);
|
||||||
|
}
|
||||||
|
Msg::PendingAction(PendingActionMsg::ReplaceSkipAll) => {
|
||||||
|
trace!(
|
||||||
|
"The user skipped all files from now on, including file {}",
|
||||||
|
file_name
|
||||||
|
);
|
||||||
|
all_opts = AllOpts::SkipAll;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
self.umount_radio_replace();
|
self.umount_radio_replace();
|
||||||
true
|
|
||||||
} else {
|
|
||||||
trace!("The user doesn't want replace file");
|
|
||||||
self.umount_radio_replace();
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GetFileToReplaceResult::Replace(files_to_replace)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get file to check for path
|
/// Get file to check for path
|
||||||
@@ -224,4 +289,40 @@ impl FileTransferActivity {
|
|||||||
p.push(e.name());
|
p.push(e.name());
|
||||||
p
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the files to transfer with overwrites.
|
||||||
|
///
|
||||||
|
/// Existing and unexisting files are splitted, and only existing files are prompted for replacement.
|
||||||
|
pub(crate) fn get_files_to_transfer_with_overwrites(
|
||||||
|
&mut self,
|
||||||
|
files: Vec<(File, PathBuf)>,
|
||||||
|
file_exists: CheckFileExists,
|
||||||
|
) -> TransferFilesWithOverwritesResult {
|
||||||
|
if !self.config().get_prompt_on_file_replace() {
|
||||||
|
return TransferFilesWithOverwritesResult::FilesToTransfer(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
// unzip between existing and non-existing files
|
||||||
|
let (existing_files, new_files): (Vec<_>, Vec<_>) =
|
||||||
|
files.into_iter().partition(|(x, dest_path)| {
|
||||||
|
let p = Self::file_to_check_many(x, dest_path);
|
||||||
|
match file_exists {
|
||||||
|
CheckFileExists::Remote => self.remote_file_exists(p.as_path()),
|
||||||
|
CheckFileExists::HostBridge => self.host_bridge_file_exists(p.as_path()),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// filter only files to replace
|
||||||
|
let existing_files = match self.get_files_to_replace(existing_files) {
|
||||||
|
GetFileToReplaceResult::Replace(files) => files,
|
||||||
|
GetFileToReplaceResult::Cancel => {
|
||||||
|
return TransferFilesWithOverwritesResult::Cancel;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// merge back
|
||||||
|
TransferFilesWithOverwritesResult::FilesToTransfer(
|
||||||
|
existing_files.into_iter().chain(new_files).collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ pub use popups::{
|
|||||||
ATTR_FILES, ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, FatalPopup,
|
ATTR_FILES, ChmodPopup, CopyPopup, DeletePopup, DisconnectPopup, ErrorPopup, FatalPopup,
|
||||||
FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
FileInfoPopup, FilterPopup, GotoPopup, KeybindingsPopup, MkdirPopup, NewfilePopup,
|
||||||
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
OpenWithPopup, ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
||||||
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
|
SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote, SymlinkPopup,
|
||||||
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList,
|
SyncBrowsingMkdirPopup, WaitPopup, WalkdirWaitPopup, WatchedPathsList, WatcherPopup,
|
||||||
WatcherPopup,
|
|
||||||
};
|
};
|
||||||
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
|
pub use transfer::{ExplorerFind, ExplorerFuzzy, ExplorerLocal, ExplorerRemote};
|
||||||
|
|
||||||
|
|||||||
@@ -1196,7 +1196,7 @@ impl ReplacePopup {
|
|||||||
.modifiers(BorderType::Rounded),
|
.modifiers(BorderType::Rounded),
|
||||||
)
|
)
|
||||||
.foreground(color)
|
.foreground(color)
|
||||||
.choices(["Yes", "No"])
|
.choices(["Replace", "Skip", "Replace All", "Skip All", "Cancel"])
|
||||||
.title(text, Alignment::Center),
|
.title(text, Alignment::Center),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1205,9 +1205,6 @@ impl ReplacePopup {
|
|||||||
impl Component<Msg, NoUserEvent> for ReplacePopup {
|
impl Component<Msg, NoUserEvent> for ReplacePopup {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
|
||||||
Some(Msg::Ui(UiMsg::ReplacePopupTabbed))
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Left, ..
|
code: Key::Left, ..
|
||||||
}) => {
|
}) => {
|
||||||
@@ -1221,102 +1218,36 @@ impl Component<Msg, NoUserEvent> for ReplacePopup {
|
|||||||
Some(Msg::None)
|
Some(Msg::None)
|
||||||
}
|
}
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||||
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
|
||||||
}
|
}
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('y'),
|
code: Key::Char('y'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::PendingAction(PendingActionMsg::TransferPendingFile)),
|
}) => Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Char('n'),
|
code: Key::Char('n'),
|
||||||
modifiers: KeyModifiers::NONE,
|
modifiers: KeyModifiers::NONE,
|
||||||
}) => Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups)),
|
}) => Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)),
|
||||||
Event::Keyboard(KeyEvent {
|
Event::Keyboard(KeyEvent {
|
||||||
code: Key::Enter, ..
|
code: Key::Enter, ..
|
||||||
}) => {
|
}) => match self.perform(Cmd::Submit) {
|
||||||
if matches!(
|
CmdResult::Submit(State::One(StateValue::Usize(0))) => {
|
||||||
self.perform(Cmd::Submit),
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite))
|
||||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
|
||||||
) {
|
|
||||||
Some(Msg::PendingAction(PendingActionMsg::TransferPendingFile))
|
|
||||||
} else {
|
|
||||||
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
|
|
||||||
}
|
}
|
||||||
}
|
CmdResult::Submit(State::One(StateValue::Usize(1))) => {
|
||||||
_ => None,
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip))
|
||||||
}
|
}
|
||||||
}
|
CmdResult::Submit(State::One(StateValue::Usize(2))) => {
|
||||||
}
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll))
|
||||||
|
}
|
||||||
#[derive(MockComponent)]
|
CmdResult::Submit(State::One(StateValue::Usize(3))) => {
|
||||||
pub struct ReplacingFilesListPopup {
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkipAll))
|
||||||
component: List,
|
}
|
||||||
}
|
CmdResult::Submit(State::One(StateValue::Usize(4))) => {
|
||||||
|
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
|
||||||
impl ReplacingFilesListPopup {
|
}
|
||||||
pub fn new(files: &[String], color: Color) -> Self {
|
_ => Some(Msg::None),
|
||||||
Self {
|
},
|
||||||
component: List::default()
|
|
||||||
.borders(
|
|
||||||
Borders::default()
|
|
||||||
.color(color)
|
|
||||||
.modifiers(BorderType::Rounded),
|
|
||||||
)
|
|
||||||
.scroll(true)
|
|
||||||
.step(4)
|
|
||||||
.highlighted_color(color)
|
|
||||||
.highlighted_str("➤ ")
|
|
||||||
.title(
|
|
||||||
"The following files are going to be replaced",
|
|
||||||
Alignment::Center,
|
|
||||||
)
|
|
||||||
.rows(files.iter().map(|x| vec![TextSpan::from(x)]).collect()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for ReplacingFilesListPopup {
|
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
|
||||||
match ev {
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
|
||||||
Some(Msg::PendingAction(PendingActionMsg::CloseReplacePopups))
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
|
|
||||||
Some(Msg::Ui(UiMsg::ReplacePopupTabbed))
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Down, ..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::Move(Direction::Down));
|
|
||||||
Some(Msg::None)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
|
||||||
self.perform(Cmd::Move(Direction::Up));
|
|
||||||
Some(Msg::None)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::PageDown,
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::Scroll(Direction::Down));
|
|
||||||
Some(Msg::None)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::PageUp, ..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::Scroll(Direction::Up));
|
|
||||||
Some(Msg::None)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent {
|
|
||||||
code: Key::Home, ..
|
|
||||||
}) => {
|
|
||||||
self.perform(Cmd::GoTo(Position::Begin));
|
|
||||||
Some(Msg::None)
|
|
||||||
}
|
|
||||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
|
||||||
self.perform(Cmd::GoTo(Position::End));
|
|
||||||
Some(Msg::None)
|
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ enum Id {
|
|||||||
QuitPopup,
|
QuitPopup,
|
||||||
RenamePopup,
|
RenamePopup,
|
||||||
ReplacePopup,
|
ReplacePopup,
|
||||||
ReplacingFilesListPopup,
|
|
||||||
SaveAsPopup,
|
SaveAsPopup,
|
||||||
SortingPopup,
|
SortingPopup,
|
||||||
StatusBarHostBridge,
|
StatusBarHostBridge,
|
||||||
@@ -98,10 +97,14 @@ enum Msg {
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum PendingActionMsg {
|
enum PendingActionMsg {
|
||||||
CloseReplacePopups,
|
|
||||||
CloseSyncBrowsingMkdirPopup,
|
CloseSyncBrowsingMkdirPopup,
|
||||||
MakePendingDirectory,
|
MakePendingDirectory,
|
||||||
TransferPendingFile,
|
/// Replace file popup
|
||||||
|
ReplaceCancel,
|
||||||
|
ReplaceOverwrite,
|
||||||
|
ReplaceOverwriteAll,
|
||||||
|
ReplaceSkip,
|
||||||
|
ReplaceSkipAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
@@ -171,8 +174,8 @@ enum UiMsg {
|
|||||||
MarkAll,
|
MarkAll,
|
||||||
/// Clear all marks
|
/// Clear all marks
|
||||||
MarkClear,
|
MarkClear,
|
||||||
|
|
||||||
Quit,
|
Quit,
|
||||||
ReplacePopupTabbed,
|
|
||||||
ShowChmodPopup,
|
ShowChmodPopup,
|
||||||
ShowCopyPopup,
|
ShowCopyPopup,
|
||||||
ShowDeletePopup,
|
ShowDeletePopup,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
// locals
|
// locals
|
||||||
// externals
|
// externals
|
||||||
use remotefs::fs::File;
|
use remotefs::fs::File;
|
||||||
use tuirealm::props::{AttrValue, Attribute};
|
|
||||||
use tuirealm::{State, StateValue, Update};
|
use tuirealm::{State, StateValue, Update};
|
||||||
|
|
||||||
use super::actions::SelectedFile;
|
use super::actions::SelectedFile;
|
||||||
@@ -504,15 +503,6 @@ impl FileTransferActivity {
|
|||||||
self.disconnect_and_quit();
|
self.disconnect_and_quit();
|
||||||
self.umount_quit();
|
self.umount_quit();
|
||||||
}
|
}
|
||||||
UiMsg::ReplacePopupTabbed => {
|
|
||||||
if let Ok(Some(AttrValue::Flag(true))) =
|
|
||||||
self.app.query(&Id::ReplacePopup, Attribute::Focus)
|
|
||||||
{
|
|
||||||
assert!(self.app.active(&Id::ReplacingFilesListPopup).is_ok());
|
|
||||||
} else {
|
|
||||||
assert!(self.app.active(&Id::ReplacePopup).is_ok());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UiMsg::ShowChmodPopup => {
|
UiMsg::ShowChmodPopup => {
|
||||||
let selected_file = match self.browser.tab() {
|
let selected_file = match self.browser.tab() {
|
||||||
#[cfg(posix)]
|
#[cfg(posix)]
|
||||||
|
|||||||
@@ -269,29 +269,10 @@ impl FileTransferActivity {
|
|||||||
// make popup
|
// make popup
|
||||||
self.app.view(&Id::DeletePopup, f, popup);
|
self.app.view(&Id::DeletePopup, f, popup);
|
||||||
} else if self.app.mounted(&Id::ReplacePopup) {
|
} else if self.app.mounted(&Id::ReplacePopup) {
|
||||||
// NOTE: handle extended / normal modes
|
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area());
|
||||||
if self.is_radio_replace_extended() {
|
f.render_widget(Clear, popup);
|
||||||
let popup = Popup(Size::Percentage(50), Size::Percentage(50)).draw_in(f.area());
|
// make popup
|
||||||
f.render_widget(Clear, popup);
|
self.app.view(&Id::ReplacePopup, f, popup);
|
||||||
let popup_chunks = Layout::default()
|
|
||||||
.direction(Direction::Vertical)
|
|
||||||
.constraints(
|
|
||||||
[
|
|
||||||
Constraint::Percentage(85), // List
|
|
||||||
Constraint::Percentage(15), // Radio
|
|
||||||
]
|
|
||||||
.as_ref(),
|
|
||||||
)
|
|
||||||
.split(popup);
|
|
||||||
self.app
|
|
||||||
.view(&Id::ReplacingFilesListPopup, f, popup_chunks[0]);
|
|
||||||
self.app.view(&Id::ReplacePopup, f, popup_chunks[1]);
|
|
||||||
} else {
|
|
||||||
let popup = Popup(Size::Percentage(50), Size::Unit(3)).draw_in(f.area());
|
|
||||||
f.render_widget(Clear, popup);
|
|
||||||
// make popup
|
|
||||||
self.app.view(&Id::ReplacePopup, f, popup);
|
|
||||||
}
|
|
||||||
} else if self.app.mounted(&Id::DisconnectPopup) {
|
} else if self.app.mounted(&Id::DisconnectPopup) {
|
||||||
let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area());
|
let popup = Popup(Size::Percentage(30), Size::Unit(3)).draw_in(f.area());
|
||||||
f.render_widget(Clear, popup);
|
f.render_widget(Clear, popup);
|
||||||
@@ -944,37 +925,8 @@ impl FileTransferActivity {
|
|||||||
assert!(self.app.active(&Id::ReplacePopup).is_ok());
|
assert!(self.app.active(&Id::ReplacePopup).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_radio_replace_many(&mut self, files: &[String]) {
|
|
||||||
let warn_color = self.theme().misc_warn_dialog;
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.remount(
|
|
||||||
Id::ReplacingFilesListPopup,
|
|
||||||
Box::new(components::ReplacingFilesListPopup::new(files, warn_color)),
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
self.app
|
|
||||||
.remount(
|
|
||||||
Id::ReplacePopup,
|
|
||||||
Box::new(components::ReplacePopup::new(None, warn_color)),
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
);
|
|
||||||
assert!(self.app.active(&Id::ReplacePopup).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether radio replace is in "extended" mode (for many files)
|
|
||||||
pub(super) fn is_radio_replace_extended(&self) -> bool {
|
|
||||||
self.app.mounted(&Id::ReplacingFilesListPopup)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn umount_radio_replace(&mut self) {
|
pub(super) fn umount_radio_replace(&mut self) {
|
||||||
let _ = self.app.umount(&Id::ReplacePopup);
|
let _ = self.app.umount(&Id::ReplacePopup);
|
||||||
let _ = self.app.umount(&Id::ReplacingFilesListPopup); // NOTE: replace anyway
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_file_info(&mut self, file: &File) {
|
pub(super) fn mount_file_info(&mut self, file: &File) {
|
||||||
|
|||||||
Reference in New Issue
Block a user