Merge pull request #32 from veeso/file-group-select

Work on multiple files in explorers
This commit is contained in:
Christian Visintin
2021-05-15 18:00:02 +02:00
committed by GitHub
17 changed files with 874 additions and 391 deletions
+6
View File
@@ -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
-2
View File
@@ -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:
+38 -3
View File
@@ -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-)
- [Keybindings ⌨](#keybindings-) - [File explorer 📂](#file-explorer-)
- [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 ⭐
+88 -52
View File
@@ -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() {
let dest_path: PathBuf = PathBuf::from(input); SelectedEntry::One(entry) => {
let entry: FsEntry = self.local().get(idx).unwrap().clone(); let dest_path: PathBuf = PathBuf::from(input);
match self.host.copy(&entry, dest_path.as_path()) { self.local_copy_file(&entry, dest_path.as_path());
Ok(_) => { // Reload entries
self.log( self.reload_local_dir();
LogLevel::Info,
format!(
"Copied \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest_path.display()
),
);
// Reload entries
let wrkdir: PathBuf = self.local().wrkdir.clone();
self.local_scan(wrkdir.as_path());
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Could not copy \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
dest_path.display(),
err
),
),
} }
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.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() {
let dest_path: PathBuf = PathBuf::from(input); SelectedEntry::One(entry) => {
let entry: FsEntry = self.remote().get(idx).unwrap().clone(); let dest_path: PathBuf = PathBuf::from(input);
match self.client.as_mut().copy(&entry, dest_path.as_path()) { self.remote_copy_file(&entry, dest_path.as_path());
Ok(_) => { // Reload entries
self.log( self.reload_remote_dir();
LogLevel::Info,
format!(
"Copied \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest_path.display()
),
);
self.reload_remote_dir();
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Could not copy \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
dest_path.display(),
err
),
),
} }
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(_) => {
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
),
),
}
}
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,58 +26,90 @@
* 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) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not delete file \"{}\": {}", full_path.display(), err),
);
}
} }
SelectedEntry::Many(entries) => {
// Iter files
for entry in entries.iter() {
// 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();
self.log( }
LogLevel::Info, SelectedEntry::Many(entries) => {
format!("Removed file \"{}\"", full_path.display()), // Iter files
); for entry in entries.iter() {
} // Delete file
Err(err) => { self.remote_remove_file(entry);
self.log_and_alert(
LogLevel::Error,
format!("Could not delete file \"{}\": {}", full_path.display(), err),
);
}
} }
// 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(
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
),
);
}
}
}
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
),
);
} }
} }
} }
+27 -24
View File
@@ -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
+62 -65
View File
@@ -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,78 +69,75 @@ 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
match self.browser.tab() { self.remove_found_file(&entry);
FileExplorerTab::FindLocal | FileExplorerTab::Local => { }
let full_path: PathBuf = entry.get_abs_path(); SelectedEntry::Many(entries) => {
// Delete file or directory and report status as popup // Iter files
match self.host.remove(&entry) { for entry in entries.iter() {
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 => {
let full_path: PathBuf = entry.get_abs_path();
// Delete file // Delete file
match self.client.remove(&entry) { self.remove_found_file(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
),
);
}
}
} }
} }
SelectedEntry::None => {}
}
}
fn remove_found_file(&mut self, entry: &FsEntry) {
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.local_remove_file(entry);
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
self.remote_remove_file(entry);
}
} }
} }
} }
@@ -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
+90 -27
View File
@@ -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,
})
/// ### get_remote_file_idx .collect();
/// SelectedEntryIndex::Many(list)
/// Get index of selected file in the remote file }
fn get_remote_file_idx(&self) -> Option<usize> { _ => SelectedEntryIndex::None,
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()) {
Ok(_) => {
// Reload files
let path: PathBuf = self.remote().wrkdir.clone();
self.remote_scan(path.as_path());
// Log
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),
);
}
}
} }
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(_) => {
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
),
),
}
}
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
),
),
} }
} }
} }
+51 -17
View File
@@ -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() { pub(crate) fn action_local_send(&mut self) {
let file: FsEntry = self.remote().get(idx).unwrap().clone(); self.action_local_send_file(None);
// Call upload; pass realfile, keep link name }
self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(input));
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 => {}
} }
} }
} }
+6 -2
View File
@@ -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
+37 -56
View File
@@ -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(); self.update_remote_filelist()
// 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()
} 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() { self.update_local_filelist()
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()
} 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,17 +319,12 @@ 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))) => { // Reload files
self.action_find_transfer(idx, None); match self.browser.tab() {
// Reload files // NOTE: swapped by purpose
match self.browser.tab() { FileExplorerTab::FindLocal => self.update_remote_filelist(),
// NOTE: swapped by purpose FileExplorerTab::FindRemote => self.update_local_filelist(),
FileExplorerTab::FindLocal => self.update_remote_filelist(),
FileExplorerTab::FindRemote => self.update_local_filelist(),
_ => None,
}
}
_ => None, _ => None,
} }
} }
@@ -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();
+16
View File
@@ -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()
+287 -32
View File
@@ -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},
@@ -133,33 +133,34 @@ impl FileListPropsBuilder {
/// OwnStates contains states for this component /// OwnStates contains states for this component
#[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?
} }
impl Default for OwnStates { 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)));
} }
} }