diff --git a/src/fs/explorer/builder.rs b/src/fs/explorer/builder.rs index 28ff326..daf9c5b 100644 --- a/src/fs/explorer/builder.rs +++ b/src/fs/explorer/builder.rs @@ -24,7 +24,7 @@ */ // Locals -use super::{ExplorerOpts, FileExplorer}; +use super::{ExplorerOpts, FileExplorer, FileSorting, GroupDirs}; // Ext use std::collections::VecDeque; @@ -62,22 +62,12 @@ impl FileExplorerBuilder { self } - /// ### sort_by_name + /// ### with_file_sorting /// - /// Enable SORT_BY_NAME option - pub fn sort_by_name(&mut self) -> &mut FileExplorerBuilder { + /// Set sorting method + pub fn with_file_sorting(&mut self, sorting: FileSorting) -> &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); + e.sort_by(sorting); } self } @@ -85,9 +75,9 @@ impl FileExplorerBuilder { /// ### with_dirs_first /// /// Enable DIRS_FIRST option - pub fn with_dirs_first(&mut self) -> &mut FileExplorerBuilder { + pub fn with_group_dirs(&mut self, group_dirs: Option) -> &mut FileExplorerBuilder { if let Some(e) = self.explorer.as_mut() { - e.opts.insert(ExplorerOpts::DIRS_FIRST); + e.group_dirs_by(group_dirs); } self } @@ -114,26 +104,23 @@ mod tests { 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.file_sorting, FileSorting::ByName); // Default + assert_eq!(explorer.group_dirs, None); 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_file_sorting(FileSorting::ByModifyTime) + .with_group_dirs(Some(GroupDirs::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.file_sorting, FileSorting::ByModifyTime); // Default + assert_eq!(explorer.group_dirs, Some(GroupDirs::First)); assert_eq!(explorer.stack_size, 24); } } diff --git a/src/fs/explorer/mod.rs b/src/fs/explorer/mod.rs index 07e40fe..43e0d7e 100644 --- a/src/fs/explorer/mod.rs +++ b/src/fs/explorer/mod.rs @@ -34,35 +34,58 @@ use std::collections::VecDeque; use std::path::{Path, PathBuf}; bitflags! { + /// ## ExplorerOpts + /// + /// ExplorerOpts are bit options which provides different behaviours to `FileExplorer` pub(crate) struct ExplorerOpts: u32 { - const SHOW_HIDDEN_FILES = 0b00000001; - const SORT_BY_NAME = 0b00000010; - const SORT_BY_MTIME = 0b00000100; - const DIRS_FIRST = 0b00001000; + const SHOW_HIDDEN_FILES = 0b00000001; } } +/// ## FileSorting +/// +/// FileSorting defines the criteria for sorting files +#[derive(Copy, Clone, PartialEq, std::fmt::Debug)] +pub enum FileSorting { + ByName, + ByModifyTime, + ByCreationTime, +} + +/// ## GroupDirs +/// +/// GroupDirs defines how directories should be grouped in sorting files +#[derive(PartialEq, std::fmt::Debug)] +pub enum GroupDirs { + First, + Last, +} + /// ## FileExplorer /// /// File explorer states pub struct FileExplorer { - 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 + pub wrkdir: PathBuf, // Current directory + pub(crate) dirstack: VecDeque, // Stack of visited directory (max 16) + pub(crate) stack_size: usize, // Directory stack size + pub(crate) file_sorting: FileSorting, // File sorting criteria + pub(crate) group_dirs: Option, // If Some, defines how to group directories + pub(crate) opts: ExplorerOpts, // Explorer options + index: usize, // Selected file + files: Vec, // Files in directory } impl Default for FileExplorer { fn default() -> Self { FileExplorer { wrkdir: PathBuf::from("/"), - index: 0, - files: Vec::new(), dirstack: VecDeque::with_capacity(16), stack_size: 16, + file_sorting: FileSorting::ByName, + group_dirs: None, opts: ExplorerOpts::empty(), + index: 0, + files: Vec::new(), } } } @@ -141,20 +164,52 @@ impl FileExplorer { // Sorting + /// ### sort_by + /// + /// Choose sorting method; then sort files + pub fn sort_by(&mut self, sorting: FileSorting) { + // If method HAS ACTUALLY CHANGED, sort (performance!) + if self.file_sorting != sorting { + self.file_sorting = sorting; + self.sort(); + } + } + + /// ### get_file_sorting + /// + /// Get current file sorting method + pub fn get_file_sorting(&self) -> FileSorting { + self.file_sorting + } + + /// ### group_dirs_by + /// + /// Choose group dirs method; then sort files + pub fn group_dirs_by(&mut self, group_dirs: Option) { + // If method HAS ACTUALLY CHANGED, sort (performance!) + if self.group_dirs != group_dirs { + self.group_dirs = group_dirs; + self.sort(); + } + } + /// ### 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(); + // Choose sorting method + match &self.file_sorting { + FileSorting::ByName => self.sort_files_by_name(), + FileSorting::ByCreationTime => self.sort_files_by_creation_time(), + FileSorting::ByModifyTime => self.sort_files_by_mtime(), } - // Directories first (MUST COME AFTER NAME) - if self.opts.intersects(ExplorerOpts::DIRS_FIRST) { - self.sort_files_directories_first(); + // Directories first (NOTE: MUST COME AFTER OTHER SORTING) + // Group directories if necessary + if let Some(group_dirs) = &self.group_dirs { + match group_dirs { + GroupDirs::First => self.sort_files_directories_first(), + GroupDirs::Last => self.sort_files_directories_last(), + } } } @@ -170,8 +225,17 @@ impl FileExplorer { /// /// Sort files by mtime; the newest comes first fn sort_files_by_mtime(&mut self) { + self.files.sort_by(|a: &FsEntry, b: &FsEntry| { + b.get_last_change_time().cmp(&a.get_last_change_time()) + }); + } + + /// ### sort_files_by_creation_time + /// + /// Sort files by creation time; the newest comes first + fn sort_files_by_creation_time(&mut self) { self.files - .sort_by_key(|x: &FsEntry| x.get_last_change_time()); + .sort_by(|a: &FsEntry, b: &FsEntry| b.get_creation_time().cmp(&a.get_creation_time())); } /// ### sort_files_directories_first @@ -181,6 +245,13 @@ impl FileExplorer { self.files.sort_by_key(|x: &FsEntry| x.is_file()); } + /// ### sort_files_directories_last + /// + /// Sort files; directories come last + fn sort_files_directories_last(&mut self) { + self.files.sort_by_key(|x: &FsEntry| x.is_dir()); + } + /// ### incr_index /// /// Increment index to the first visible FsEntry. @@ -380,6 +451,9 @@ mod tests { assert_eq!(explorer.wrkdir, PathBuf::from("/")); assert_eq!(explorer.stack_size, 16); assert_eq!(explorer.index, 0); + assert_eq!(explorer.group_dirs, None); + assert_eq!(explorer.file_sorting, FileSorting::ByName); + assert_eq!(explorer.get_file_sorting(), FileSorting::ByName); } #[test] @@ -412,8 +486,9 @@ mod tests { #[test] 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 + // Don't show hidden files + explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES); + // Create files explorer.set_files(vec![ make_fs_entry("README.md", false), make_fs_entry("src/", true), @@ -423,14 +498,7 @@ mod tests { 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 + // Verify (files are sorted by name) assert_eq!( explorer.files.get(0).unwrap().get_name(), String::from(".git/") @@ -448,8 +516,7 @@ mod tests { 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) + // Create files (files are then sorted by name DEFAULT) explorer.set_files(vec![ make_fs_entry("README.md", false), make_fs_entry("src/", true), @@ -563,7 +630,6 @@ mod tests { #[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), @@ -576,6 +642,7 @@ mod tests { make_fs_entry("Cargo.lock", false), make_fs_entry("codecov.yml", false), ]); + explorer.sort_by(FileSorting::ByName); // First entry should be "Cargo.lock" assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock"); // Last should be "src/" @@ -585,13 +652,13 @@ mod tests { #[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]); + explorer.sort_by(FileSorting::ByModifyTime); // First entry should be "CODE_OF_CONDUCT.md" assert_eq!( explorer.files.get(0).unwrap().get_name(), @@ -602,10 +669,27 @@ mod tests { } #[test] - fn test_fs_explorer_sort_by_name_and_dir() { + fn test_fs_explorer_sort_by_creation_time() { + let mut explorer: FileExplorer = FileExplorer::default(); + 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]); + explorer.sort_by(FileSorting::ByCreationTime); + // 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_dirs_first() { 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), @@ -619,6 +703,8 @@ mod tests { make_fs_entry("Cargo.lock", false), make_fs_entry("codecov.yml", false), ]); + explorer.sort_by(FileSorting::ByName); + explorer.group_dirs_by(Some(GroupDirs::First)); // 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/"); @@ -628,6 +714,33 @@ mod tests { assert_eq!(explorer.files.get(9).unwrap().get_name(), "README.md"); } + #[test] + fn test_fs_explorer_sort_by_name_and_dirs_last() { + let mut explorer: FileExplorer = FileExplorer::default(); + // 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), + ]); + explorer.sort_by(FileSorting::ByName); + explorer.group_dirs_by(Some(GroupDirs::Last)); + // Last entry should be "src" + assert_eq!(explorer.files.get(8).unwrap().get_name(), "docs/"); + assert_eq!(explorer.files.get(9).unwrap().get_name(), "src/"); + // first is file for alphabetical order + assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock"); + // Last in files should be "README.md" (last file for alphabetical ordening) + assert_eq!(explorer.files.get(7).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/ui/activities/filetransfer_activity/misc.rs b/src/ui/activities/filetransfer_activity/misc.rs index 42c0562..4d810a5 100644 --- a/src/ui/activities/filetransfer_activity/misc.rs +++ b/src/ui/activities/filetransfer_activity/misc.rs @@ -24,7 +24,7 @@ use super::{ Color, ConfigClient, FileTransferActivity, InputField, InputMode, LogLevel, LogRecord, PopupType, }; -use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer}; +use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs}; use crate::system::environment; use crate::system::sshkey_storage::SshKeyStorage; // Ext @@ -131,8 +131,8 @@ impl FileTransferActivity { /// Build explorer reading configuration from `ConfigClient` pub(super) fn build_explorer(cli: Option<&ConfigClient>) -> FileExplorer { FileExplorerBuilder::new() - .sort_by_name() - .with_dirs_first() + .with_file_sorting(FileSorting::ByName) + .with_group_dirs(Some(GroupDirs::First)) .with_stack_size(16) .build() }