diff --git a/CHANGELOG.md b/CHANGELOG.md index cf6ae61..c54bbc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ FIXME: Released on - `A`: Toggle hidden files - `N`: New file - Dependencies: + - added `bitflags 1.2.1` - removed `data-encoding` - updated `rand` to `0.8.0` - removed `ring` diff --git a/Cargo.lock b/Cargo.lock index 87dfa7d..05aeb57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1049,6 +1049,7 @@ dependencies = [ name = "termscp" version = "0.3.0" dependencies = [ + "bitflags", "bytesize", "chrono", "content_inspector", diff --git a/Cargo.toml b/Cargo.toml index 783df5f..b0d41c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bitflags = "1.2.1" bytesize = "1.0.1" chrono = "0.4.19" content_inspector = "0.2.4" diff --git a/src/fs/explorer/builder.rs b/src/fs/explorer/builder.rs new file mode 100644 index 0000000..28ff326 --- /dev/null +++ b/src/fs/explorer/builder.rs @@ -0,0 +1,139 @@ +//! ## Builder +//! +//! `builder` is the module which provides a builder for FileExplorer + +/* +* +* 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::{ExplorerOpts, FileExplorer}; +// Ext +use std::collections::VecDeque; + +/// ## FileExplorerBuilder +/// +/// Struct used to create a `FileExplorer` +pub struct FileExplorerBuilder { + explorer: Option, +} + +impl FileExplorerBuilder { + /// ### new + /// + /// Build a new `FileExplorerBuilder` + pub fn new() -> Self { + FileExplorerBuilder { + explorer: Some(FileExplorer::default()), + } + } + + /// ### build + /// + /// Take FileExplorer out of builder + pub fn build(&mut self) -> FileExplorer { + self.explorer.take().unwrap() + } + + /// ### with_hidden_files + /// + /// Enable HIDDEN_FILES option + pub fn with_hidden_files(&mut self) -> &mut FileExplorerBuilder { + if let Some(e) = self.explorer.as_mut() { + e.opts.insert(ExplorerOpts::SHOW_HIDDEN_FILES); + } + self + } + + /// ### sort_by_name + /// + /// Enable SORT_BY_NAME option + pub fn sort_by_name(&mut self) -> &mut FileExplorerBuilder { + if let Some(e) = self.explorer.as_mut() { + e.opts.insert(ExplorerOpts::SORT_BY_NAME); + } + self + } + + /// ### sort_by_mtime + /// + /// Enable SORT_BY_MTIME option + pub fn sort_by_mtime(&mut self) -> &mut FileExplorerBuilder { + if let Some(e) = self.explorer.as_mut() { + e.opts.insert(ExplorerOpts::SORT_BY_MTIME); + } + self + } + + /// ### with_dirs_first + /// + /// Enable DIRS_FIRST option + pub fn with_dirs_first(&mut self) -> &mut FileExplorerBuilder { + if let Some(e) = self.explorer.as_mut() { + e.opts.insert(ExplorerOpts::DIRS_FIRST); + } + self + } + + /// ### with_stack_size + /// + /// Set stack size for FileExplorer + pub fn with_stack_size(&mut self, sz: usize) -> &mut FileExplorerBuilder { + if let Some(e) = self.explorer.as_mut() { + e.stack_size = sz; + e.dirstack = VecDeque::with_capacity(sz); + } + self + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn test_fs_explorer_builder_new_default() { + let explorer: FileExplorer = FileExplorerBuilder::new().build(); + // Verify + assert!(!explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)); + assert!(!explorer.opts.intersects(ExplorerOpts::SORT_BY_MTIME)); + assert!(!explorer.opts.intersects(ExplorerOpts::SORT_BY_NAME)); + assert!(!explorer.opts.intersects(ExplorerOpts::DIRS_FIRST)); + assert_eq!(explorer.stack_size, 16); + } + + #[test] + fn test_fs_explorer_builder_new_all() { + let explorer: FileExplorer = FileExplorerBuilder::new() + .sort_by_mtime() + .sort_by_name() + .with_dirs_first() + .with_hidden_files() + .with_stack_size(24) + .build(); + // Verify + assert!(explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)); + assert!(explorer.opts.intersects(ExplorerOpts::SORT_BY_MTIME)); + assert!(explorer.opts.intersects(ExplorerOpts::SORT_BY_NAME)); + assert!(explorer.opts.intersects(ExplorerOpts::DIRS_FIRST)); + assert_eq!(explorer.stack_size, 24); + } +} diff --git a/src/ui/activities/filetransfer_activity/explorer.rs b/src/fs/explorer/mod.rs similarity index 68% rename from src/ui/activities/filetransfer_activity/explorer.rs rename to src/fs/explorer/mod.rs index cc5a481..07e40fe 100644 --- a/src/ui/activities/filetransfer_activity/explorer.rs +++ b/src/fs/explorer/mod.rs @@ -1,6 +1,6 @@ -//! ## FileTransferActivity +//! ## Explorer //! -//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall +//! `explorer` is the module which provides an Helper in handling Directory status through /* * @@ -23,39 +23,51 @@ * */ +// Mods +pub(crate) mod builder; +// Deps +extern crate bitflags; // Locals use super::FsEntry; // Ext use std::collections::VecDeque; use std::path::{Path, PathBuf}; +bitflags! { + pub(crate) struct ExplorerOpts: u32 { + const SHOW_HIDDEN_FILES = 0b00000001; + const SORT_BY_NAME = 0b00000010; + const SORT_BY_MTIME = 0b00000100; + const DIRS_FIRST = 0b00001000; + } +} + /// ## 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 + pub wrkdir: PathBuf, // Current directory + index: usize, // Selected file + files: Vec, // Files in directory + pub(crate) dirstack: VecDeque, // Stack of visited directory (max 16) + pub(crate) stack_size: usize, // Directory stack size + pub(crate) opts: ExplorerOpts, // Explorer options } -impl FileExplorer { - /// ### new - /// - /// Instantiates a new FileExplorer - pub fn new(stack_size: usize) -> FileExplorer { +impl Default for FileExplorer { + fn default() -> Self { 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 + dirstack: VecDeque::with_capacity(16), + stack_size: 16, + opts: ExplorerOpts::empty(), } } +} +impl FileExplorer { /// ### pushd /// /// push directory to stack @@ -78,10 +90,13 @@ impl FileExplorer { /// ### set_files /// /// Set Explorer files - /// Index is then moved to first valid `FsEntry` for current setup + /// This method will also sort entries based on current options + /// Once all sorting have been performed, index is moved to first valid entry. pub fn set_files(&mut self, files: Vec) { self.files = files; - // Set index to first valid entry + // Sort + self.sort(); + // Reset index self.index_at_first(); } @@ -97,11 +112,17 @@ impl FileExplorer { /// 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 - } + // Filter + let opts: ExplorerOpts = self.opts; + Box::new(self.files.iter().filter(move |x| { + // If true, element IS NOT filtered + let mut pass: bool = true; + // If hidden files SHOULDN'T be shown, AND pass with not hidden + if !opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) { + pass &= !x.is_hidden(); + } + pass + })) } /// ### iter_files_all @@ -118,16 +139,46 @@ impl FileExplorer { self.files.get(self.index) } + // Sorting + + /// ### sort + /// + /// Sort files based on Explorer options. + fn sort(&mut self) { + // Sort by name + if self.opts.intersects(ExplorerOpts::SORT_BY_NAME) { + self.sort_files_by_name(); + } else if self.opts.intersects(ExplorerOpts::SORT_BY_MTIME) { + // Sort by mtime NOTE: else if cause exclusive + self.sort_files_by_name(); + } + // Directories first (MUST COME AFTER NAME) + if self.opts.intersects(ExplorerOpts::DIRS_FIRST) { + self.sort_files_directories_first(); + } + } + /// ### 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(); + fn sort_files_by_name(&mut self) { + self.files + .sort_by_key(|x: &FsEntry| x.get_name().to_lowercase()); + } + + /// ### sort_files_by_mtime + /// + /// Sort files by mtime; the newest comes first + fn sort_files_by_mtime(&mut self) { + self.files + .sort_by_key(|x: &FsEntry| x.get_last_change_time()); + } + + /// ### sort_files_directories_first + /// + /// Sort files; directories come first + fn sort_files_directories_first(&mut self) { + self.files.sort_by_key(|x: &FsEntry| x.is_file()); } /// ### incr_index @@ -145,7 +196,7 @@ impl FileExplorer { // Validate match self.files.get(self.index) { Some(assoc_entry) => { - if !self.hidden_files { + if !self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) { // Check if file is hidden, otherwise increment if assoc_entry.is_hidden() { // Check if all files are hidden (NOTE: PREVENT STACK OVERFLOWS) @@ -194,7 +245,7 @@ impl FileExplorer { // Validate index match self.files.get(self.index) { Some(assoc_entry) => { - if !self.hidden_files { + if !self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) { // Check if file is hidden, otherwise increment if assoc_entry.is_hidden() { // Check if all files are hidden (NOTE: PREVENT STACK OVERFLOWS) @@ -238,7 +289,7 @@ impl FileExplorer { /// /// Return first valid index fn get_first_valid_index(&self) -> usize { - match self.hidden_files { + match self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) { true => 0, false => { // Look for first "non-hidden" entry @@ -302,7 +353,7 @@ impl FileExplorer { /// /// Enable/disable hidden files pub fn toggle_hidden_files(&mut self) { - self.hidden_files = !self.hidden_files; + self.opts.toggle(ExplorerOpts::SHOW_HIDDEN_FILES); // Adjust index if self.index < self.get_first_valid_index() { self.index_at_first(); @@ -316,23 +367,26 @@ mod tests { use super::*; use crate::fs::{FsDirectory, FsFile}; - use std::time::SystemTime; + use std::thread::sleep; + use std::time::{Duration, SystemTime}; #[test] - fn test_ui_filetransfer_activity_explorer_new() { - let explorer: FileExplorer = FileExplorer::new(16); + fn test_fs_explorer_new() { + let explorer: FileExplorer = FileExplorer::default(); // Verify assert_eq!(explorer.dirstack.len(), 0); assert_eq!(explorer.files.len(), 0); - assert_eq!(explorer.hidden_files, false); + assert_eq!(explorer.opts, ExplorerOpts::empty()); 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); + fn test_fs_explorer_stack() { + let mut explorer: FileExplorer = FileExplorer::default(); + explorer.stack_size = 2; + explorer.dirstack = VecDeque::with_capacity(2); // Push dir explorer.pushd(&Path::new("/tmp")); explorer.pushd(&Path::new("/home/omar")); @@ -356,10 +410,10 @@ mod tests { } #[test] - fn test_ui_filetransfer_activity_explorer_files() { - let mut explorer: FileExplorer = FileExplorer::new(16); - explorer.hidden_files = false; - // Create files + fn test_fs_explorer_files() { + let mut explorer: FileExplorer = FileExplorer::default(); + explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES); // Don't show hidden files + // Create files explorer.set_files(vec![ make_fs_entry("README.md", false), make_fs_entry("src/", true), @@ -391,10 +445,11 @@ mod tests { } #[test] - fn test_ui_filetransfer_activity_explorer_index() { - let mut explorer: FileExplorer = FileExplorer::new(16); - explorer.hidden_files = false; - // Create files + fn test_fs_explorer_index() { + let mut explorer: FileExplorer = FileExplorer::default(); + explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES); + explorer.opts.insert(ExplorerOpts::SORT_BY_NAME); + // Create files (files are then sorted by name) explorer.set_files(vec![ make_fs_entry("README.md", false), make_fs_entry("src/", true), @@ -409,14 +464,15 @@ mod tests { 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); + assert_eq!( + explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES), + false + ); // Increment index explorer.incr_index(); // Index should now be 3 (was 0, + 2 + 1); first 2 files are hidden (.git, .gitignore) @@ -462,7 +518,10 @@ mod tests { assert_eq!(explorer.get_relative_index(), 0); // Toggle hidden files explorer.toggle_hidden_files(); - assert_eq!(explorer.hidden_files, true); + assert_eq!( + explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES), + true + ); // Move index to 0 explorer.set_index(0); assert_eq!(explorer.get_index(), 0); @@ -483,7 +542,10 @@ mod tests { assert_eq!(explorer.get_relative_index(), 0); // Now relative matches // Toggle; move at first explorer.toggle_hidden_files(); - assert_eq!(explorer.hidden_files, false); + assert_eq!( + explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES), + false + ); explorer.index_at_first(); assert_eq!(explorer.get_index(), 2); assert_eq!(explorer.get_relative_index(), 0); @@ -498,6 +560,74 @@ mod tests { assert_eq!(explorer.get_relative_index(), 0); } + #[test] + fn test_fs_explorer_sort_by_name() { + let mut explorer: FileExplorer = FileExplorer::default(); + explorer.opts.insert(ExplorerOpts::SORT_BY_NAME); + // Create files (files are then sorted by name) + explorer.set_files(vec![ + make_fs_entry("README.md", false), + make_fs_entry("src/", 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), + ]); + // First entry should be "Cargo.lock" + assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock"); + // Last should be "src/" + assert_eq!(explorer.files.get(8).unwrap().get_name(), "src/"); + } + + #[test] + fn test_fs_explorer_sort_by_mtime() { + let mut explorer: FileExplorer = FileExplorer::default(); + explorer.opts.insert(ExplorerOpts::SORT_BY_MTIME); + let entry1: FsEntry = make_fs_entry("README.md", false); + // Wait 1 sec + sleep(Duration::from_secs(1)); + let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false); + // Create files (files are then sorted by name) + explorer.set_files(vec![entry1, entry2]); + // First entry should be "CODE_OF_CONDUCT.md" + assert_eq!( + explorer.files.get(0).unwrap().get_name(), + "CODE_OF_CONDUCT.md" + ); + // Last should be "src/" + assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); + } + + #[test] + fn test_fs_explorer_sort_by_name_and_dir() { + let mut explorer: FileExplorer = FileExplorer::default(); + explorer.opts.insert(ExplorerOpts::SORT_BY_NAME); + explorer.opts.insert(ExplorerOpts::DIRS_FIRST); + // Create files (files are then sorted by name) + explorer.set_files(vec![ + make_fs_entry("README.md", false), + make_fs_entry("src/", true), + make_fs_entry("docs/", 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), + ]); + // First entry should be "docs" + assert_eq!(explorer.files.get(0).unwrap().get_name(), "docs/"); + assert_eq!(explorer.files.get(1).unwrap().get_name(), "src/"); + // 3rd is file first for alphabetical order + assert_eq!(explorer.files.get(2).unwrap().get_name(), "Cargo.lock"); + // Last should be "README.md" (last file for alphabetical ordening) + assert_eq!(explorer.files.get(9).unwrap().get_name(), "README.md"); + } + fn make_fs_entry(name: &str, is_dir: bool) -> FsEntry { let t_now: SystemTime = SystemTime::now(); match is_dir { diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 0e62298..b66ae1b 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -23,12 +23,16 @@ * */ +// Mod +pub mod explorer; + +// Deps extern crate bytesize; #[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))] extern crate users; - +// Locals use crate::utils::fmt::{fmt_pex, fmt_time}; - +// Ext use bytesize::ByteSize; use std::path::PathBuf; use std::time::SystemTime; diff --git a/src/lib.rs b/src/lib.rs index 087e803..a779075 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,8 @@ * */ +#[macro_use] +extern crate bitflags; #[macro_use] extern crate lazy_static; #[macro_use] diff --git a/src/main.rs b/src/main.rs index 93eafb5..048c233 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,8 @@ const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); // Crates extern crate getopts; #[macro_use] +extern crate bitflags; +#[macro_use] extern crate lazy_static; #[macro_use] extern crate magic_crypt; diff --git a/src/ui/activities/filetransfer_activity/misc.rs b/src/ui/activities/filetransfer_activity/misc.rs index 9931519..42c0562 100644 --- a/src/ui/activities/filetransfer_activity/misc.rs +++ b/src/ui/activities/filetransfer_activity/misc.rs @@ -24,6 +24,7 @@ use super::{ Color, ConfigClient, FileTransferActivity, InputField, InputMode, LogLevel, LogRecord, PopupType, }; +use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer}; use crate::system::environment; use crate::system::sshkey_storage::SshKeyStorage; // Ext @@ -125,6 +126,17 @@ impl FileTransferActivity { } } + /// ### build_explorer + /// + /// Build explorer reading configuration from `ConfigClient` + pub(super) fn build_explorer(cli: Option<&ConfigClient>) -> FileExplorer { + FileExplorerBuilder::new() + .sort_by_name() + .with_dirs_first() + .with_stack_size(16) + .build() + } + /// ### setup_text_editor /// /// Set text editor to use diff --git a/src/ui/activities/filetransfer_activity/mod.rs b/src/ui/activities/filetransfer_activity/mod.rs index b8a9a77..e727008 100644 --- a/src/ui/activities/filetransfer_activity/mod.rs +++ b/src/ui/activities/filetransfer_activity/mod.rs @@ -25,7 +25,6 @@ // This module is split into files, cause it's just too big mod callbacks; -mod explorer; mod input; mod layout; mod misc; @@ -44,9 +43,9 @@ use crate::filetransfer::ftp_transfer::FtpFileTransfer; use crate::filetransfer::scp_transfer::ScpFileTransfer; use crate::filetransfer::sftp_transfer::SftpFileTransfer; use crate::filetransfer::{FileTransfer, FileTransferProtocol}; +use crate::fs::explorer::FileExplorer; use crate::fs::FsEntry; use crate::system::config_client::ConfigClient; -use explorer::FileExplorer; // Includes use chrono::{DateTime, Local}; @@ -269,10 +268,10 @@ impl FileTransferActivity { Self::make_ssh_storage(config_client.as_ref()), )), }, - config_cli: config_client, params, - local: FileExplorer::new(16), - remote: FileExplorer::new(16), + local: Self::build_explorer(config_client.as_ref()), + remote: Self::build_explorer(config_client.as_ref()), + config_cli: config_client, tab: FileExplorerTab::Local, log_index: 0, log_records: VecDeque::with_capacity(256), // 256 events is enough I guess diff --git a/src/ui/activities/filetransfer_activity/session.rs b/src/ui/activities/filetransfer_activity/session.rs index 3a2c32d..381538e 100644 --- a/src/ui/activities/filetransfer_activity/session.rs +++ b/src/ui/activities/filetransfer_activity/session.rs @@ -603,9 +603,8 @@ impl FileTransferActivity { pub(super) fn local_scan(&mut self, path: &Path) { match self.context.as_ref().unwrap().local.scan_dir(path) { Ok(files) => { + // Set files and sort (sorting is implicit) 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(), @@ -630,9 +629,8 @@ impl FileTransferActivity { pub(super) fn remote_scan(&mut self, path: &Path) { match self.client.list_dir(path) { Ok(files) => { + // Set files and sort (sorting is implicit) 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(),