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
commit 0d2967423b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 874 additions and 391 deletions

View file

@ -25,8 +25,14 @@ Released on FIXME: ??
- Added the possibility to enabled the synchronized brower navigation
- when you enter a directory, the same directory will be entered on the other tab
- Enable sync browser with `<Y>`
- Read more on manual: [Synchronized browsing](docs/man.md#Synchronized-browsing-)
- **Remote and Local hosts file formatter**:
- 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
- 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

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
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.
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:

View file

@ -4,7 +4,10 @@
- [Usage ❓](#usage-)
- [Address argument 🌎](#address-argument-)
- [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-)
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
- [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 |
|---------------|-------------------------------------------------------|-------------|
@ -100,7 +114,8 @@ Password can be basically provided through 3 ways when address argument is provi
| `<G>` | Go to supplied path | Go to |
| `<H>` | Show help | Help |
| `<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 |
| `<O>` | Edit file; see [Text editor](#text-editor-) | Open |
| `<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 |
| `<Y>` | Toggle synchronized browsing | sYnc |
| `<DEL>` | Delete file | |
| `<CTRL+A>` | Select all files | |
| `<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 ⭐

View file

@ -26,41 +26,34 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel};
use std::path::PathBuf;
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use std::path::{Path, PathBuf};
impl FileTransferActivity {
/// ### action_local_copy
///
/// Copy file on local
pub(crate) fn action_local_copy(&mut self, input: String) {
if let Some(idx) = self.get_local_file_idx() {
let dest_path: PathBuf = PathBuf::from(input);
let entry: FsEntry = self.local().get(idx).unwrap().clone();
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
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
),
),
match self.get_local_selected_entries() {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.local_copy_file(&entry, dest_path.as_path());
// Reload entries
self.reload_local_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.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
pub(crate) fn action_remote_copy(&mut self, input: String) {
if let Some(idx) = self.get_remote_file_idx() {
let dest_path: PathBuf = PathBuf::from(input);
let entry: FsEntry = self.remote().get(idx).unwrap().clone();
match self.client.as_mut().copy(&entry, dest_path.as_path()) {
Ok(_) => {
self.log(
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
),
),
match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.remote_copy_file(&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(_) => {
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
),
),
}
}
}

View file

@ -26,58 +26,90 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel};
use std::path::PathBuf;
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
impl FileTransferActivity {
pub(crate) fn action_local_delete(&mut self) {
let entry: Option<FsEntry> = self.get_local_file_entry().cloned();
if let Some(entry) = entry {
let full_path: PathBuf = entry.get_abs_path();
// 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),
);
}
match self.get_local_selected_entries() {
SelectedEntry::One(entry) => {
// Delete file
self.local_remove_file(&entry);
// Reload
self.reload_local_dir();
}
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) {
if let Some(idx) = self.get_remote_file_idx() {
// Check if file entry exists
let entry = self.remote().get(idx).cloned();
if let Some(entry) = entry {
let full_path: PathBuf = entry.get_abs_path();
match self.get_remote_selected_entries() {
SelectedEntry::One(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),
);
}
self.remote_remove_file(&entry);
// Reload
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(
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
),
);
}
}
}

View file

@ -26,51 +26,54 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel};
use std::path::PathBuf;
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
impl FileTransferActivity {
pub(crate) fn action_edit_local_file(&mut self) {
if self.get_local_file_entry().is_some() {
let fsentry: FsEntry = self.get_local_file_entry().unwrap().clone();
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![],
};
// Edit all entries
for entry in entries.iter() {
// Check if file
if fsentry.is_file() {
if entry.is_file() {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"...", fsentry.get_abs_path().display()),
format!("Opening file \"{}\"...", entry.get_abs_path().display()),
);
// Edit file
match self.edit_local_file(fsentry.get_abs_path().as_path()) {
Ok(_) => {
// Reload directory
let pwd: PathBuf = self.local().wrkdir.clone();
self.local_scan(pwd.as_path());
}
Err(err) => self.log_and_alert(LogLevel::Error, err),
if let Err(err) = self.edit_local_file(entry.get_abs_path().as_path()) {
self.log_and_alert(LogLevel::Error, err);
}
}
}
// Reload entries
self.reload_local_dir();
}
pub(crate) fn action_edit_remote_file(&mut self) {
if self.get_remote_file_entry().is_some() {
let fsentry: FsEntry = self.get_remote_file_entry().unwrap().clone();
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![],
};
// Edit all entries
for entry in entries.iter() {
// Check if file
if let FsEntry::File(file) = fsentry.clone() {
if let FsEntry::File(file) = entry {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"...", fsentry.get_abs_path().display()),
format!("Opening file \"{}\"...", entry.get_abs_path().display()),
);
// Edit file
match self.edit_remote_file(&file) {
Ok(_) => {
// Reload directory
let pwd: PathBuf = self.remote().wrkdir.clone();
self.remote_scan(pwd.as_path());
}
Err(err) => self.log_and_alert(LogLevel::Error, err),
if let Err(err) = self.edit_remote_file(&file) {
self.log_and_alert(LogLevel::Error, err);
}
}
}
// Reload entries
self.reload_remote_dir();
}
}

View file

@ -27,7 +27,6 @@
*/
// locals
use super::{FileTransferActivity, LogLevel};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_exec(&mut self, input: String) {
@ -35,8 +34,8 @@ impl FileTransferActivity {
Ok(output) => {
// Reload files
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
let wrkdir: PathBuf = self.local().wrkdir.clone();
self.local_scan(wrkdir.as_path());
// Reload entries
self.reload_local_dir();
}
Err(err) => {
// Report err

View file

@ -27,7 +27,7 @@
*/
// locals
use super::super::browser::FileExplorerTab;
use super::{FileTransferActivity, FsEntry, LogLevel};
use super::{FileTransferActivity, FsEntry, SelectedEntry};
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
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
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() {
None => PathBuf::from("."),
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>) {
let entry: Option<FsEntry> = self.found().as_ref().unwrap().get(idx).cloned();
if let Some(entry) = entry {
// Download file
match self.browser.tab() {
pub(crate) fn action_find_transfer(&mut self, save_as: Option<String>) {
let wrkdir: PathBuf = match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(),
FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(),
};
match self.get_found_selected_entries() {
SelectedEntry::One(entry) => match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
let wrkdir: PathBuf = self.remote().wrkdir.clone();
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), name);
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), save_as);
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
let wrkdir: PathBuf = self.local().wrkdir.clone();
self.filetransfer_recv(&entry.get_realfile(), wrkdir.as_path(), name);
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() {
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) {
let entry: Option<FsEntry> = self.found().as_ref().unwrap().get(idx).cloned();
if let Some(entry) = entry {
// Download file
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
let full_path: PathBuf = entry.get_abs_path();
// 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 => {
let full_path: PathBuf = entry.get_abs_path();
pub(crate) fn action_find_delete(&mut self) {
match self.get_found_selected_entries() {
SelectedEntry::One(entry) => {
// Delete file
self.remove_found_file(&entry);
}
SelectedEntry::Many(entries) => {
// Iter files
for entry in entries.iter() {
// 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
),
);
}
}
self.remove_found_file(entry);
}
}
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);
}
}
}
}

View file

@ -35,8 +35,8 @@ impl FileTransferActivity {
Ok(_) => {
// Reload files
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
let wrkdir: PathBuf = self.local().wrkdir.clone();
self.local_scan(wrkdir.as_path());
// Reload entries
self.reload_local_dir();
}
Err(err) => {
// Report err

View file

@ -40,46 +40,109 @@ pub(crate) mod newfile;
pub(crate) mod rename;
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 {
/// ### get_local_file_entry
/// ### get_local_selected_entries
///
/// Get local file entry
pub(crate) fn get_local_file_entry(&self) -> Option<&FsEntry> {
match self.get_local_file_idx() {
None => None,
Some(idx) => self.local().get(idx),
pub(crate) fn get_local_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_LOCAL) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(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
pub(crate) fn get_remote_file_entry(&self) -> Option<&FsEntry> {
match self.get_remote_file_idx() {
None => None,
Some(idx) => self.remote().get(idx),
pub(crate) fn get_remote_selected_entries(&self) -> SelectedEntry {
match self.get_selected_index(super::COMPONENT_EXPLORER_REMOTE) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(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
/// ### get_local_file_idx
///
/// Get index of selected file in the local tab
fn get_local_file_idx(&self) -> Option<usize> {
match self.view.get_state(super::COMPONENT_EXPLORER_LOCAL) {
Some(Payload::One(Value::Usize(idx))) => Some(idx),
_ => 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,
fn get_selected_index(&self, component: &str) -> SelectedEntryIndex {
match self.view.get_state(component) {
Some(Payload::One(Value::Usize(idx))) => SelectedEntryIndex::One(idx),
Some(Payload::Vec(files)) => {
let list: Vec<usize> = files
.iter()
.map(|x| match x {
Value::Usize(v) => *v,
_ => 0,
})
.collect();
SelectedEntryIndex::Many(list)
}
_ => SelectedEntryIndex::None,
}
}
}

View file

@ -59,8 +59,7 @@ impl FileTransferActivity {
);
}
// Reload files
let path: PathBuf = self.local().wrkdir.clone();
self.local_scan(path.as_path());
self.reload_local_dir();
}
pub(crate) fn action_remote_newfile(&mut self, input: String) {
@ -119,8 +118,7 @@ impl FileTransferActivity {
);
}
// Reload files
let path: PathBuf = self.remote().wrkdir.clone();
self.remote_scan(path.as_path());
self.reload_remote_dir();
}
}
}

View file

@ -26,77 +26,103 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel};
use std::path::PathBuf;
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use std::path::{Path, PathBuf};
impl FileTransferActivity {
pub(crate) fn action_local_rename(&mut self, input: String) {
let entry: Option<FsEntry> = self.get_local_file_entry().cloned();
if let Some(entry) = entry {
let mut dst_path: PathBuf = PathBuf::from(input);
// Check if path is relative
if dst_path.as_path().is_relative() {
let mut wrkdir: PathBuf = self.local().wrkdir.clone();
wrkdir.push(dst_path);
dst_path = wrkdir;
match self.get_local_selected_entries() {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.local_rename_file(&entry, dest_path.as_path());
// Reload entries
self.reload_local_dir();
}
let full_path: PathBuf = entry.get_abs_path();
// Rename file or directory and report status as popup
match self.host.rename(&entry, dst_path.as_path()) {
Ok(_) => {
// Reload files
let path: PathBuf = self.local().wrkdir.clone();
self.local_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.local_rename_file(entry, dest_path.as_path());
}
// Reload entries
self.reload_local_dir();
}
SelectedEntry::None => {}
}
}
pub(crate) fn action_remote_rename(&mut self, input: String) {
if let Some(idx) = self.get_remote_file_idx() {
let entry = self.remote().get(idx).cloned();
if let Some(entry) = entry {
let dst_path: PathBuf = PathBuf::from(input);
let full_path: PathBuf = entry.get_abs_path();
// Rename file or directory and report status as popup
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),
);
}
}
match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.remote_rename_file(&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_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
),
),
}
}
}

View file

@ -26,31 +26,65 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry};
use super::{FileTransferActivity, SelectedEntry};
use std::path::PathBuf;
impl FileTransferActivity {
pub(crate) fn action_local_saveas(&mut self, input: String) {
if let Some(idx) = self.get_local_file_idx() {
// 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));
}
}
self.action_local_send_file(Some(input));
}
pub(crate) fn action_remote_saveas(&mut self, input: String) {
if let Some(idx) = self.get_remote_file_idx() {
// 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));
self.action_remote_recv_file(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 => {}
}
}
}

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
///
/// Send fs entry to remote.
@ -257,8 +262,7 @@ impl FileTransferActivity {
}
}
// Scan dir on remote
let path: PathBuf = self.remote().wrkdir.clone();
self.remote_scan(path.as_path());
self.reload_remote_dir();
// If aborted; show popup
if self.transfer.aborted {
// Log abort

View file

@ -29,10 +29,10 @@
extern crate bytesize;
// locals
use super::{
browser::FileExplorerTab, FileTransferActivity, LogLevel, COMPONENT_EXPLORER_FIND,
COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE, COMPONENT_INPUT_COPY,
COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO, COMPONENT_INPUT_MKDIR,
COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS,
actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel,
COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE,
COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO,
COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS,
COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR, COMPONENT_RADIO_DELETE,
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING,
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
@ -101,18 +101,8 @@ impl FileTransferActivity {
}
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_SPACE) => {
// Get pwd
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()
} else {
None
}
self.action_local_send();
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_A) => {
// Toggle hidden files
@ -121,8 +111,7 @@ impl FileTransferActivity {
self.update_local_filelist()
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_I) => {
let file: Option<FsEntry> = self.get_local_file_entry().cloned();
if let Some(file) = file {
if let SelectedEntry::One(file) = self.get_local_selected_entries() {
self.mount_file_info(&file);
}
None
@ -175,17 +164,8 @@ impl FileTransferActivity {
}
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_SPACE) => {
// Get file and clone (due to mutable / immutable stuff...)
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()
} else {
None
}
self.action_remote_recv();
self.update_local_filelist()
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_BACKSPACE) => {
// Go to previous directory
@ -204,8 +184,7 @@ impl FileTransferActivity {
self.update_remote_filelist()
}
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_I) => {
let file: Option<FsEntry> = self.get_remote_file_entry().cloned();
if let Some(file) = file {
if let SelectedEntry::One(file) = self.get_remote_selected_entries() {
self.mount_file_info(&file);
}
None
@ -324,9 +303,9 @@ impl FileTransferActivity {
self.finalize_find();
None
}
(COMPONENT_EXPLORER_FIND, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
(COMPONENT_EXPLORER_FIND, Msg::OnSubmit(_)) => {
// Find changedir
self.action_find_changedir(*idx);
self.action_find_changedir();
// Umount find
self.umount_find();
// Finalize find
@ -340,17 +319,12 @@ impl FileTransferActivity {
}
(COMPONENT_EXPLORER_FIND, &MSG_KEY_SPACE) => {
// Get entry
match self.view.get_state(COMPONENT_EXPLORER_FIND) {
Some(Payload::One(Value::Usize(idx))) => {
self.action_find_transfer(idx, None);
// Reload files
match self.browser.tab() {
// NOTE: swapped by purpose
FileExplorerTab::FindLocal => self.update_remote_filelist(),
FileExplorerTab::FindRemote => self.update_local_filelist(),
_ => None,
}
}
self.action_find_transfer(None);
// Reload files
match self.browser.tab() {
// NOTE: swapped by purpose
FileExplorerTab::FindLocal => self.update_remote_filelist(),
FileExplorerTab::FindRemote => self.update_local_filelist(),
_ => None,
}
}
@ -540,11 +514,7 @@ impl FileTransferActivity {
FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
// Get entry
if let Some(Payload::One(Value::Usize(idx))) =
self.view.get_state(COMPONENT_EXPLORER_FIND)
{
self.action_find_transfer(idx, Some(input.to_string()));
}
self.action_find_transfer(Some(input.to_string()));
}
}
self.umount_saveas();
@ -576,14 +546,25 @@ impl FileTransferActivity {
FileExplorerTab::Remote => self.action_remote_delete(),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
// Get entry
if let Some(Payload::One(Value::Usize(idx))) =
self.view.get_state(COMPONENT_EXPLORER_FIND)
{
self.action_find_delete(idx);
// Reload entries
self.found_mut().unwrap().del_entry(idx);
self.update_find_list();
self.action_find_delete();
// Delete entries
match self.view.get_state(COMPONENT_EXPLORER_FIND) {
Some(Payload::One(Value::Usize(idx))) => {
// Reload entries
self.found_mut().unwrap().del_entry(idx);
}
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();

View file

@ -975,6 +975,14 @@ impl FileTransferActivity {
)
.add_col(TextSpan::from(" Reload directory content"))
.add_row()
.add_col(
TextSpanBuilder::new("<M>")
.bold()
.with_foreground(Color::Cyan)
.build(),
)
.add_col(TextSpan::from(" Select file"))
.add_row()
.add_col(
TextSpanBuilder::new("<N>")
.bold()
@ -1047,6 +1055,14 @@ impl FileTransferActivity {
)
.add_col(TextSpan::from(" Delete selected file"))
.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(
TextSpanBuilder::new("<CTRL+C>")
.bold()

View file

@ -27,7 +27,7 @@
*/
// ext
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::tui::{
layout::{Corner, Rect},
@ -133,33 +133,34 @@ impl FileListPropsBuilder {
/// OwnStates contains states for this component
#[derive(Clone)]
struct OwnStates {
list_index: usize, // Index of selected element in list
list_len: usize, // Length of file list
focus: bool, // Has focus?
list_index: usize, // Index of selected element in list
selected: Vec<usize>, // Selected files
focus: bool, // Has focus?
}
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
list_index: 0,
list_len: 0,
selected: Vec::new(),
focus: false,
}
}
}
impl OwnStates {
/// ### set_list_len
/// ### init_list_states
///
/// Set list length
pub fn set_list_len(&mut self, len: usize) {
self.list_len = len;
/// Initialize list states
pub fn init_list_states(&mut self, len: usize) {
self.selected = Vec::with_capacity(len);
self.fix_list_index();
}
/// ### get_list_index
/// ### list_index
///
/// Return current value for list index
pub fn get_list_index(&self) -> usize {
pub fn list_index(&self) -> usize {
self.list_index
}
@ -168,7 +169,7 @@ impl OwnStates {
/// Incremenet list index
pub fn incr_list_index(&mut self) {
// 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;
}
}
@ -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
///
/// Keep index if possible, otherwise set to lenght - 1
pub fn fix_list_index(&mut self) {
if self.list_index >= self.list_len && self.list_len > 0 {
self.list_index = self.list_len - 1;
} else if self.list_len == 0 {
fn fix_list_index(&mut self) {
if self.list_index >= self.list_len() && self.list_len() > 0 {
self.list_index = self.list_len() - 1;
} else if self.list_len() == 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
@ -213,11 +281,8 @@ impl FileList {
pub fn new(props: Props) -> Self {
// Initialize states
let mut states: OwnStates = OwnStates::default();
// Set list length
states.set_list_len(match &props.texts.spans {
Some(tokens) => tokens.len(),
None => 0,
});
// Init list states
states.init_list_states(props.texts.spans.as_ref().map(|x| x.len()).unwrap_or(0));
FileList { props, states }
}
}
@ -231,7 +296,14 @@ impl Component for FileList {
None => vec![],
Some(lines) => lines
.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(),
};
let (fg, bg): (Color, Color) = match self.states.focus {
@ -263,13 +335,15 @@ impl Component for FileList {
fn update(&mut self, props: Props) -> Msg {
self.props = props;
// re-Set list length
self.states.set_list_len(match &self.props.texts.spans {
Some(tokens) => tokens.len(),
None => 0,
});
// Fix list index
self.states.fix_list_index();
// re-Set list states
self.states.init_list_states(
self.props
.texts
.spans
.as_ref()
.map(|x| x.len())
.unwrap_or(0),
);
Msg::None
}
@ -305,6 +379,20 @@ impl Component for FileList {
}
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 => {
// Report event
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 {
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
@ -349,6 +451,72 @@ mod tests {
use pretty_assertions::assert_eq;
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]
fn test_ui_components_file_list() {
// Make component
@ -375,7 +543,9 @@ mod tests {
assert_eq!(component.props.texts.spans.as_ref().unwrap().len(), 2);
// Verify states
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);
// Focus
component.active();
@ -408,7 +578,7 @@ mod tests {
);
// Verify states
assert_eq!(component.states.list_index, 1); // Kept
assert_eq!(component.states.list_len, 3);
assert_eq!(component.states.list_len(), 3);
// get value
assert_eq!(component.get_state(), Payload::One(Value::Usize(1)));
// Render
@ -451,5 +621,90 @@ mod tests {
component.on(Event::Key(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)));
}
}