Files
termscp/src/fs/explorer/mod.rs

663 lines
23 KiB
Rust

//! ## Explorer
//!
//! `explorer` is the module which provides an Helper in handling Directory status through
/*
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
// 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<FsEntry>, // Files in directory
pub(crate) dirstack: VecDeque<PathBuf>, // Stack of visited directory (max 16)
pub(crate) stack_size: usize, // Directory stack size
pub(crate) opts: ExplorerOpts, // Explorer options
}
impl Default for FileExplorer {
fn default() -> Self {
FileExplorer {
wrkdir: PathBuf::from("/"),
index: 0,
files: Vec::new(),
dirstack: VecDeque::with_capacity(16),
stack_size: 16,
opts: ExplorerOpts::empty(),
}
}
}
impl FileExplorer {
/// ### 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<PathBuf> {
self.dirstack.pop_back()
}
/// ### set_files
///
/// Set Explorer files
/// 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<FsEntry>) {
self.files = files;
// Sort
self.sort();
// Reset index
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<dyn Iterator<Item = &FsEntry> + '_> {
// 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
///
/// Iterate all files; doesn't care about options
pub fn iter_files_all(&self) -> Box<dyn Iterator<Item = &FsEntry> + '_> {
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)
}
// 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
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
///
/// 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.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)
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.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)
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.opts.intersects(ExplorerOpts::SHOW_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.opts.toggle(ExplorerOpts::SHOW_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::thread::sleep;
use std::time::{Duration, SystemTime};
#[test]
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.opts, ExplorerOpts::empty());
assert_eq!(explorer.wrkdir, PathBuf::from("/"));
assert_eq!(explorer.stack_size, 16);
assert_eq!(explorer.index, 0);
}
#[test]
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"));
// 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_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),
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_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),
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();
// 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.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)
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.opts.intersects(ExplorerOpts::SHOW_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.opts.intersects(ExplorerOpts::SHOW_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);
}
#[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 {
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
}),
}
}
}