mirror of
https://github.com/veeso/termscp.git
synced 2026-09-26 14:01:17 -07:00
Merge pull request #32 from veeso/file-group-select
Work on multiple files in explorers
This commit is contained in:
@@ -25,8 +25,14 @@ Released on FIXME: ??
|
|||||||
- Added the possibility to enabled the synchronized brower navigation
|
- Added the possibility to enabled the synchronized brower navigation
|
||||||
- when you enter a directory, the same directory will be entered on the other tab
|
- when you enter a directory, the same directory will be entered on the other tab
|
||||||
- Enable sync browser with `<Y>`
|
- Enable sync browser with `<Y>`
|
||||||
|
- Read more on manual: [Synchronized browsing](docs/man.md#Synchronized-browsing-)
|
||||||
- **Remote and Local hosts file formatter**:
|
- **Remote and Local hosts file formatter**:
|
||||||
- Added the possibility to set different formatters for local and remote hosts
|
- Added the possibility to set different formatters for local and remote hosts
|
||||||
|
- **Work on multiple files**:
|
||||||
|
- Added the possibility to work on **multiple files simultaneously**
|
||||||
|
- Select a file with `<M>`, the file when selected will have a `*` prepended to its name
|
||||||
|
- Select all files in the current directory with `<CTRL+A>`
|
||||||
|
- Read more on manual: [Work on multiple files](docs/man.md#Work-on-multiple-files-)
|
||||||
- Enhancements
|
- Enhancements
|
||||||
- Added a status bar in the file explorer showing whether the sync browser is enabled and which file sorting mode is selected
|
- Added a status bar in the file explorer showing whether the sync browser is enabled and which file sorting mode is selected
|
||||||
- Removed the goold old figlet title
|
- Removed the goold old figlet title
|
||||||
|
|||||||
@@ -184,8 +184,6 @@ The developer documentation can be found on Rust Docs at <https://docs.rs/termsc
|
|||||||
|
|
||||||
- **Themes provider 🎨**: I'm still thinking about how I will implement this, but basically the idea is to have a configuration file where it will be possible
|
- **Themes provider 🎨**: I'm still thinking about how I will implement this, but basically the idea is to have a configuration file where it will be possible
|
||||||
to define the color schema for the entire application. I haven't planned this release yet
|
to define the color schema for the entire application. I haven't planned this release yet
|
||||||
- **Synchronized browsing of local and remote directories ⌚**: See [Issue 8](https://github.com/veeso/termscp/issues/8)
|
|
||||||
- **Group file select 🤩**: Possibility to select a group of files in explorers to operate on
|
|
||||||
|
|
||||||
No other new feature is planned at the moment. I actually think that termscp is getting mature and now I should focus upcoming updates more on bug fixing and code/performance improvements than on new features.
|
No other new feature is planned at the moment. I actually think that termscp is getting mature and now I should focus upcoming updates more on bug fixing and code/performance improvements than on new features.
|
||||||
Anyway there are some ideas which I'd like to implement. If you want to start working on them, feel free to open a PR:
|
Anyway there are some ideas which I'd like to implement. If you want to start working on them, feel free to open a PR:
|
||||||
|
|||||||
+37
-2
@@ -4,7 +4,10 @@
|
|||||||
- [Usage ❓](#usage-)
|
- [Usage ❓](#usage-)
|
||||||
- [Address argument 🌎](#address-argument-)
|
- [Address argument 🌎](#address-argument-)
|
||||||
- [How Password can be provided 🔐](#how-password-can-be-provided-)
|
- [How Password can be provided 🔐](#how-password-can-be-provided-)
|
||||||
|
- [File explorer 📂](#file-explorer-)
|
||||||
- [Keybindings ⌨](#keybindings-)
|
- [Keybindings ⌨](#keybindings-)
|
||||||
|
- [Work on multiple files 🥷](#work-on-multiple-files-)
|
||||||
|
- [Synchronized browsing ⏲️](#synchronized-browsing-️)
|
||||||
- [Bookmarks ⭐](#bookmarks-)
|
- [Bookmarks ⭐](#bookmarks-)
|
||||||
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
|
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
|
||||||
- [Configuration ⚙️](#configuration-️)
|
- [Configuration ⚙️](#configuration-️)
|
||||||
@@ -76,7 +79,18 @@ Password can be basically provided through 3 ways when address argument is provi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Keybindings ⌨
|
## File explorer 📂
|
||||||
|
|
||||||
|
When we refer to file explorers in termscp, we refer to the panels you can see after establishing a connection with the remote.
|
||||||
|
These panels are basically 3 (yes, three actually):
|
||||||
|
|
||||||
|
- Local explorer panel: it is displayed on the left of your screen and shows the current directory entries for localhost
|
||||||
|
- Remote explorer panel: it is displayed on the right of your screen and shows the current directory entries for the remote host.
|
||||||
|
- Find results panel: depending on where you're searching for files (local/remote) it will replace the local or the explorer panel. This panel shows the entries matching the search query you performed.
|
||||||
|
|
||||||
|
In order to change panel you need to type `<LEFT>` to move the remote explorer panel and `<RIGHT>` to move back to the local explorer panel. Whenever you are in the find results panel, you need to press `<ESC>` to exit panel and go back to the previous panel.
|
||||||
|
|
||||||
|
### Keybindings ⌨
|
||||||
|
|
||||||
| Key | Command | Reminder |
|
| Key | Command | Reminder |
|
||||||
|---------------|-------------------------------------------------------|-------------|
|
|---------------|-------------------------------------------------------|-------------|
|
||||||
@@ -100,7 +114,8 @@ Password can be basically provided through 3 ways when address argument is provi
|
|||||||
| `<G>` | Go to supplied path | Go to |
|
| `<G>` | Go to supplied path | Go to |
|
||||||
| `<H>` | Show help | Help |
|
| `<H>` | Show help | Help |
|
||||||
| `<I>` | Show info about selected file or directory | Info |
|
| `<I>` | Show info about selected file or directory | Info |
|
||||||
| `<L>` | Reload current directory's content | List |
|
| `<L>` | Reload current directory's content / Clear selection | List |
|
||||||
|
| `<M>` | Select a file | Mark |
|
||||||
| `<N>` | Create new file with provided name | New |
|
| `<N>` | Create new file with provided name | New |
|
||||||
| `<O>` | Edit file; see [Text editor](#text-editor-) | Open |
|
| `<O>` | Edit file; see [Text editor](#text-editor-) | Open |
|
||||||
| `<Q>` | Quit termscp | Quit |
|
| `<Q>` | Quit termscp | Quit |
|
||||||
@@ -110,8 +125,28 @@ Password can be basically provided through 3 ways when address argument is provi
|
|||||||
| `<X>` | Execute a command | eXecute |
|
| `<X>` | Execute a command | eXecute |
|
||||||
| `<Y>` | Toggle synchronized browsing | sYnc |
|
| `<Y>` | Toggle synchronized browsing | sYnc |
|
||||||
| `<DEL>` | Delete file | |
|
| `<DEL>` | Delete file | |
|
||||||
|
| `<CTRL+A>` | Select all files | |
|
||||||
| `<CTRL+C>` | Abort file transfer process | |
|
| `<CTRL+C>` | Abort file transfer process | |
|
||||||
|
|
||||||
|
### Work on multiple files 🥷
|
||||||
|
|
||||||
|
You can opt to work on multiple files, selecting them pressing `<M>`, in order to select the current file, or pressing `<CTRL+A>`, which will select all the files in the working directory.
|
||||||
|
Once a file is marked for selection, it will be displayed with a `*` on the left.
|
||||||
|
When working on selection, only selected file will be processed for actions, while the current highlighted item will be ignored.
|
||||||
|
It is possible to work on multiple files also when in the find result panel.
|
||||||
|
All the actions are available when working with multiple files, but be aware that some actions work in a slightly different way. Let's dive in:
|
||||||
|
|
||||||
|
- *Copy*: whenever you copy a file, you'll be prompted to insert the destination name. When working with multiple file, this name refers to the destination directory where all these files will be copied.
|
||||||
|
- *Rename*: same as copy, but will move files there.
|
||||||
|
- *Save as*: same as copy, but will write them there.
|
||||||
|
|
||||||
|
### Synchronized browsing ⏲️
|
||||||
|
|
||||||
|
When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels.
|
||||||
|
This means that whenever you'll change the working directory on one panel, the same action will be reproduced on the other panel. If you want to enable synchronized browsing just press `<Y>`; press twice to disable. While enabled, the synchronized browising state will be reported on the status bar on `ON`.
|
||||||
|
|
||||||
|
*Warning*: at the moment, whenever you try to access an unexisting directory, you won't be prompted to create it. This might change in a future update.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Bookmarks ⭐
|
## Bookmarks ⭐
|
||||||
|
|||||||
@@ -26,41 +26,34 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel};
|
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// ### action_local_copy
|
/// ### action_local_copy
|
||||||
///
|
///
|
||||||
/// Copy file on local
|
/// Copy file on local
|
||||||
pub(crate) fn action_local_copy(&mut self, input: String) {
|
pub(crate) fn action_local_copy(&mut self, input: String) {
|
||||||
if let Some(idx) = self.get_local_file_idx() {
|
match self.get_local_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
let dest_path: PathBuf = PathBuf::from(input);
|
let dest_path: PathBuf = PathBuf::from(input);
|
||||||
let entry: FsEntry = self.local().get(idx).unwrap().clone();
|
self.local_copy_file(&entry, dest_path.as_path());
|
||||||
match self.host.copy(&entry, dest_path.as_path()) {
|
|
||||||
Ok(_) => {
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!(
|
|
||||||
"Copied \"{}\" to \"{}\"",
|
|
||||||
entry.get_abs_path().display(),
|
|
||||||
dest_path.display()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// Reload entries
|
// Reload entries
|
||||||
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
self.reload_local_dir();
|
||||||
self.local_scan(wrkdir.as_path());
|
|
||||||
}
|
}
|
||||||
Err(err) => self.log_and_alert(
|
SelectedEntry::Many(entries) => {
|
||||||
LogLevel::Error,
|
// Try to copy each file to Input/{FILE_NAME}
|
||||||
format!(
|
let base_path: PathBuf = PathBuf::from(input);
|
||||||
"Could not copy \"{}\" to \"{}\": {}",
|
// Iter files
|
||||||
entry.get_abs_path().display(),
|
for entry in entries.iter() {
|
||||||
dest_path.display(),
|
let mut dest_path: PathBuf = base_path.clone();
|
||||||
err
|
dest_path.push(entry.get_name());
|
||||||
),
|
self.local_copy_file(entry, dest_path.as_path());
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,31 +61,74 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Copy file on remote
|
/// Copy file on remote
|
||||||
pub(crate) fn action_remote_copy(&mut self, input: String) {
|
pub(crate) fn action_remote_copy(&mut self, input: String) {
|
||||||
if let Some(idx) = self.get_remote_file_idx() {
|
match self.get_remote_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
let dest_path: PathBuf = PathBuf::from(input);
|
let dest_path: PathBuf = PathBuf::from(input);
|
||||||
let entry: FsEntry = self.remote().get(idx).unwrap().clone();
|
self.remote_copy_file(&entry, dest_path.as_path());
|
||||||
match self.client.as_mut().copy(&entry, dest_path.as_path()) {
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::Many(entries) => {
|
||||||
|
// Try to copy each file to Input/{FILE_NAME}
|
||||||
|
let base_path: PathBuf = PathBuf::from(input);
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
let mut dest_path: PathBuf = base_path.clone();
|
||||||
|
dest_path.push(entry.get_name());
|
||||||
|
self.remote_copy_file(entry, dest_path.as_path());
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_copy_file(&mut self, entry: &FsEntry, dest: &Path) {
|
||||||
|
match self.host.copy(entry, dest) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Copied \"{}\" to \"{}\"",
|
"Copied \"{}\" to \"{}\"",
|
||||||
entry.get_abs_path().display(),
|
entry.get_abs_path().display(),
|
||||||
dest_path.display()
|
dest.display()
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
self.reload_remote_dir();
|
|
||||||
}
|
}
|
||||||
Err(err) => self.log_and_alert(
|
Err(err) => self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not copy \"{}\" to \"{}\": {}",
|
"Could not copy \"{}\" to \"{}\": {}",
|
||||||
entry.get_abs_path().display(),
|
entry.get_abs_path().display(),
|
||||||
dest_path.display(),
|
dest.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remote_copy_file(&mut self, entry: &FsEntry, dest: &Path) {
|
||||||
|
match self.client.as_mut().copy(entry, dest) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"Copied \"{}\" to \"{}\"",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not copy \"{}\" to \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,59 +26,91 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel};
|
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_delete(&mut self) {
|
pub(crate) fn action_local_delete(&mut self) {
|
||||||
let entry: Option<FsEntry> = self.get_local_file_entry().cloned();
|
match self.get_local_selected_entries() {
|
||||||
if let Some(entry) = entry {
|
SelectedEntry::One(entry) => {
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
// Delete file
|
||||||
// Delete file or directory and report status as popup
|
self.local_remove_file(&entry);
|
||||||
match self.host.remove(&entry) {
|
// Reload
|
||||||
Ok(_) => {
|
self.reload_local_dir();
|
||||||
// Reload files
|
|
||||||
let p: PathBuf = self.local().wrkdir.clone();
|
|
||||||
self.local_scan(p.as_path());
|
|
||||||
// Log
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Removed file \"{}\"", full_path.display()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
SelectedEntry::Many(entries) => {
|
||||||
self.log_and_alert(
|
// Iter files
|
||||||
LogLevel::Error,
|
for entry in entries.iter() {
|
||||||
format!("Could not delete file \"{}\": {}", full_path.display(), err),
|
// Delete file
|
||||||
);
|
self.local_remove_file(entry);
|
||||||
}
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
}
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_delete(&mut self) {
|
pub(crate) fn action_remote_delete(&mut self) {
|
||||||
if let Some(idx) = self.get_remote_file_idx() {
|
match self.get_remote_selected_entries() {
|
||||||
// Check if file entry exists
|
SelectedEntry::One(entry) => {
|
||||||
let entry = self.remote().get(idx).cloned();
|
|
||||||
if let Some(entry) = entry {
|
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
|
||||||
// Delete file
|
// Delete file
|
||||||
match self.client.remove(&entry) {
|
self.remote_remove_file(&entry);
|
||||||
Ok(_) => {
|
// Reload
|
||||||
self.reload_remote_dir();
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::Many(entries) => {
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
// Delete file
|
||||||
|
self.remote_remove_file(entry);
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn local_remove_file(&mut self, entry: &FsEntry) {
|
||||||
|
match self.host.remove(&entry) {
|
||||||
|
Ok(_) => {
|
||||||
|
// Log
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!("Removed file \"{}\"", full_path.display()),
|
format!("Removed file \"{}\"", entry.get_abs_path().display()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Could not delete file \"{}\": {}", full_path.display(), err),
|
format!(
|
||||||
|
"Could not delete file \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remote_remove_file(&mut self, entry: &FsEntry) {
|
||||||
|
match self.client.remove(&entry) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("Removed file \"{}\"", entry.get_abs_path().display()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
self.log_and_alert(
|
||||||
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not delete file \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,51 +26,54 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel};
|
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_edit_local_file(&mut self) {
|
pub(crate) fn action_edit_local_file(&mut self) {
|
||||||
if self.get_local_file_entry().is_some() {
|
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
|
||||||
let fsentry: FsEntry = self.get_local_file_entry().unwrap().clone();
|
SelectedEntry::One(entry) => vec![entry],
|
||||||
|
SelectedEntry::Many(entries) => entries,
|
||||||
|
SelectedEntry::None => vec![],
|
||||||
|
};
|
||||||
|
// Edit all entries
|
||||||
|
for entry in entries.iter() {
|
||||||
// Check if file
|
// Check if file
|
||||||
if fsentry.is_file() {
|
if entry.is_file() {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!("Opening file \"{}\"...", fsentry.get_abs_path().display()),
|
format!("Opening file \"{}\"...", entry.get_abs_path().display()),
|
||||||
);
|
);
|
||||||
// Edit file
|
// Edit file
|
||||||
match self.edit_local_file(fsentry.get_abs_path().as_path()) {
|
if let Err(err) = self.edit_local_file(entry.get_abs_path().as_path()) {
|
||||||
Ok(_) => {
|
self.log_and_alert(LogLevel::Error, err);
|
||||||
// Reload directory
|
|
||||||
let pwd: PathBuf = self.local().wrkdir.clone();
|
|
||||||
self.local_scan(pwd.as_path());
|
|
||||||
}
|
|
||||||
Err(err) => self.log_and_alert(LogLevel::Error, err),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_edit_remote_file(&mut self) {
|
pub(crate) fn action_edit_remote_file(&mut self) {
|
||||||
if self.get_remote_file_entry().is_some() {
|
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
|
||||||
let fsentry: FsEntry = self.get_remote_file_entry().unwrap().clone();
|
SelectedEntry::One(entry) => vec![entry],
|
||||||
|
SelectedEntry::Many(entries) => entries,
|
||||||
|
SelectedEntry::None => vec![],
|
||||||
|
};
|
||||||
|
// Edit all entries
|
||||||
|
for entry in entries.iter() {
|
||||||
// Check if file
|
// Check if file
|
||||||
if let FsEntry::File(file) = fsentry.clone() {
|
if let FsEntry::File(file) = entry {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!("Opening file \"{}\"...", fsentry.get_abs_path().display()),
|
format!("Opening file \"{}\"...", entry.get_abs_path().display()),
|
||||||
);
|
);
|
||||||
// Edit file
|
// Edit file
|
||||||
match self.edit_remote_file(&file) {
|
if let Err(err) = self.edit_remote_file(&file) {
|
||||||
Ok(_) => {
|
self.log_and_alert(LogLevel::Error, err);
|
||||||
// Reload directory
|
|
||||||
let pwd: PathBuf = self.remote().wrkdir.clone();
|
|
||||||
self.remote_scan(pwd.as_path());
|
|
||||||
}
|
|
||||||
Err(err) => self.log_and_alert(LogLevel::Error, err),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_exec(&mut self, input: String) {
|
pub(crate) fn action_local_exec(&mut self, input: String) {
|
||||||
@@ -35,8 +34,8 @@ impl FileTransferActivity {
|
|||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
// Reload files
|
// Reload files
|
||||||
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
|
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
|
||||||
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
// Reload entries
|
||||||
self.local_scan(wrkdir.as_path());
|
self.reload_local_dir();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Report err
|
// Report err
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::super::browser::FileExplorerTab;
|
use super::super::browser::FileExplorerTab;
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel};
|
use super::{FileTransferActivity, FsEntry, SelectedEntry};
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -46,12 +46,12 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_find_changedir(&mut self, idx: usize) {
|
pub(crate) fn action_find_changedir(&mut self) {
|
||||||
// Match entry
|
// Match entry
|
||||||
if let Some(entry) = self.found().as_ref().unwrap().get(idx) {
|
if let SelectedEntry::One(entry) = self.get_found_selected_entries() {
|
||||||
// Get path: if a directory, use directory path; if it is a File, get parent path
|
// Get path: if a directory, use directory path; if it is a File, get parent path
|
||||||
let path: PathBuf = match entry {
|
let path: PathBuf = match entry {
|
||||||
FsEntry::Directory(dir) => dir.abs_path.clone(),
|
FsEntry::Directory(dir) => dir.abs_path,
|
||||||
FsEntry::File(file) => match file.abs_path.parent() {
|
FsEntry::File(file) => match file.abs_path.parent() {
|
||||||
None => PathBuf::from("."),
|
None => PathBuf::from("."),
|
||||||
Some(p) => p.to_path_buf(),
|
Some(p) => p.to_path_buf(),
|
||||||
@@ -69,77 +69,74 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_find_transfer(&mut self, idx: usize, name: Option<String>) {
|
pub(crate) fn action_find_transfer(&mut self, save_as: Option<String>) {
|
||||||
let entry: Option<FsEntry> = self.found().as_ref().unwrap().get(idx).cloned();
|
let wrkdir: PathBuf = match self.browser.tab() {
|
||||||
if let Some(entry) = entry {
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(),
|
||||||
// Download file
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(),
|
||||||
match self.browser.tab() {
|
};
|
||||||
|
match self.get_found_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||||
let wrkdir: PathBuf = self.remote().wrkdir.clone();
|
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), save_as);
|
||||||
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), name);
|
|
||||||
}
|
}
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
self.filetransfer_recv(&entry.get_realfile(), wrkdir.as_path(), save_as);
|
||||||
self.filetransfer_recv(&entry.get_realfile(), wrkdir.as_path(), name);
|
}
|
||||||
|
},
|
||||||
|
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 {
|
||||||
|
dest_path.push(save_as);
|
||||||
|
}
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
match self.browser.tab() {
|
||||||
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||||
|
self.filetransfer_send(
|
||||||
|
&entry.get_realfile(),
|
||||||
|
dest_path.as_path(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
|
self.filetransfer_recv(
|
||||||
|
&entry.get_realfile(),
|
||||||
|
dest_path.as_path(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn action_find_delete(&mut self, idx: usize) {
|
pub(crate) fn action_find_delete(&mut self) {
|
||||||
let entry: Option<FsEntry> = self.found().as_ref().unwrap().get(idx).cloned();
|
match self.get_found_selected_entries() {
|
||||||
if let Some(entry) = entry {
|
SelectedEntry::One(entry) => {
|
||||||
// Download file
|
// Delete file
|
||||||
|
self.remove_found_file(&entry);
|
||||||
|
}
|
||||||
|
SelectedEntry::Many(entries) => {
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
// Delete file
|
||||||
|
self.remove_found_file(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_found_file(&mut self, entry: &FsEntry) {
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
self.local_remove_file(entry);
|
||||||
// Delete file or directory and report status as popup
|
|
||||||
match self.host.remove(&entry) {
|
|
||||||
Ok(_) => {
|
|
||||||
// Reload files
|
|
||||||
let p: PathBuf = self.local().wrkdir.clone();
|
|
||||||
self.local_scan(p.as_path());
|
|
||||||
// Log
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Removed file \"{}\"", full_path.display()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not delete file \"{}\": {}",
|
|
||||||
full_path.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
self.remote_remove_file(entry);
|
||||||
// Delete file
|
|
||||||
match self.client.remove(&entry) {
|
|
||||||
Ok(_) => {
|
|
||||||
self.reload_remote_dir();
|
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!("Removed file \"{}\"", full_path.display()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!(
|
|
||||||
"Could not delete file \"{}\": {}",
|
|
||||||
full_path.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ impl FileTransferActivity {
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Reload files
|
// Reload files
|
||||||
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
|
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
|
||||||
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
// Reload entries
|
||||||
self.local_scan(wrkdir.as_path());
|
self.reload_local_dir();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// Report err
|
// Report err
|
||||||
|
|||||||
@@ -40,46 +40,109 @@ pub(crate) mod newfile;
|
|||||||
pub(crate) mod rename;
|
pub(crate) mod rename;
|
||||||
pub(crate) mod save;
|
pub(crate) mod save;
|
||||||
|
|
||||||
|
pub(crate) enum SelectedEntry {
|
||||||
|
One(FsEntry),
|
||||||
|
Many(Vec<FsEntry>),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SelectedEntryIndex {
|
||||||
|
One(usize),
|
||||||
|
Many(Vec<usize>),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<&FsEntry>> for SelectedEntry {
|
||||||
|
fn from(opt: Option<&FsEntry>) -> Self {
|
||||||
|
match opt {
|
||||||
|
Some(e) => SelectedEntry::One(e.clone()),
|
||||||
|
None => SelectedEntry::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Vec<&FsEntry>> for SelectedEntry {
|
||||||
|
fn from(files: Vec<&FsEntry>) -> Self {
|
||||||
|
SelectedEntry::Many(files.into_iter().cloned().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
/// ### get_local_file_entry
|
/// ### get_local_selected_entries
|
||||||
///
|
///
|
||||||
/// Get local file entry
|
/// Get local file entry
|
||||||
pub(crate) fn get_local_file_entry(&self) -> Option<&FsEntry> {
|
pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry {
|
||||||
match self.get_local_file_idx() {
|
match self.get_selected_index(super::COMPONENT_EXPLORER_LOCAL) {
|
||||||
None => None,
|
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)),
|
||||||
Some(idx) => self.local().get(idx),
|
SelectedEntryIndex::Many(files) => {
|
||||||
|
let files: Vec<&FsEntry> = files
|
||||||
|
.iter()
|
||||||
|
.map(|x| self.local().get(*x)) // Usize to Option<FsEntry>
|
||||||
|
.filter(|x| x.is_some()) // Get only some values
|
||||||
|
.map(|x| x.unwrap()) // Option to FsEntry
|
||||||
|
.collect();
|
||||||
|
SelectedEntry::from(files)
|
||||||
|
}
|
||||||
|
SelectedEntryIndex::None => SelectedEntry::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get_remote_file_entry
|
/// ### get_remote_selected_entries
|
||||||
///
|
///
|
||||||
/// Get remote file entry
|
/// Get remote file entry
|
||||||
pub(crate) fn get_remote_file_entry(&self) -> Option<&FsEntry> {
|
pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry {
|
||||||
match self.get_remote_file_idx() {
|
match self.get_selected_index(super::COMPONENT_EXPLORER_REMOTE) {
|
||||||
None => None,
|
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)),
|
||||||
Some(idx) => self.remote().get(idx),
|
SelectedEntryIndex::Many(files) => {
|
||||||
|
let files: Vec<&FsEntry> = files
|
||||||
|
.iter()
|
||||||
|
.map(|x| self.remote().get(*x)) // Usize to Option<FsEntry>
|
||||||
|
.filter(|x| x.is_some()) // Get only some values
|
||||||
|
.map(|x| x.unwrap()) // Option to FsEntry
|
||||||
|
.collect();
|
||||||
|
SelectedEntry::from(files)
|
||||||
|
}
|
||||||
|
SelectedEntryIndex::None => SelectedEntry::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_remote_selected_entries
|
||||||
|
///
|
||||||
|
/// Get remote file entry
|
||||||
|
pub(crate) fn get_found_selected_entries(&self) -> SelectedEntry {
|
||||||
|
match self.get_selected_index(super::COMPONENT_EXPLORER_FIND) {
|
||||||
|
SelectedEntryIndex::One(idx) => {
|
||||||
|
SelectedEntry::from(self.found().as_ref().unwrap().get(idx))
|
||||||
|
}
|
||||||
|
SelectedEntryIndex::Many(files) => {
|
||||||
|
let files: Vec<&FsEntry> = files
|
||||||
|
.iter()
|
||||||
|
.map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option<FsEntry>
|
||||||
|
.filter(|x| x.is_some()) // Get only some values
|
||||||
|
.map(|x| x.unwrap()) // Option to FsEntry
|
||||||
|
.collect();
|
||||||
|
SelectedEntry::from(files)
|
||||||
|
}
|
||||||
|
SelectedEntryIndex::None => SelectedEntry::None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- private
|
// -- private
|
||||||
|
|
||||||
/// ### get_local_file_idx
|
fn get_selected_index(&self, component: &str) -> SelectedEntryIndex {
|
||||||
///
|
match self.view.get_state(component) {
|
||||||
/// Get index of selected file in the local tab
|
Some(Payload::One(Value::Usize(idx))) => SelectedEntryIndex::One(idx),
|
||||||
fn get_local_file_idx(&self) -> Option<usize> {
|
Some(Payload::Vec(files)) => {
|
||||||
match self.view.get_state(super::COMPONENT_EXPLORER_LOCAL) {
|
let list: Vec<usize> = files
|
||||||
Some(Payload::One(Value::Usize(idx))) => Some(idx),
|
.iter()
|
||||||
_ => None,
|
.map(|x| match x {
|
||||||
|
Value::Usize(v) => *v,
|
||||||
|
_ => 0,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
SelectedEntryIndex::Many(list)
|
||||||
}
|
}
|
||||||
}
|
_ => SelectedEntryIndex::None,
|
||||||
|
|
||||||
/// ### get_remote_file_idx
|
|
||||||
///
|
|
||||||
/// Get index of selected file in the remote file
|
|
||||||
fn get_remote_file_idx(&self) -> Option<usize> {
|
|
||||||
match self.view.get_state(super::COMPONENT_EXPLORER_REMOTE) {
|
|
||||||
Some(Payload::One(Value::Usize(idx))) => Some(idx),
|
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ impl FileTransferActivity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Reload files
|
// Reload files
|
||||||
let path: PathBuf = self.local().wrkdir.clone();
|
self.reload_local_dir();
|
||||||
self.local_scan(path.as_path());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_newfile(&mut self, input: String) {
|
pub(crate) fn action_remote_newfile(&mut self, input: String) {
|
||||||
@@ -119,8 +118,7 @@ impl FileTransferActivity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Reload files
|
// Reload files
|
||||||
let path: PathBuf = self.remote().wrkdir.clone();
|
self.reload_remote_dir();
|
||||||
self.remote_scan(path.as_path());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,77 +26,103 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel};
|
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_rename(&mut self, input: String) {
|
pub(crate) fn action_local_rename(&mut self, input: String) {
|
||||||
let entry: Option<FsEntry> = self.get_local_file_entry().cloned();
|
match self.get_local_selected_entries() {
|
||||||
if let Some(entry) = entry {
|
SelectedEntry::One(entry) => {
|
||||||
let mut dst_path: PathBuf = PathBuf::from(input);
|
let dest_path: PathBuf = PathBuf::from(input);
|
||||||
// Check if path is relative
|
self.local_rename_file(&entry, dest_path.as_path());
|
||||||
if dst_path.as_path().is_relative() {
|
// Reload entries
|
||||||
let mut wrkdir: PathBuf = self.local().wrkdir.clone();
|
self.reload_local_dir();
|
||||||
wrkdir.push(dst_path);
|
|
||||||
dst_path = wrkdir;
|
|
||||||
}
|
}
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
SelectedEntry::Many(entries) => {
|
||||||
// Rename file or directory and report status as popup
|
// Try to copy each file to Input/{FILE_NAME}
|
||||||
match self.host.rename(&entry, dst_path.as_path()) {
|
let base_path: PathBuf = PathBuf::from(input);
|
||||||
Ok(_) => {
|
// Iter files
|
||||||
// Reload files
|
for entry in entries.iter() {
|
||||||
let path: PathBuf = self.local().wrkdir.clone();
|
let mut dest_path: PathBuf = base_path.clone();
|
||||||
self.local_scan(path.as_path());
|
dest_path.push(entry.get_name());
|
||||||
// Log
|
self.local_rename_file(entry, dest_path.as_path());
|
||||||
self.log(
|
|
||||||
LogLevel::Info,
|
|
||||||
format!(
|
|
||||||
"Renamed file \"{}\" to \"{}\"",
|
|
||||||
full_path.display(),
|
|
||||||
dst_path.display()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
|
||||||
format!("Could not rename file \"{}\": {}", full_path.display(), err),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_local_dir();
|
||||||
}
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_rename(&mut self, input: String) {
|
pub(crate) fn action_remote_rename(&mut self, input: String) {
|
||||||
if let Some(idx) = self.get_remote_file_idx() {
|
match self.get_remote_selected_entries() {
|
||||||
let entry = self.remote().get(idx).cloned();
|
SelectedEntry::One(entry) => {
|
||||||
if let Some(entry) = entry {
|
let dest_path: PathBuf = PathBuf::from(input);
|
||||||
let dst_path: PathBuf = PathBuf::from(input);
|
self.remote_rename_file(&entry, dest_path.as_path());
|
||||||
let full_path: PathBuf = entry.get_abs_path();
|
// Reload entries
|
||||||
// Rename file or directory and report status as popup
|
self.reload_remote_dir();
|
||||||
match self.client.as_mut().rename(&entry, dst_path.as_path()) {
|
}
|
||||||
|
SelectedEntry::Many(entries) => {
|
||||||
|
// Try to copy each file to Input/{FILE_NAME}
|
||||||
|
let base_path: PathBuf = PathBuf::from(input);
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
let mut dest_path: PathBuf = base_path.clone();
|
||||||
|
dest_path.push(entry.get_name());
|
||||||
|
self.remote_rename_file(entry, dest_path.as_path());
|
||||||
|
}
|
||||||
|
// Reload entries
|
||||||
|
self.reload_remote_dir();
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_rename_file(&mut self, entry: &FsEntry, dest: &Path) {
|
||||||
|
match self.host.rename(entry, dest) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Reload files
|
|
||||||
let path: PathBuf = self.remote().wrkdir.clone();
|
|
||||||
self.remote_scan(path.as_path());
|
|
||||||
// Log
|
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Renamed file \"{}\" to \"{}\"",
|
"Moved \"{}\" to \"{}\"",
|
||||||
full_path.display(),
|
entry.get_abs_path().display(),
|
||||||
dst_path.display()
|
dest.display()
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => self.log_and_alert(
|
||||||
self.log_and_alert(
|
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!("Could not rename file \"{}\": {}", full_path.display(), err),
|
format!(
|
||||||
|
"Could not move \"{}\" to \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote_rename_file(&mut self, entry: &FsEntry, dest: &Path) {
|
||||||
|
match self.client.as_mut().rename(entry, dest) {
|
||||||
|
Ok(_) => {
|
||||||
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!(
|
||||||
|
"Moved \"{}\" to \"{}\"",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display()
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
Err(err) => self.log_and_alert(
|
||||||
}
|
LogLevel::Error,
|
||||||
|
format!(
|
||||||
|
"Could not move \"{}\" to \"{}\": {}",
|
||||||
|
entry.get_abs_path().display(),
|
||||||
|
dest.display(),
|
||||||
|
err
|
||||||
|
),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,31 +26,65 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry};
|
use super::{FileTransferActivity, SelectedEntry};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_saveas(&mut self, input: String) {
|
pub(crate) fn action_local_saveas(&mut self, input: String) {
|
||||||
if let Some(idx) = self.get_local_file_idx() {
|
self.action_local_send_file(Some(input));
|
||||||
// Get pwd
|
|
||||||
let wrkdir: PathBuf = self.remote().wrkdir.clone();
|
|
||||||
if self.local().get(idx).is_some() {
|
|
||||||
let file: FsEntry = self.local().get(idx).unwrap().clone();
|
|
||||||
// Call upload; pass realfile, keep link name
|
|
||||||
self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(input));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_saveas(&mut self, input: String) {
|
pub(crate) fn action_remote_saveas(&mut self, input: String) {
|
||||||
if let Some(idx) = self.get_remote_file_idx() {
|
self.action_remote_recv_file(Some(input));
|
||||||
// Get pwd
|
|
||||||
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
|
||||||
if self.remote().get(idx).is_some() {
|
|
||||||
let file: FsEntry = self.remote().get(idx).unwrap().clone();
|
|
||||||
// Call upload; pass realfile, keep link name
|
|
||||||
self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(input));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_local_send(&mut self) {
|
||||||
|
self.action_local_send_file(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn action_remote_recv(&mut self) {
|
||||||
|
self.action_remote_recv_file(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_local_send_file(&mut self, save_as: Option<String>) {
|
||||||
|
let wrkdir: PathBuf = self.remote().wrkdir.clone();
|
||||||
|
match self.get_local_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), save_as);
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
dest_path.push(save_as);
|
||||||
|
}
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
self.filetransfer_send(&entry.get_realfile(), dest_path.as_path(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_remote_recv_file(&mut self, save_as: Option<String>) {
|
||||||
|
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
||||||
|
match self.get_remote_selected_entries() {
|
||||||
|
SelectedEntry::One(entry) => {
|
||||||
|
self.filetransfer_recv(&entry.get_realfile(), wrkdir.as_path(), save_as);
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
dest_path.push(save_as);
|
||||||
|
}
|
||||||
|
// Iter files
|
||||||
|
for entry in entries.iter() {
|
||||||
|
self.filetransfer_recv(&entry.get_realfile(), dest_path.as_path(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectedEntry::None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,11 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn reload_local_dir(&mut self) {
|
||||||
|
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
||||||
|
self.local_scan(wrkdir.as_path());
|
||||||
|
}
|
||||||
|
|
||||||
/// ### filetransfer_send
|
/// ### filetransfer_send
|
||||||
///
|
///
|
||||||
/// Send fs entry to remote.
|
/// Send fs entry to remote.
|
||||||
@@ -257,8 +262,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Scan dir on remote
|
// Scan dir on remote
|
||||||
let path: PathBuf = self.remote().wrkdir.clone();
|
self.reload_remote_dir();
|
||||||
self.remote_scan(path.as_path());
|
|
||||||
// If aborted; show popup
|
// If aborted; show popup
|
||||||
if self.transfer.aborted {
|
if self.transfer.aborted {
|
||||||
// Log abort
|
// Log abort
|
||||||
|
|||||||
@@ -29,10 +29,10 @@
|
|||||||
extern crate bytesize;
|
extern crate bytesize;
|
||||||
// locals
|
// locals
|
||||||
use super::{
|
use super::{
|
||||||
browser::FileExplorerTab, FileTransferActivity, LogLevel, COMPONENT_EXPLORER_FIND,
|
actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel,
|
||||||
COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY,
|
COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE,
|
||||||
COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR,
|
COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO,
|
||||||
COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS,
|
COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS,
|
||||||
COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR, COMPONENT_RADIO_DELETE,
|
COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR, COMPONENT_RADIO_DELETE,
|
||||||
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING,
|
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING,
|
||||||
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
|
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
|
||||||
@@ -101,18 +101,8 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_SPACE) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_SPACE) => {
|
||||||
// Get pwd
|
self.action_local_send();
|
||||||
let wrkdir: PathBuf = self.remote().wrkdir.clone();
|
|
||||||
// Get file and clone (due to mutable / immutable stuff...)
|
|
||||||
if self.get_local_file_entry().is_some() {
|
|
||||||
let file: FsEntry = self.get_local_file_entry().unwrap().clone();
|
|
||||||
let name: String = file.get_name().to_string();
|
|
||||||
// Call upload; pass realfile, keep link name
|
|
||||||
self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(name));
|
|
||||||
self.update_remote_filelist()
|
self.update_remote_filelist()
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_A) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_A) => {
|
||||||
// Toggle hidden files
|
// Toggle hidden files
|
||||||
@@ -121,8 +111,7 @@ impl FileTransferActivity {
|
|||||||
self.update_local_filelist()
|
self.update_local_filelist()
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_I) => {
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_I) => {
|
||||||
let file: Option<FsEntry> = self.get_local_file_entry().cloned();
|
if let SelectedEntry::One(file) = self.get_local_selected_entries() {
|
||||||
if let Some(file) = file {
|
|
||||||
self.mount_file_info(&file);
|
self.mount_file_info(&file);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -175,17 +164,8 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_SPACE) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_SPACE) => {
|
||||||
// Get file and clone (due to mutable / immutable stuff...)
|
self.action_remote_recv();
|
||||||
if self.get_remote_file_entry().is_some() {
|
|
||||||
let file: FsEntry = self.get_remote_file_entry().unwrap().clone();
|
|
||||||
let name: String = file.get_name().to_string();
|
|
||||||
// Call upload; pass realfile, keep link name
|
|
||||||
let wrkdir: PathBuf = self.local().wrkdir.clone();
|
|
||||||
self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(name));
|
|
||||||
self.update_local_filelist()
|
self.update_local_filelist()
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_BACKSPACE) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_BACKSPACE) => {
|
||||||
// Go to previous directory
|
// Go to previous directory
|
||||||
@@ -204,8 +184,7 @@ impl FileTransferActivity {
|
|||||||
self.update_remote_filelist()
|
self.update_remote_filelist()
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_I) => {
|
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_I) => {
|
||||||
let file: Option<FsEntry> = self.get_remote_file_entry().cloned();
|
if let SelectedEntry::One(file) = self.get_remote_selected_entries() {
|
||||||
if let Some(file) = file {
|
|
||||||
self.mount_file_info(&file);
|
self.mount_file_info(&file);
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -324,9 +303,9 @@ impl FileTransferActivity {
|
|||||||
self.finalize_find();
|
self.finalize_find();
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_FIND, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
|
(COMPONENT_EXPLORER_FIND, Msg::OnSubmit(_)) => {
|
||||||
// Find changedir
|
// Find changedir
|
||||||
self.action_find_changedir(*idx);
|
self.action_find_changedir();
|
||||||
// Umount find
|
// Umount find
|
||||||
self.umount_find();
|
self.umount_find();
|
||||||
// Finalize find
|
// Finalize find
|
||||||
@@ -340,9 +319,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
(COMPONENT_EXPLORER_FIND, &MSG_KEY_SPACE) => {
|
(COMPONENT_EXPLORER_FIND, &MSG_KEY_SPACE) => {
|
||||||
// Get entry
|
// Get entry
|
||||||
match self.view.get_state(COMPONENT_EXPLORER_FIND) {
|
self.action_find_transfer(None);
|
||||||
Some(Payload::One(Value::Usize(idx))) => {
|
|
||||||
self.action_find_transfer(idx, None);
|
|
||||||
// Reload files
|
// Reload files
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
// NOTE: swapped by purpose
|
// NOTE: swapped by purpose
|
||||||
@@ -351,9 +328,6 @@ impl FileTransferActivity {
|
|||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// -- switch to log
|
// -- switch to log
|
||||||
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_TAB)
|
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_TAB)
|
||||||
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_TAB) => {
|
| (COMPONENT_EXPLORER_REMOTE, &MSG_KEY_TAB) => {
|
||||||
@@ -540,11 +514,7 @@ impl FileTransferActivity {
|
|||||||
FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()),
|
FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()),
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||||
// Get entry
|
// Get entry
|
||||||
if let Some(Payload::One(Value::Usize(idx))) =
|
self.action_find_transfer(Some(input.to_string()));
|
||||||
self.view.get_state(COMPONENT_EXPLORER_FIND)
|
|
||||||
{
|
|
||||||
self.action_find_transfer(idx, Some(input.to_string()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.umount_saveas();
|
self.umount_saveas();
|
||||||
@@ -576,14 +546,25 @@ impl FileTransferActivity {
|
|||||||
FileExplorerTab::Remote => self.action_remote_delete(),
|
FileExplorerTab::Remote => self.action_remote_delete(),
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
|
||||||
// Get entry
|
// Get entry
|
||||||
if let Some(Payload::One(Value::Usize(idx))) =
|
self.action_find_delete();
|
||||||
self.view.get_state(COMPONENT_EXPLORER_FIND)
|
// Delete entries
|
||||||
{
|
match self.view.get_state(COMPONENT_EXPLORER_FIND) {
|
||||||
self.action_find_delete(idx);
|
Some(Payload::One(Value::Usize(idx))) => {
|
||||||
// Reload entries
|
// Reload entries
|
||||||
self.found_mut().unwrap().del_entry(idx);
|
self.found_mut().unwrap().del_entry(idx);
|
||||||
self.update_find_list();
|
|
||||||
}
|
}
|
||||||
|
Some(Payload::Vec(values)) => {
|
||||||
|
values
|
||||||
|
.iter()
|
||||||
|
.map(|x| match x {
|
||||||
|
Value::Usize(v) => *v,
|
||||||
|
_ => 0,
|
||||||
|
})
|
||||||
|
.for_each(|x| self.found_mut().unwrap().del_entry(x));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.update_find_list();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.umount_radio_delete();
|
self.umount_radio_delete();
|
||||||
|
|||||||
@@ -975,6 +975,14 @@ impl FileTransferActivity {
|
|||||||
)
|
)
|
||||||
.add_col(TextSpan::from(" Reload directory content"))
|
.add_col(TextSpan::from(" Reload directory content"))
|
||||||
.add_row()
|
.add_row()
|
||||||
|
.add_col(
|
||||||
|
TextSpanBuilder::new("<M>")
|
||||||
|
.bold()
|
||||||
|
.with_foreground(Color::Cyan)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.add_col(TextSpan::from(" Select file"))
|
||||||
|
.add_row()
|
||||||
.add_col(
|
.add_col(
|
||||||
TextSpanBuilder::new("<N>")
|
TextSpanBuilder::new("<N>")
|
||||||
.bold()
|
.bold()
|
||||||
@@ -1047,6 +1055,14 @@ impl FileTransferActivity {
|
|||||||
)
|
)
|
||||||
.add_col(TextSpan::from(" Delete selected file"))
|
.add_col(TextSpan::from(" Delete selected file"))
|
||||||
.add_row()
|
.add_row()
|
||||||
|
.add_col(
|
||||||
|
TextSpanBuilder::new("<CTRL+A>")
|
||||||
|
.bold()
|
||||||
|
.with_foreground(Color::Cyan)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.add_col(TextSpan::from(" Select all files"))
|
||||||
|
.add_row()
|
||||||
.add_col(
|
.add_col(
|
||||||
TextSpanBuilder::new("<CTRL+C>")
|
TextSpanBuilder::new("<CTRL+C>")
|
||||||
.bold()
|
.bold()
|
||||||
|
|||||||
+285
-30
@@ -27,7 +27,7 @@
|
|||||||
*/
|
*/
|
||||||
// ext
|
// ext
|
||||||
use tuirealm::components::utils::get_block;
|
use tuirealm::components::utils::get_block;
|
||||||
use tuirealm::event::{Event, KeyCode};
|
use tuirealm::event::{Event, KeyCode, KeyModifiers};
|
||||||
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
|
use tuirealm::props::{BordersProps, Props, PropsBuilder, TextParts, TextSpan};
|
||||||
use tuirealm::tui::{
|
use tuirealm::tui::{
|
||||||
layout::{Corner, Rect},
|
layout::{Corner, Rect},
|
||||||
@@ -134,7 +134,7 @@ impl FileListPropsBuilder {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct OwnStates {
|
struct OwnStates {
|
||||||
list_index: usize, // Index of selected element in list
|
list_index: usize, // Index of selected element in list
|
||||||
list_len: usize, // Length of file list
|
selected: Vec<usize>, // Selected files
|
||||||
focus: bool, // Has focus?
|
focus: bool, // Has focus?
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,24 +142,25 @@ impl Default for OwnStates {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
OwnStates {
|
OwnStates {
|
||||||
list_index: 0,
|
list_index: 0,
|
||||||
list_len: 0,
|
selected: Vec::new(),
|
||||||
focus: false,
|
focus: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OwnStates {
|
impl OwnStates {
|
||||||
/// ### set_list_len
|
/// ### init_list_states
|
||||||
///
|
///
|
||||||
/// Set list length
|
/// Initialize list states
|
||||||
pub fn set_list_len(&mut self, len: usize) {
|
pub fn init_list_states(&mut self, len: usize) {
|
||||||
self.list_len = len;
|
self.selected = Vec::with_capacity(len);
|
||||||
|
self.fix_list_index();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get_list_index
|
/// ### list_index
|
||||||
///
|
///
|
||||||
/// Return current value for list index
|
/// Return current value for list index
|
||||||
pub fn get_list_index(&self) -> usize {
|
pub fn list_index(&self) -> usize {
|
||||||
self.list_index
|
self.list_index
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +169,7 @@ impl OwnStates {
|
|||||||
/// Incremenet list index
|
/// Incremenet list index
|
||||||
pub fn incr_list_index(&mut self) {
|
pub fn incr_list_index(&mut self) {
|
||||||
// Check if index is at last element
|
// Check if index is at last element
|
||||||
if self.list_index + 1 < self.list_len {
|
if self.list_index + 1 < self.list_len() {
|
||||||
self.list_index += 1;
|
self.list_index += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,16 +184,83 @@ impl OwnStates {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### list_len
|
||||||
|
///
|
||||||
|
/// Returns the length of the file list, which is actually the capacity of the selection vector
|
||||||
|
pub fn list_len(&self) -> usize {
|
||||||
|
self.selected.capacity()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### is_selected
|
||||||
|
///
|
||||||
|
/// Returns whether the file with index `entry` is selected
|
||||||
|
pub fn is_selected(&self, entry: usize) -> bool {
|
||||||
|
self.selected.contains(&entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### is_selection_empty
|
||||||
|
///
|
||||||
|
/// Returns whether the selection is currently empty
|
||||||
|
pub fn is_selection_empty(&self) -> bool {
|
||||||
|
self.selected.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### get_selection
|
||||||
|
///
|
||||||
|
/// Returns current file selection
|
||||||
|
pub fn get_selection(&self) -> Vec<usize> {
|
||||||
|
self.selected.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// ### fix_list_index
|
/// ### fix_list_index
|
||||||
///
|
///
|
||||||
/// Keep index if possible, otherwise set to lenght - 1
|
/// Keep index if possible, otherwise set to lenght - 1
|
||||||
pub fn fix_list_index(&mut self) {
|
fn fix_list_index(&mut self) {
|
||||||
if self.list_index >= self.list_len && self.list_len > 0 {
|
if self.list_index >= self.list_len() && self.list_len() > 0 {
|
||||||
self.list_index = self.list_len - 1;
|
self.list_index = self.list_len() - 1;
|
||||||
} else if self.list_len == 0 {
|
} else if self.list_len() == 0 {
|
||||||
self.list_index = 0;
|
self.list_index = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- select manipulation
|
||||||
|
|
||||||
|
/// ### toggle_file
|
||||||
|
///
|
||||||
|
/// Select or deselect file with provided entry index
|
||||||
|
pub fn toggle_file(&mut self, entry: usize) {
|
||||||
|
match self.is_selected(entry) {
|
||||||
|
true => self.deselect(entry),
|
||||||
|
false => self.select(entry),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### select_all
|
||||||
|
///
|
||||||
|
/// Select all files
|
||||||
|
pub fn select_all(&mut self) {
|
||||||
|
for i in 0..self.list_len() {
|
||||||
|
self.select(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### select
|
||||||
|
///
|
||||||
|
/// Select provided index if not selected yet
|
||||||
|
fn select(&mut self, entry: usize) {
|
||||||
|
if !self.is_selected(entry) {
|
||||||
|
self.selected.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### deselect
|
||||||
|
///
|
||||||
|
/// Remove element file with associated index
|
||||||
|
fn deselect(&mut self, entry: usize) {
|
||||||
|
if self.is_selected(entry) {
|
||||||
|
self.selected.retain(|&x| x != entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- Component
|
// -- Component
|
||||||
@@ -213,11 +281,8 @@ impl FileList {
|
|||||||
pub fn new(props: Props) -> Self {
|
pub fn new(props: Props) -> Self {
|
||||||
// Initialize states
|
// Initialize states
|
||||||
let mut states: OwnStates = OwnStates::default();
|
let mut states: OwnStates = OwnStates::default();
|
||||||
// Set list length
|
// Init list states
|
||||||
states.set_list_len(match &props.texts.spans {
|
states.init_list_states(props.texts.spans.as_ref().map(|x| x.len()).unwrap_or(0));
|
||||||
Some(tokens) => tokens.len(),
|
|
||||||
None => 0,
|
|
||||||
});
|
|
||||||
FileList { props, states }
|
FileList { props, states }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,7 +296,14 @@ impl Component for FileList {
|
|||||||
None => vec![],
|
None => vec![],
|
||||||
Some(lines) => lines
|
Some(lines) => lines
|
||||||
.iter()
|
.iter()
|
||||||
.map(|line| ListItem::new(Span::from(line.content.to_string())))
|
.enumerate()
|
||||||
|
.map(|(num, line)| {
|
||||||
|
let to_display: String = match self.states.is_selected(num) {
|
||||||
|
true => format!("*{}", line.content),
|
||||||
|
false => line.content.to_string(),
|
||||||
|
};
|
||||||
|
ListItem::new(Span::from(to_display))
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
let (fg, bg): (Color, Color) = match self.states.focus {
|
let (fg, bg): (Color, Color) = match self.states.focus {
|
||||||
@@ -263,13 +335,15 @@ impl Component for FileList {
|
|||||||
|
|
||||||
fn update(&mut self, props: Props) -> Msg {
|
fn update(&mut self, props: Props) -> Msg {
|
||||||
self.props = props;
|
self.props = props;
|
||||||
// re-Set list length
|
// re-Set list states
|
||||||
self.states.set_list_len(match &self.props.texts.spans {
|
self.states.init_list_states(
|
||||||
Some(tokens) => tokens.len(),
|
self.props
|
||||||
None => 0,
|
.texts
|
||||||
});
|
.spans
|
||||||
// Fix list index
|
.as_ref()
|
||||||
self.states.fix_list_index();
|
.map(|x| x.len())
|
||||||
|
.unwrap_or(0),
|
||||||
|
);
|
||||||
Msg::None
|
Msg::None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,6 +379,20 @@ impl Component for FileList {
|
|||||||
}
|
}
|
||||||
Msg::None
|
Msg::None
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('a') => match key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||||
|
// CTRL+A
|
||||||
|
true => {
|
||||||
|
// Select all
|
||||||
|
self.states.select_all();
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
|
false => Msg::OnKey(key),
|
||||||
|
},
|
||||||
|
KeyCode::Char('m') => {
|
||||||
|
// Toggle current file in selection
|
||||||
|
self.states.toggle_file(self.states.list_index());
|
||||||
|
Msg::None
|
||||||
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
// Report event
|
// Report event
|
||||||
Msg::OnSubmit(self.get_state())
|
Msg::OnSubmit(self.get_state())
|
||||||
@@ -320,8 +408,22 @@ impl Component for FileList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### get_state
|
||||||
|
///
|
||||||
|
/// Get state returns for this component two different payloads based on the states:
|
||||||
|
/// - if the file selection is empty, returns the highlighted item as `One` of `Usize`
|
||||||
|
/// - if at least one item is selected, return the selected as a `Vec` of `Usize`
|
||||||
fn get_state(&self) -> Payload {
|
fn get_state(&self) -> Payload {
|
||||||
Payload::One(Value::Usize(self.states.get_list_index()))
|
match self.states.is_selection_empty() {
|
||||||
|
true => Payload::One(Value::Usize(self.states.list_index())),
|
||||||
|
false => Payload::Vec(
|
||||||
|
self.states
|
||||||
|
.get_selection()
|
||||||
|
.into_iter()
|
||||||
|
.map(Value::Usize)
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- events
|
// -- events
|
||||||
@@ -349,6 +451,72 @@ mod tests {
|
|||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use tuirealm::event::KeyEvent;
|
use tuirealm::event::KeyEvent;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ui_components_file_list_states() {
|
||||||
|
let mut states: OwnStates = OwnStates::default();
|
||||||
|
assert_eq!(states.list_len(), 0);
|
||||||
|
assert_eq!(states.selected.len(), 0);
|
||||||
|
assert_eq!(states.focus, false);
|
||||||
|
// Init states
|
||||||
|
states.init_list_states(4);
|
||||||
|
assert_eq!(states.list_len(), 4);
|
||||||
|
assert_eq!(states.selected.len(), 0);
|
||||||
|
assert!(states.is_selection_empty());
|
||||||
|
// Select all files
|
||||||
|
states.select_all();
|
||||||
|
assert_eq!(states.list_len(), 4);
|
||||||
|
assert_eq!(states.selected.len(), 4);
|
||||||
|
assert_eq!(states.is_selection_empty(), false);
|
||||||
|
assert_eq!(states.get_selection(), vec![0, 1, 2, 3]);
|
||||||
|
// Verify reset
|
||||||
|
states.init_list_states(5);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 0);
|
||||||
|
// Toggle file
|
||||||
|
states.toggle_file(2);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 1);
|
||||||
|
assert_eq!(states.selected[0], 2);
|
||||||
|
states.toggle_file(4);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 2);
|
||||||
|
assert_eq!(states.selected[1], 4);
|
||||||
|
states.toggle_file(2);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 1);
|
||||||
|
assert_eq!(states.selected[0], 4);
|
||||||
|
// Select twice (nothing should change)
|
||||||
|
states.select(4);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 1);
|
||||||
|
assert_eq!(states.selected[0], 4);
|
||||||
|
// Deselect not-selectd item
|
||||||
|
states.deselect(2);
|
||||||
|
assert_eq!(states.list_len(), 5);
|
||||||
|
assert_eq!(states.selected.len(), 1);
|
||||||
|
assert_eq!(states.selected[0], 4);
|
||||||
|
// Index
|
||||||
|
states.init_list_states(2);
|
||||||
|
states.incr_list_index();
|
||||||
|
assert_eq!(states.list_index(), 1);
|
||||||
|
states.incr_list_index();
|
||||||
|
assert_eq!(states.list_index(), 1);
|
||||||
|
states.decr_list_index();
|
||||||
|
assert_eq!(states.list_index(), 0);
|
||||||
|
states.decr_list_index();
|
||||||
|
assert_eq!(states.list_index(), 0);
|
||||||
|
// Try fixing index
|
||||||
|
states.init_list_states(5);
|
||||||
|
states.list_index = 4;
|
||||||
|
states.init_list_states(3);
|
||||||
|
assert_eq!(states.list_index(), 2);
|
||||||
|
states.init_list_states(6);
|
||||||
|
assert_eq!(states.list_index(), 2);
|
||||||
|
// Focus
|
||||||
|
states.focus = true;
|
||||||
|
assert_eq!(states.focus, true);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ui_components_file_list() {
|
fn test_ui_components_file_list() {
|
||||||
// Make component
|
// Make component
|
||||||
@@ -375,7 +543,9 @@ mod tests {
|
|||||||
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2);
|
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2);
|
||||||
// Verify states
|
// Verify states
|
||||||
assert_eq!(component.states.list_index, 0);
|
assert_eq!(component.states.list_index, 0);
|
||||||
assert_eq!(component.states.list_len, 2);
|
assert_eq!(component.states.selected.len(), 0);
|
||||||
|
assert_eq!(component.states.list_len(), 2);
|
||||||
|
assert_eq!(component.states.selected.capacity(), 2);
|
||||||
assert_eq!(component.states.focus, false);
|
assert_eq!(component.states.focus, false);
|
||||||
// Focus
|
// Focus
|
||||||
component.active();
|
component.active();
|
||||||
@@ -408,7 +578,7 @@ mod tests {
|
|||||||
);
|
);
|
||||||
// Verify states
|
// Verify states
|
||||||
assert_eq!(component.states.list_index, 1); // Kept
|
assert_eq!(component.states.list_index, 1); // Kept
|
||||||
assert_eq!(component.states.list_len, 3);
|
assert_eq!(component.states.list_len(), 3);
|
||||||
// get value
|
// get value
|
||||||
assert_eq!(component.get_state(), Payload::One(Value::Usize(1)));
|
assert_eq!(component.get_state(), Payload::One(Value::Usize(1)));
|
||||||
// Render
|
// Render
|
||||||
@@ -451,5 +621,90 @@ mod tests {
|
|||||||
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
|
||||||
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
Msg::OnKey(KeyEvent::from(KeyCode::Backspace))
|
||||||
);
|
);
|
||||||
|
// Verify 'A' still works
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('a')))),
|
||||||
|
Msg::OnKey(KeyEvent::from(KeyCode::Char('a')))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ui_components_file_list_selection() {
|
||||||
|
// Make component
|
||||||
|
let mut component: FileList = FileList::new(
|
||||||
|
FileListPropsBuilder::default()
|
||||||
|
.with_files(
|
||||||
|
Some(String::from("files")),
|
||||||
|
vec![
|
||||||
|
String::from("file1"),
|
||||||
|
String::from("file2"),
|
||||||
|
String::from("file3"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
// Get state
|
||||||
|
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
|
||||||
|
// Select one
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// Now should be a vec
|
||||||
|
assert_eq!(component.get_state(), Payload::Vec(vec![Value::Usize(0)]));
|
||||||
|
// De-select
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
assert_eq!(component.get_state(), Payload::One(Value::Usize(0)));
|
||||||
|
// Go down
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// Select
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
assert_eq!(component.get_state(), Payload::Vec(vec![Value::Usize(1)]));
|
||||||
|
// Go down and select
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Down))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent::from(KeyCode::Char('m')))),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
component.get_state(),
|
||||||
|
Payload::Vec(vec![Value::Usize(1), Value::Usize(2)])
|
||||||
|
);
|
||||||
|
// Select all
|
||||||
|
assert_eq!(
|
||||||
|
component.on(Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char('a'),
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
})),
|
||||||
|
Msg::None
|
||||||
|
);
|
||||||
|
// All selected
|
||||||
|
assert_eq!(
|
||||||
|
component.get_state(),
|
||||||
|
Payload::Vec(vec![Value::Usize(1), Value::Usize(2), Value::Usize(0)])
|
||||||
|
);
|
||||||
|
// Update files
|
||||||
|
component.update(
|
||||||
|
FileListPropsBuilder::from(component.get_props())
|
||||||
|
.with_files(
|
||||||
|
Some(String::from("filelist")),
|
||||||
|
vec![String::from("file1"), String::from("file2")],
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
// Selection should now be empty
|
||||||
|
assert_eq!(component.get_state(), Payload::One(Value::Usize(1)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user