diff --git a/CHANGELOG.md b/CHANGELOG.md index 42ac483..cf6ae61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,10 @@ FIXME: Released on - Enhancements: - Replaced `sha256` sum with last modification time check, to verify if a file has been changed in the text editor - Default protocol changed to default protocol in configuration when providing address as CLI argument + - Explorers: + - Hidden files are now not shown by default; use `A` to show hidden files. - Keybindings: + - `A`: Toggle hidden files - `N`: New file - Dependencies: - removed `data-encoding` diff --git a/README.md b/README.md index 42c1a0c..ed7f1f2 100644 --- a/README.md +++ b/README.md @@ -290,9 +290,10 @@ You can access the SSH key storage, from configuration moving to the `SSH Keys` | `` | Move down in selected list by 8 rows | | | `` | Enter directory | | | `` | Upload / download selected file | | +| `` | Toggle hidden files | All | | `` | Copy file/directory | Copy | | `` | Make directory | Directory | -| `` | Delete file (Same as `CANC`) | Erase | +| `` | Delete file (Same as `DEL`) | Erase | | `` | Go to supplied path | Go to | | `` | Show help | Help | | `` | Show info about selected file or directory | Info | diff --git a/src/fs/mod.rs b/src/fs/mod.rs index a7dae6c..0e62298 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -212,7 +212,7 @@ impl FsEntry { /// /// Returns whether FsEntry is hidden pub fn is_hidden(&self) -> bool { - self.get_name().starts_with(".") + self.get_name().starts_with('.') } /// ### get_realfile diff --git a/src/ui/activities/filetransfer_activity/callbacks.rs b/src/ui/activities/filetransfer_activity/callbacks.rs index b7439d1..4d6e525 100644 --- a/src/ui/activities/filetransfer_activity/callbacks.rs +++ b/src/ui/activities/filetransfer_activity/callbacks.rs @@ -1,3 +1,7 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + /* * * Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com @@ -71,8 +75,8 @@ impl FileTransferActivity { match self.tab { FileExplorerTab::Local => { // Get selected entry - if self.local.files.get(self.local.index).is_some() { - let entry: FsEntry = self.local.files.get(self.local.index).unwrap().clone(); + if self.local.get_current_file().is_some() { + let entry: FsEntry = self.local.get_current_file().unwrap().clone(); if let Some(ctx) = self.context.as_mut() { match ctx.local.copy(&entry, dest_path.as_path()) { Ok(_) => { @@ -104,8 +108,8 @@ impl FileTransferActivity { } FileExplorerTab::Remote => { // Get selected entry - if self.remote.files.get(self.remote.index).is_some() { - let entry: FsEntry = self.remote.files.get(self.remote.index).unwrap().clone(); + if self.remote.get_current_file().is_some() { + let entry: FsEntry = self.remote.get_current_file().unwrap().clone(); match self.client.as_mut().copy(&entry, dest_path.as_path()) { Ok(_) => { self.log( @@ -205,7 +209,7 @@ impl FileTransferActivity { dst_path = wrkdir; } // Check if file entry exists - if let Some(entry) = self.local.files.get(self.local.index) { + if let Some(entry) = self.local.get_current_file() { let full_path: PathBuf = entry.get_abs_path(); // Rename file or directory and report status as popup match self @@ -245,7 +249,7 @@ impl FileTransferActivity { } FileExplorerTab::Remote => { // Check if file entry exists - if let Some(entry) = self.remote.files.get(self.remote.index) { + if let Some(entry) = self.remote.get_current_file() { let full_path: PathBuf = entry.get_abs_path(); // Rename file or directory and report status as popup let dst_path: PathBuf = PathBuf::from(input); @@ -289,7 +293,7 @@ impl FileTransferActivity { match self.tab { FileExplorerTab::Local => { // Check if file entry exists - if let Some(entry) = self.local.files.get(self.local.index) { + if let Some(entry) = self.local.get_current_file() { let full_path: PathBuf = entry.get_abs_path(); // Delete file or directory and report status as popup match self.context.as_mut().unwrap().local.remove(entry) { @@ -318,7 +322,7 @@ impl FileTransferActivity { } FileExplorerTab::Remote => { // Check if file entry exists - if let Some(entry) = self.remote.files.get(self.remote.index) { + if let Some(entry) = self.remote.get_current_file() { let full_path: PathBuf = entry.get_abs_path(); // Delete file match self.client.remove(entry) { @@ -355,16 +359,16 @@ impl FileTransferActivity { // Get pwd let wrkdir: PathBuf = self.remote.wrkdir.clone(); // Get file and clone (due to mutable / immutable stuff...) - if self.local.files.get(self.local.index).is_some() { - let file: FsEntry = self.local.files.get(self.local.index).unwrap().clone(); + if self.local.get_current_file().is_some() { + let file: FsEntry = self.local.get_current_file().unwrap().clone(); // Call upload; pass realfile, keep link name self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(input)); } } FileExplorerTab::Remote => { // Get file and clone (due to mutable / immutable stuff...) - if self.remote.files.get(self.remote.index).is_some() { - let file: FsEntry = self.remote.files.get(self.remote.index).unwrap().clone(); + if self.remote.get_current_file().is_some() { + let file: FsEntry = self.remote.get_current_file().unwrap().clone(); // Call upload; pass realfile, keep link name let wrkdir: PathBuf = self.local.wrkdir.clone(); self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(input)); @@ -380,15 +384,19 @@ impl FileTransferActivity { match self.tab { FileExplorerTab::Local => { // Check if file exists - for file in self.local.files.iter() { + let mut file_exists: bool = false; + for file in self.local.iter_files_all() { if input == file.get_name() { - self.log_and_alert( - LogLevel::Warn, - format!("File \"{}\" already exists", input,), - ); - return; + file_exists = true; } } + if file_exists { + self.log_and_alert( + LogLevel::Warn, + format!("File \"{}\" already exists", input,), + ); + return; + } // Create file let file_path: PathBuf = PathBuf::from(input.as_str()); if let Some(ctx) = self.context.as_mut() { @@ -409,15 +417,19 @@ impl FileTransferActivity { } FileExplorerTab::Remote => { // Check if file exists - for file in self.remote.files.iter() { + let mut file_exists: bool = false; + for file in self.remote.iter_files_all() { if input == file.get_name() { - self.log_and_alert( - LogLevel::Warn, - format!("File \"{}\" already exists", input,), - ); - return; + file_exists = true; } } + if file_exists { + self.log_and_alert( + LogLevel::Warn, + format!("File \"{}\" already exists", input,), + ); + return; + } // Get path on remote let file_path: PathBuf = PathBuf::from(input.as_str()); // Create file (on local) diff --git a/src/ui/activities/filetransfer_activity/explorer.rs b/src/ui/activities/filetransfer_activity/explorer.rs new file mode 100644 index 0000000..cc5a481 --- /dev/null +++ b/src/ui/activities/filetransfer_activity/explorer.rs @@ -0,0 +1,532 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + +/* +* +* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com +* +* This file is part of "TermSCP" +* +* TermSCP is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* TermSCP is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with TermSCP. If not, see . +* +*/ + +// Locals +use super::FsEntry; +// Ext +use std::collections::VecDeque; +use std::path::{Path, PathBuf}; + +/// ## FileExplorer +/// +/// File explorer states +pub struct FileExplorer { + pub wrkdir: PathBuf, // Current directory + index: usize, // Selected file + files: Vec, // Files in directory + dirstack: VecDeque, // Stack of visited directory (max 16) + stack_size: usize, // Directory stack size + hidden_files: bool, // Should hidden files be shown or not; hidden if false +} + +impl FileExplorer { + /// ### new + /// + /// Instantiates a new FileExplorer + pub fn new(stack_size: usize) -> FileExplorer { + FileExplorer { + wrkdir: PathBuf::from("/"), + index: 0, + files: Vec::new(), + dirstack: VecDeque::with_capacity(stack_size), + stack_size, + hidden_files: false, // Default: don't show hidden files + } + } + + /// ### pushd + /// + /// push directory to stack + pub fn pushd(&mut self, dir: &Path) { + // Check if stack would overflow the size + while self.dirstack.len() >= self.stack_size { + self.dirstack.pop_front(); // Start cleaning events from back + } + // Eventually push front the new record + self.dirstack.push_back(PathBuf::from(dir)); + } + + /// ### popd + /// + /// Pop directory from the stack and return the directory + pub fn popd(&mut self) -> Option { + self.dirstack.pop_back() + } + + /// ### set_files + /// + /// Set Explorer files + /// Index is then moved to first valid `FsEntry` for current setup + pub fn set_files(&mut self, files: Vec) { + self.files = files; + // Set index to first valid entry + self.index_at_first(); + } + + /// ### count + /// + /// Return amount of files + pub fn count(&self) -> usize { + self.files.len() + } + + /// ### iter_files + /// + /// Iterate over files + /// Filters are applied based on current options (e.g. hidden files not returned) + pub fn iter_files(&self) -> Box + '_> { + // Match options + match self.hidden_files { + false => Box::new(self.files.iter().filter(|x| !x.is_hidden())), // Show only visible files + true => self.iter_files_all(), // Show all + } + } + + /// ### iter_files_all + /// + /// Iterate all files; doesn't care about options + pub fn iter_files_all(&self) -> Box + '_> { + Box::new(self.files.iter()) + } + + /// ### get_current_file + /// + /// Get file at index + pub fn get_current_file(&self) -> Option<&FsEntry> { + self.files.get(self.index) + } + + /// ### sort_files_by_name + /// + /// Sort explorer files by their name. All names are converted to lowercase + pub fn sort_files_by_name(&mut self) { + self.files.sort_by_key(|x: &FsEntry| match x { + FsEntry::Directory(dir) => dir.name.as_str().to_lowercase(), + FsEntry::File(file) => file.name.as_str().to_lowercase(), + }); + // Reset index + self.index_at_first(); + } + + /// ### incr_index + /// + /// Increment index to the first visible FsEntry. + /// If index goes to `files.len() - 1`, the value will be seto to the minimum acceptable value + pub fn incr_index(&mut self) { + let sz: usize = self.files.len(); + // Increment or wrap + if self.index + 1 >= sz { + self.index = 0; // Wrap + } else { + self.index += 1; // Increment + } + // Validate + match self.files.get(self.index) { + Some(assoc_entry) => { + if !self.hidden_files { + // Check if file is hidden, otherwise increment + if assoc_entry.is_hidden() { + // Check if all files are hidden (NOTE: PREVENT STACK OVERFLOWS) + let hidden_files: usize = + self.files.iter().filter(|x| x.is_hidden()).count(); + // Only if there are more files, than hidden files keep incrementing + if sz > hidden_files { + self.incr_index(); + } + } + } + } + None => self.index = 0, // Reset to 0, for safety reasons + } + } + + /// ### incr_index_by + /// + /// Increment index by up to n + /// If index goes to `files.len() - 1`, the value will be seto to the minimum acceptable value + pub fn incr_index_by(&mut self, n: usize) { + for _ in 0..n { + let prev_idx: usize = self.index; + // Increment + self.incr_index(); + // If prev index is > index and break + if prev_idx > self.index { + self.index = prev_idx; + break; + } + } + } + + /// ### decr_index + /// + /// Decrement index to the first visible FsEntry. + /// If index is 0, its value will be set to the maximum acceptable value + pub fn decr_index(&mut self) { + let sz: usize = self.files.len(); + // Increment or wrap + if self.index > 0 { + self.index -= 1; // Decrement + } else { + self.index = sz - 1; // Wrap + } + // Validate index + match self.files.get(self.index) { + Some(assoc_entry) => { + if !self.hidden_files { + // Check if file is hidden, otherwise increment + if assoc_entry.is_hidden() { + // Check if all files are hidden (NOTE: PREVENT STACK OVERFLOWS) + let hidden_files: usize = + self.files.iter().filter(|x| x.is_hidden()).count(); + // Only if there are more files, than hidden files keep decrementing + if sz > hidden_files { + self.decr_index(); + } + } + } + } + None => self.index = 0, // Reset to 0, for safety reasons + } + } + + /// ### decr_index_by + /// + /// Decrement index by up to n + pub fn decr_index_by(&mut self, n: usize) { + for _ in 0..n { + let prev_idx: usize = self.index; + // Increment + self.decr_index(); + // If prev index is < index and break + if prev_idx < self.index { + self.index = prev_idx; + break; + } + } + } + + /// ### index_at_first + /// + /// Move index to first "visible" fs entry + pub fn index_at_first(&mut self) { + self.index = self.get_first_valid_index(); + } + + /// ### get_first_valid_index + /// + /// Return first valid index + fn get_first_valid_index(&self) -> usize { + match self.hidden_files { + true => 0, + false => { + // Look for first "non-hidden" entry + for (i, f) in self.files.iter().enumerate() { + if !f.is_hidden() { + return i; + } + } + // If all files are hidden, return 0 + 0 + } + } + } + + /// ### get_index + /// + /// Return index + pub fn get_index(&self) -> usize { + self.index + } + + /// ### get_relative_index + /// + /// Get relative index based on current options + pub fn get_relative_index(&self) -> usize { + match self.files.get(self.index) { + Some(abs_entry) => { + // Search abs entry in relative iterator + for (i, f) in self.iter_files().enumerate() { + if abs_entry.get_name() == f.get_name() { + // If abs entry is f, return index + return i; + } + } + // Return 0 if not found + 0 + } + None => 0, // Absolute entry doesn't exist + } + } + + /// ### set_index + /// + /// Set index to idx. + /// If index exceeds size, is set to count() - 1; or 0 + pub fn set_index(&mut self, idx: usize) { + let visible_sz: usize = self.iter_files().count(); + match idx >= visible_sz { + true => match visible_sz { + 0 => self.index_at_first(), + _ => self.index = visible_sz - 1, + }, + false => match self.get_first_valid_index() > idx { + true => self.index_at_first(), + false => self.index = idx, + }, + } + } + + /// ### toggle_hidden_files + /// + /// Enable/disable hidden files + pub fn toggle_hidden_files(&mut self) { + self.hidden_files = !self.hidden_files; + // Adjust index + if self.index < self.get_first_valid_index() { + self.index_at_first(); + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::fs::{FsDirectory, FsFile}; + + use std::time::SystemTime; + + #[test] + fn test_ui_filetransfer_activity_explorer_new() { + let explorer: FileExplorer = FileExplorer::new(16); + // Verify + assert_eq!(explorer.dirstack.len(), 0); + assert_eq!(explorer.files.len(), 0); + assert_eq!(explorer.hidden_files, false); + assert_eq!(explorer.wrkdir, PathBuf::from("/")); + assert_eq!(explorer.stack_size, 16); + assert_eq!(explorer.index, 0); + } + + #[test] + fn test_ui_filetransfer_activity_explorer_stack() { + let mut explorer: FileExplorer = FileExplorer::new(2); + // Push dir + explorer.pushd(&Path::new("/tmp")); + explorer.pushd(&Path::new("/home/omar")); + // Pop + assert_eq!(explorer.popd().unwrap(), PathBuf::from("/home/omar")); + assert_eq!(explorer.dirstack.len(), 1); + assert_eq!(explorer.popd().unwrap(), PathBuf::from("/tmp")); + assert_eq!(explorer.dirstack.len(), 0); + // Dirstack is empty now + assert!(explorer.popd().is_none()); + // Exceed limit + explorer.pushd(&Path::new("/tmp")); + explorer.pushd(&Path::new("/home/omar")); + explorer.pushd(&Path::new("/dev")); + assert_eq!(explorer.dirstack.len(), 2); + assert_eq!(*explorer.dirstack.get(1).unwrap(), PathBuf::from("/dev")); + assert_eq!( + *explorer.dirstack.get(0).unwrap(), + PathBuf::from("/home/omar") + ); + } + + #[test] + fn test_ui_filetransfer_activity_explorer_files() { + let mut explorer: FileExplorer = FileExplorer::new(16); + explorer.hidden_files = false; + // Create files + explorer.set_files(vec![ + make_fs_entry("README.md", false), + make_fs_entry("src/", true), + make_fs_entry(".git/", true), + make_fs_entry("CONTRIBUTING.md", false), + make_fs_entry("codecov.yml", false), + make_fs_entry(".gitignore", false), + ]); + assert_eq!(explorer.count(), 6); + // Verify + assert_eq!( + explorer.files.get(0).unwrap().get_name(), + String::from("README.md") + ); + // Sort files + explorer.sort_files_by_name(); + // Verify + assert_eq!( + explorer.files.get(0).unwrap().get_name(), + String::from(".git/") + ); + // Iter files (all) + assert_eq!(explorer.iter_files_all().count(), 6); + // Iter files (hidden excluded) (.git, .gitignore are hidden) + assert_eq!(explorer.iter_files().count(), 4); + // Toggle hidden + explorer.toggle_hidden_files(); + assert_eq!(explorer.iter_files().count(), 6); // All files are returned now + } + + #[test] + fn test_ui_filetransfer_activity_explorer_index() { + let mut explorer: FileExplorer = FileExplorer::new(16); + explorer.hidden_files = false; + // Create files + explorer.set_files(vec![ + make_fs_entry("README.md", false), + make_fs_entry("src/", true), + make_fs_entry(".git/", true), + make_fs_entry("CONTRIBUTING.md", false), + make_fs_entry("CODE_OF_CONDUCT.md", false), + make_fs_entry("CHANGELOG.md", false), + make_fs_entry("LICENSE", false), + make_fs_entry("Cargo.toml", false), + make_fs_entry("Cargo.lock", false), + make_fs_entry("codecov.yml", false), + make_fs_entry(".gitignore", false), + ]); + let sz: usize = explorer.count(); + // Sort by name + explorer.sort_files_by_name(); + // Get first index + assert_eq!(explorer.get_first_valid_index(), 2); + // Index should be 2 now; files hidden; this happens because `index_at_first` is called after loading files + assert_eq!(explorer.get_index(), 2); + assert_eq!(explorer.get_relative_index(), 0); // Relative index should be 0 + assert_eq!(explorer.hidden_files, false); + // Increment index + explorer.incr_index(); + // Index should now be 3 (was 0, + 2 + 1); first 2 files are hidden (.git, .gitignore) + assert_eq!(explorer.get_index(), 3); + // Relative index should be 1 instead + assert_eq!(explorer.get_relative_index(), 1); + // Increment by 2 + explorer.incr_index_by(2); + // Index should now be 5, 3 + assert_eq!(explorer.get_index(), 5); + assert_eq!(explorer.get_relative_index(), 3); + // Increment by (exceed size) + explorer.incr_index_by(20); + // Index should be at last element + assert_eq!(explorer.get_index(), sz - 1); + assert_eq!(explorer.get_relative_index(), sz - 3); + // Increment; should go to 2 + explorer.incr_index(); + assert_eq!(explorer.get_index(), 2); + assert_eq!(explorer.get_relative_index(), 0); + // Increment and then decrement + explorer.incr_index(); + explorer.decr_index(); + assert_eq!(explorer.get_index(), 2); + assert_eq!(explorer.get_relative_index(), 0); + // Decrement (and wrap) + explorer.decr_index(); + // Index should be at last element + assert_eq!(explorer.get_index(), sz - 1); + assert_eq!(explorer.get_relative_index(), sz - 3); + // Set index to 5 + explorer.set_index(5); + assert_eq!(explorer.get_index(), 5); + assert_eq!(explorer.get_relative_index(), 3); + // Decr by 2 + explorer.decr_index_by(2); + assert_eq!(explorer.get_index(), 3); + assert_eq!(explorer.get_relative_index(), 1); + // Decr by 2 + explorer.decr_index_by(2); + // Should decrement actually by 1 (since first two files are hidden) + assert_eq!(explorer.get_index(), 2); + assert_eq!(explorer.get_relative_index(), 0); + // Toggle hidden files + explorer.toggle_hidden_files(); + assert_eq!(explorer.hidden_files, true); + // Move index to 0 + explorer.set_index(0); + assert_eq!(explorer.get_index(), 0); + // Toggle hidden files + explorer.toggle_hidden_files(); + // Index should now have been moved to 2 + assert_eq!(explorer.get_index(), 2); + // Show hidden files + explorer.toggle_hidden_files(); + // Set index to 5 + explorer.set_index(5); + // Verify index + assert_eq!(explorer.get_index(), 5); + assert_eq!(explorer.get_relative_index(), 5); // Now relative matches + // Decrement by 6, goes to 0 + explorer.decr_index_by(6); + assert_eq!(explorer.get_index(), 0); + assert_eq!(explorer.get_relative_index(), 0); // Now relative matches + // Toggle; move at first + explorer.toggle_hidden_files(); + assert_eq!(explorer.hidden_files, false); + explorer.index_at_first(); + assert_eq!(explorer.get_index(), 2); + assert_eq!(explorer.get_relative_index(), 0); + // Verify set index if exceeds + let sz: usize = explorer.iter_files().count(); + explorer.set_index(sz); + assert_eq!(explorer.get_index(), sz - 1); // Should be at last element + // Empty files + explorer.files.clear(); + explorer.index_at_first(); + assert_eq!(explorer.get_index(), 0); + assert_eq!(explorer.get_relative_index(), 0); + } + + fn make_fs_entry(name: &str, is_dir: bool) -> FsEntry { + let t_now: SystemTime = SystemTime::now(); + match is_dir { + false => FsEntry::File(FsFile { + name: name.to_string(), + abs_path: PathBuf::from(name), + last_change_time: t_now, + last_access_time: t_now, + creation_time: t_now, + size: 64, + ftype: None, // File type + readonly: false, + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((6, 4, 4)), // UNIX only + }), + true => FsEntry::Directory(FsDirectory { + name: name.to_string(), + abs_path: PathBuf::from(name), + last_change_time: t_now, + last_access_time: t_now, + creation_time: t_now, + readonly: false, + symlink: None, // UNIX only + user: Some(0), // UNIX only + group: Some(0), // UNIX only + unix_pex: Some((7, 5, 5)), // UNIX only + }), + } + } +} diff --git a/src/ui/activities/filetransfer_activity/input.rs b/src/ui/activities/filetransfer_activity/input.rs index 8b7c224..3e886c3 100644 --- a/src/ui/activities/filetransfer_activity/input.rs +++ b/src/ui/activities/filetransfer_activity/input.rs @@ -1,3 +1,7 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + /* * * Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com @@ -102,41 +106,28 @@ impl FileTransferActivity { KeyCode::Tab => self.switch_input_field(), // switch tab KeyCode::Right => self.tab = FileExplorerTab::Remote, // switch to right tab KeyCode::Up => { - // Move index up; or move to the last element if 0 - self.local.index = match self.local.index { - 0 => self.local.files.len() - 1, - _ => self.local.index - 1, - }; + // Decrement index + self.local.decr_index(); } KeyCode::Down => { - // Move index down - if self.local.index + 1 < self.local.files.len() { - self.local.index += 1; - } else { - self.local.index = 0; // Move at the beginning of the list - } + // Increment index + self.local.incr_index(); } KeyCode::PageUp => { - // Move index up (fast) - if self.local.index > 8 { - self.local.index -= 8; // Decrease by `8` if possible - } else { - self.local.index = 0; // Set to 0 otherwise - } + // Decrement index by 8 + self.local.decr_index_by(8); } KeyCode::PageDown => { - // Move index down (fast) - if self.local.index + 8 >= self.local.files.len() { - // If overflows, set to size - self.local.index = self.local.files.len() - 1; - } else { - self.local.index += 8; // Increase by `8` - } + // Increment index by 8 + self.local.incr_index_by(8); } KeyCode::Enter => { // Match selected file - let local_files: Vec = self.local.files.clone(); - if let Some(entry) = local_files.get(self.local.index) { + let mut entry: Option = None; + if let Some(e) = self.local.get_current_file() { + entry = Some(e.clone()); + } + if let Some(entry) = entry { // If directory, enter directory, otherwise check if symlink match entry { FsEntry::Directory(dir) => { @@ -162,7 +153,7 @@ impl FileTransferActivity { } KeyCode::Delete => { // Get file at index - if let Some(entry) = self.local.files.get(self.local.index) { + if let Some(entry) = self.local.get_current_file() { // Get file name let file_name: String = match entry { FsEntry::Directory(dir) => dir.name.clone(), @@ -177,6 +168,10 @@ impl FileTransferActivity { } } KeyCode::Char(ch) => match ch { + 'a' | 'A' => { + // Toggle hidden files + self.local.toggle_hidden_files(); + } 'c' | 'C' => { // Copy self.input_mode = InputMode::Popup(PopupType::Input( @@ -193,7 +188,7 @@ impl FileTransferActivity { } 'e' | 'E' => { // Get file at index - if let Some(entry) = self.local.files.get(self.local.index) { + if let Some(entry) = self.local.get_current_file() { // Get file name let file_name: String = match entry { FsEntry::Directory(dir) => dir.name.clone(), @@ -237,10 +232,9 @@ impl FileTransferActivity { } 'o' | 'O' => { // Edit local file - if self.local.files.get(self.local.index).is_some() { + if self.local.get_current_file().is_some() { // Clone entry due to mutable stuff... - let fsentry: FsEntry = - self.local.files.get(self.local.index).unwrap().clone(); + let fsentry: FsEntry = self.local.get_current_file().unwrap().clone(); // Check if file if fsentry.is_file() { self.log( @@ -294,9 +288,8 @@ impl FileTransferActivity { // Get pwd let wrkdir: PathBuf = self.remote.wrkdir.clone(); // Get file and clone (due to mutable / immutable stuff...) - if self.local.files.get(self.local.index).is_some() { - let file: FsEntry = - self.local.files.get(self.local.index).unwrap().clone(); + if self.local.get_current_file().is_some() { + let file: FsEntry = self.local.get_current_file().unwrap().clone(); let name: String = file.get_name().to_string(); // Call upload; pass realfile, keep link name self.filetransfer_send( @@ -328,41 +321,28 @@ impl FileTransferActivity { KeyCode::Tab => self.switch_input_field(), // switch tab KeyCode::Left => self.tab = FileExplorerTab::Local, // switch to local tab KeyCode::Up => { - // Move index up; or move to the last element if 0 - self.remote.index = match self.remote.index { - 0 => self.remote.files.len() - 1, - _ => self.remote.index - 1, - }; + // Decrement index + self.remote.decr_index(); } KeyCode::Down => { - // Move index down - if self.remote.index + 1 < self.remote.files.len() { - self.remote.index += 1; - } else { - self.remote.index = 0; // Move at the beginning of the list - } + // Increment index + self.remote.incr_index(); } KeyCode::PageUp => { - // Move index up (fast) - if self.remote.index > 8 { - self.remote.index -= 8; // Decrease by `8` if possible - } else { - self.remote.index = 0; // Set to 0 otherwise - } + // Decrement index by 8 + self.remote.decr_index_by(8); } KeyCode::PageDown => { - // Move index down (fast) - if self.remote.index + 8 >= self.remote.files.len() { - // If overflows, set to size - self.remote.index = self.remote.files.len() - 1; - } else { - self.remote.index += 8; // Increase by `8` - } + // Increment index by 8 + self.remote.incr_index_by(8); } KeyCode::Enter => { // Match selected file - let files: Vec = self.remote.files.clone(); - if let Some(entry) = files.get(self.remote.index) { + let mut entry: Option = None; + if let Some(e) = self.remote.get_current_file() { + entry = Some(e.clone()); + } + if let Some(entry) = entry { // If directory, enter directory; if file, check if is symlink match entry { FsEntry::Directory(dir) => { @@ -388,7 +368,7 @@ impl FileTransferActivity { } KeyCode::Delete => { // Get file at index - if let Some(entry) = self.remote.files.get(self.remote.index) { + if let Some(entry) = self.remote.get_current_file() { // Get file name let file_name: String = match entry { FsEntry::Directory(dir) => dir.name.clone(), @@ -403,6 +383,10 @@ impl FileTransferActivity { } } KeyCode::Char(ch) => match ch { + 'a' | 'A' => { + // Toggle hidden files + self.remote.toggle_hidden_files(); + } 'c' | 'C' => { // Copy self.input_mode = InputMode::Popup(PopupType::Input( @@ -419,7 +403,7 @@ impl FileTransferActivity { } 'e' | 'E' => { // Get file at index - if let Some(entry) = self.remote.files.get(self.remote.index) { + if let Some(entry) = self.remote.get_current_file() { // Get file name let file_name: String = match entry { FsEntry::Directory(dir) => dir.name.clone(), @@ -462,10 +446,9 @@ impl FileTransferActivity { } 'o' | 'O' => { // Edit remote file - if self.remote.files.get(self.remote.index).is_some() { + if self.remote.get_current_file().is_some() { // Clone entry due to mutable stuff... - let fsentry: FsEntry = - self.remote.files.get(self.remote.index).unwrap().clone(); + let fsentry: FsEntry = self.remote.get_current_file().unwrap().clone(); // Check if file if let FsEntry::File(file) = fsentry { self.log( @@ -516,9 +499,8 @@ impl FileTransferActivity { } ' ' => { // Get file and clone (due to mutable / immutable stuff...) - if self.remote.files.get(self.remote.index).is_some() { - let file: FsEntry = - self.remote.files.get(self.remote.index).unwrap().clone(); + if self.remote.get_current_file().is_some() { + let file: FsEntry = self.remote.get_current_file().unwrap().clone(); let name: String = file.get_name().to_string(); // Call upload; pass realfile, keep link name let wrkdir: PathBuf = self.local.wrkdir.clone(); diff --git a/src/ui/activities/filetransfer_activity/layout.rs b/src/ui/activities/filetransfer_activity/layout.rs index e611ef1..95afa4d 100644 --- a/src/ui/activities/filetransfer_activity/layout.rs +++ b/src/ui/activities/filetransfer_activity/layout.rs @@ -1,3 +1,7 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + /* * * Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com @@ -70,10 +74,10 @@ impl FileTransferActivity { .split(chunks[0]); // Set localhost state let mut localhost_state: ListState = ListState::default(); - localhost_state.select(Some(self.local.index)); + localhost_state.select(Some(self.local.get_relative_index())); // Set remote state let mut remote_state: ListState = ListState::default(); - remote_state.select(Some(self.remote.index)); + remote_state.select(Some(self.remote.get_relative_index())); // Draw tabs f.render_stateful_widget( self.draw_local_explorer(tabs_chunks[0].width), @@ -158,8 +162,7 @@ impl FileTransferActivity { }; let files: Vec = self .local - .files - .iter() + .iter_files() .map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry)))) .collect(); // Get colors to use; highlight element inverting fg/bg only when tab is active @@ -199,8 +202,7 @@ impl FileTransferActivity { pub(super) fn draw_remote_explorer(&self, width: u16) -> List { let files: Vec = self .remote - .files - .iter() + .iter_files() .map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry)))) .collect(); // Get colors to use; highlight element inverting fg/bg only when tab is active @@ -468,12 +470,12 @@ impl FileTransferActivity { let fsentry: Option<&FsEntry> = match self.tab { FileExplorerTab::Local => { // Get selected file - match self.local.files.get(self.local.index) { + match self.local.get_current_file() { Some(entry) => Some(entry), None => None, } } - FileExplorerTab::Remote => match self.remote.files.get(self.remote.index) { + FileExplorerTab::Remote => match self.remote.get_current_file() { Some(entry) => Some(entry), None => None, }, @@ -715,6 +717,16 @@ impl FileTransferActivity { Span::raw(" "), Span::raw("Delete file"), ])), + ListItem::new(Spans::from(vec![ + Span::styled( + "", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::raw(" "), + Span::raw("Toggle hidden files"), + ])), ListItem::new(Spans::from(vec![ Span::styled( "", diff --git a/src/ui/activities/filetransfer_activity/mod.rs b/src/ui/activities/filetransfer_activity/mod.rs index 9431699..b8a9a77 100644 --- a/src/ui/activities/filetransfer_activity/mod.rs +++ b/src/ui/activities/filetransfer_activity/mod.rs @@ -25,6 +25,7 @@ // This module is split into files, cause it's just too big mod callbacks; +mod explorer; mod input; mod layout; mod misc; @@ -45,13 +46,14 @@ use crate::filetransfer::sftp_transfer::SftpFileTransfer; use crate::filetransfer::{FileTransfer, FileTransferProtocol}; use crate::fs::FsEntry; use crate::system::config_client::ConfigClient; +use explorer::FileExplorer; // Includes use chrono::{DateTime, Local}; use crossterm::event::Event as InputEvent; use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; use std::collections::VecDeque; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::time::Instant; use tui::style::Color; @@ -113,59 +115,6 @@ enum InputMode { Popup(PopupType), } -/// ## FileExplorer -/// -/// File explorer states -struct FileExplorer { - pub wrkdir: PathBuf, // Current directory - pub index: usize, // Selected file - pub files: Vec, // Files in directory - dirstack: VecDeque, // Stack of visited directory (max 16) -} - -impl FileExplorer { - /// ### new - /// - /// Instantiates a new FileExplorer - pub fn new() -> FileExplorer { - FileExplorer { - wrkdir: PathBuf::from("/"), - index: 0, - files: Vec::new(), - dirstack: VecDeque::with_capacity(16), - } - } - - /// ### pushd - /// - /// push directory to stack - pub fn pushd(&mut self, dir: &Path) { - // Check if stack overflows the size - if self.dirstack.len() + 1 > 16 { - self.dirstack.pop_back(); // Start cleaning events from back - } - // Eventually push front the new record - self.dirstack.push_front(PathBuf::from(dir)); - } - - /// ### popd - /// - /// Pop directory from the stack and return the directory - pub fn popd(&mut self) -> Option { - self.dirstack.pop_front() - } - - /// ### sort_files_by_name - /// - /// Sort explorer files by their name - pub fn sort_files_by_name(&mut self) { - self.files.sort_by_key(|x: &FsEntry| match x { - FsEntry::Directory(dir) => dir.name.as_str().to_lowercase(), - FsEntry::File(file) => file.name.as_str().to_lowercase(), - }); - } -} - /// ## FileExplorerTab /// /// File explorer tab @@ -312,18 +261,18 @@ impl FileTransferActivity { quit: false, context: None, client: match protocol { - FileTransferProtocol::Sftp => { - Box::new(SftpFileTransfer::new(Self::make_ssh_storage(config_client.as_ref()))) - } + FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new( + Self::make_ssh_storage(config_client.as_ref()), + )), FileTransferProtocol::Ftp(ftps) => Box::new(FtpFileTransfer::new(ftps)), - FileTransferProtocol::Scp => { - Box::new(ScpFileTransfer::new(Self::make_ssh_storage(config_client.as_ref()))) - } + FileTransferProtocol::Scp => Box::new(ScpFileTransfer::new( + Self::make_ssh_storage(config_client.as_ref()), + )), }, config_cli: config_client, params, - local: FileExplorer::new(), - remote: FileExplorer::new(), + local: FileExplorer::new(16), + remote: FileExplorer::new(16), tab: FileExplorerTab::Local, log_index: 0, log_records: VecDeque::with_capacity(256), // 256 events is enough I guess @@ -335,7 +284,6 @@ impl FileTransferActivity { transfer: TransferStates::default(), } } - } /** diff --git a/src/ui/activities/filetransfer_activity/session.rs b/src/ui/activities/filetransfer_activity/session.rs index e090f06..3a2c32d 100644 --- a/src/ui/activities/filetransfer_activity/session.rs +++ b/src/ui/activities/filetransfer_activity/session.rs @@ -1,3 +1,7 @@ +//! ## FileTransferActivity +//! +//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall + /* * * Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com @@ -599,17 +603,17 @@ impl FileTransferActivity { pub(super) fn local_scan(&mut self, path: &Path) { match self.context.as_ref().unwrap().local.scan_dir(path) { Ok(files) => { - self.local.files = files; - // Set index; keep if possible, otherwise set to last item - self.local.index = match self.local.files.get(self.local.index) { - Some(_) => self.local.index, - None => match self.local.files.len() { - 0 => 0, - _ => self.local.files.len() - 1, - }, - }; + self.local.set_files(files); // Sort files self.local.sort_files_by_name(); + // Set index; keep if possible, otherwise set to last item + self.local.set_index(match self.local.get_current_file() { + Some(_) => self.local.get_index(), + None => match self.local.count() { + 0 => 0, + _ => self.local.count() - 1, + }, + }); } Err(err) => { self.log_and_alert( @@ -626,17 +630,17 @@ impl FileTransferActivity { pub(super) fn remote_scan(&mut self, path: &Path) { match self.client.list_dir(path) { Ok(files) => { - self.remote.files = files; - // Set index; keep if possible, otherwise set to last item - self.remote.index = match self.remote.files.get(self.remote.index) { - Some(_) => self.remote.index, - None => match self.remote.files.len() { - 0 => 0, - _ => self.remote.files.len() - 1, - }, - }; + self.remote.set_files(files); // Sort files self.remote.sort_files_by_name(); + // Set index; keep if possible, otherwise set to last item + self.remote.set_index(match self.remote.get_current_file() { + Some(_) => self.remote.get_index(), + None => match self.remote.count() { + 0 => 0, + _ => self.remote.count() - 1, + }, + }); } Err(err) => { self.log_and_alert( @@ -663,7 +667,7 @@ impl FileTransferActivity { // Reload files self.local_scan(path); // Reset index - self.local.index = 0; + self.local.set_index(0); // Set wrkdir self.local.wrkdir = PathBuf::from(path); // Push prev_dir to stack @@ -694,7 +698,7 @@ impl FileTransferActivity { // Update files self.remote_scan(path); // Reset index - self.remote.index = 0; + self.remote.set_index(0); // Set wrkdir self.remote.wrkdir = PathBuf::from(path); // Push prev_dir to stack