mirror of
https://github.com/veeso/termscp.git
synced 2025-12-07 09:36:00 -08:00
Removed filetransfer module; migrated to remotefs crate
This commit is contained in:
committed by
Christian Visintin
parent
25dd1b9b0a
commit
df7a4381c4
147
src/explorer/builder.rs
Normal file
147
src/explorer/builder.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
//! ## Builder
|
||||
//!
|
||||
//! `builder` is the module which provides a builder for FileExplorer
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::formatter::Formatter;
|
||||
use super::{ExplorerOpts, FileExplorer, FileSorting, GroupDirs};
|
||||
// Ext
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// ## FileExplorerBuilder
|
||||
///
|
||||
/// Struct used to create a `FileExplorer`
|
||||
pub struct FileExplorerBuilder {
|
||||
explorer: Option<FileExplorer>,
|
||||
}
|
||||
|
||||
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, val: bool) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
match val {
|
||||
true => e.opts.insert(ExplorerOpts::SHOW_HIDDEN_FILES),
|
||||
false => e.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES),
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_file_sorting
|
||||
///
|
||||
/// Set sorting method
|
||||
pub fn with_file_sorting(&mut self, sorting: FileSorting) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
e.sort_by(sorting);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_dirs_first
|
||||
///
|
||||
/// Enable DIRS_FIRST option
|
||||
pub fn with_group_dirs(&mut self, group_dirs: Option<GroupDirs>) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
e.group_dirs_by(group_dirs);
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
/// ### with_formatter
|
||||
///
|
||||
/// Set formatter for FileExplorer
|
||||
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
if let Some(fmt_str) = fmt_str {
|
||||
e.fmt = Formatter::new(fmt_str);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_builder_new_default() {
|
||||
let explorer: FileExplorer = FileExplorerBuilder::new().build();
|
||||
// Verify
|
||||
assert!(!explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES));
|
||||
assert_eq!(explorer.file_sorting, FileSorting::Name); // 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()
|
||||
.with_file_sorting(FileSorting::ModifyTime)
|
||||
.with_group_dirs(Some(GroupDirs::First))
|
||||
.with_hidden_files(true)
|
||||
.with_stack_size(24)
|
||||
.with_formatter(Some("{NAME}"))
|
||||
.build();
|
||||
// Verify
|
||||
assert!(explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES));
|
||||
assert_eq!(explorer.file_sorting, FileSorting::ModifyTime); // Default
|
||||
assert_eq!(explorer.group_dirs, Some(GroupDirs::First));
|
||||
assert_eq!(explorer.stack_size, 24);
|
||||
}
|
||||
}
|
||||
968
src/explorer/formatter.rs
Normal file
968
src/explorer/formatter.rs
Normal file
@@ -0,0 +1,968 @@
|
||||
//! ## Formatter
|
||||
//!
|
||||
//! `formatter` is the module which provides formatting utilities for `FileExplorer`
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
|
||||
use crate::utils::path::diff_paths;
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use regex::Regex;
|
||||
use remotefs::Entry;
|
||||
use std::path::PathBuf;
|
||||
#[cfg(target_family = "unix")]
|
||||
use users::{get_group_by_gid, get_user_by_uid};
|
||||
// Types
|
||||
// FmtCallback: Formatter, fsentry: &Entry, cur_str, prefix, length, extra
|
||||
type FmtCallback = fn(&Formatter, &Entry, &str, &str, Option<&usize>, Option<&String>) -> String;
|
||||
|
||||
// Keys
|
||||
const FMT_KEY_ATIME: &str = "ATIME";
|
||||
const FMT_KEY_CTIME: &str = "CTIME";
|
||||
const FMT_KEY_GROUP: &str = "GROUP";
|
||||
const FMT_KEY_MTIME: &str = "MTIME";
|
||||
const FMT_KEY_NAME: &str = "NAME";
|
||||
const FMT_KEY_PATH: &str = "PATH";
|
||||
const FMT_KEY_PEX: &str = "PEX";
|
||||
const FMT_KEY_SIZE: &str = "SIZE";
|
||||
const FMT_KEY_SYMLINK: &str = "SYMLINK";
|
||||
const FMT_KEY_USER: &str = "USER";
|
||||
// Default
|
||||
const FMT_DEFAULT_STX: &str = "{NAME} {PEX} {USER} {SIZE} {MTIME}";
|
||||
// Regex
|
||||
lazy_static! {
|
||||
/**
|
||||
* Regex matches:
|
||||
* - group 0: KEY NAME
|
||||
* - group 1?: LENGTH
|
||||
* - group 2?: EXTRA
|
||||
*/
|
||||
static ref FMT_KEY_REGEX: Regex = Regex::new(r"\{(.*?)\}").ok().unwrap();
|
||||
static ref FMT_ATTR_REGEX: Regex = Regex::new(r"(?:([A-Z]+))(:?([0-9]+))?(:?(.+))?").ok().unwrap();
|
||||
}
|
||||
|
||||
/// ## CallChainBlock
|
||||
///
|
||||
/// Call Chain block is a block in a chain of functions which are called in order to format the Entry.
|
||||
/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found
|
||||
/// a chain of function is made using the Formatters method.
|
||||
/// This method provides an extremely fast way to format fs entries
|
||||
struct CallChainBlock {
|
||||
/// The function to call to format current item
|
||||
func: FmtCallback,
|
||||
/// All the content which is between two `{KEY}` items
|
||||
prefix: String,
|
||||
/// The fmt len, specied for key as `{KEY:LEN}`
|
||||
fmt_len: Option<usize>,
|
||||
/// The extra argument for formatting, specified for key as `{KEY:LEN:EXTRA}`
|
||||
fmt_extra: Option<String>,
|
||||
/// The next block to format
|
||||
next_block: Option<Box<CallChainBlock>>,
|
||||
}
|
||||
|
||||
impl CallChainBlock {
|
||||
/// ### new
|
||||
///
|
||||
/// Create a new `CallChainBlock`
|
||||
pub fn new(
|
||||
func: FmtCallback,
|
||||
prefix: String,
|
||||
fmt_len: Option<usize>,
|
||||
fmt_extra: Option<String>,
|
||||
) -> Self {
|
||||
CallChainBlock {
|
||||
func,
|
||||
prefix,
|
||||
fmt_len,
|
||||
fmt_extra,
|
||||
next_block: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### next
|
||||
///
|
||||
/// Call next callback in the CallChain
|
||||
pub fn next(&self, fmt: &Formatter, fsentry: &Entry, cur_str: &str) -> String {
|
||||
// Call func
|
||||
let new_str: String = (self.func)(
|
||||
fmt,
|
||||
fsentry,
|
||||
cur_str,
|
||||
self.prefix.as_str(),
|
||||
self.fmt_len.as_ref(),
|
||||
self.fmt_extra.as_ref(),
|
||||
);
|
||||
// If next is some, call next, otherwise (END OF CHAIN) return new_str
|
||||
match &self.next_block {
|
||||
Some(block) => block.next(fmt, fsentry, new_str.as_str()),
|
||||
None => new_str,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### push
|
||||
///
|
||||
/// Push func to the last element in the Call chain
|
||||
pub fn push(
|
||||
&mut self,
|
||||
func: FmtCallback,
|
||||
prefix: String,
|
||||
fmt_len: Option<usize>,
|
||||
fmt_extra: Option<String>,
|
||||
) {
|
||||
// Call recursively until an element with next_block equal to None is found
|
||||
match &mut self.next_block {
|
||||
None => {
|
||||
self.next_block = Some(Box::new(CallChainBlock::new(
|
||||
func, prefix, fmt_len, fmt_extra,
|
||||
)))
|
||||
}
|
||||
Some(block) => block.push(func, prefix, fmt_len, fmt_extra),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Formatter
|
||||
///
|
||||
/// Formatter takes care of formatting FsEntries according to the provided keys.
|
||||
/// Formatting is performed using the `CallChainBlock`, which composed makes a Call Chain. This method is extremely fast compared to match the format groups
|
||||
/// at each fmt call.
|
||||
pub struct Formatter {
|
||||
call_chain: CallChainBlock,
|
||||
}
|
||||
|
||||
impl Default for Formatter {
|
||||
/// ### default
|
||||
///
|
||||
/// Instantiates a Formatter with the default fmt syntax
|
||||
fn default() -> Self {
|
||||
Formatter {
|
||||
call_chain: Self::make_callchain(FMT_DEFAULT_STX),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatter {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new `Formatter` with the provided format string
|
||||
pub fn new(fmt_str: &str) -> Self {
|
||||
Formatter {
|
||||
call_chain: Self::make_callchain(fmt_str),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt
|
||||
///
|
||||
/// Format fsentry
|
||||
pub fn fmt(&self, fsentry: &Entry) -> String {
|
||||
// Execute callchain blocks
|
||||
self.call_chain.next(self, fsentry, "")
|
||||
}
|
||||
|
||||
// Fmt methods
|
||||
|
||||
/// ### fmt_atime
|
||||
///
|
||||
/// Format last access time
|
||||
fn fmt_atime(
|
||||
&self,
|
||||
fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get date (use extra args as format or default "%b %d %Y %H:%M")
|
||||
let datetime: String = fmt_time(
|
||||
fsentry.metadata().atime,
|
||||
match fmt_extra {
|
||||
Some(fmt) => fmt.as_ref(),
|
||||
None => "%b %d %Y %H:%M",
|
||||
},
|
||||
);
|
||||
// Add to cur str, prefix and the key value
|
||||
format!(
|
||||
"{}{}{:0width$}",
|
||||
cur_str,
|
||||
prefix,
|
||||
datetime,
|
||||
width = fmt_len.unwrap_or(&17)
|
||||
)
|
||||
}
|
||||
|
||||
/// ### fmt_ctime
|
||||
///
|
||||
/// Format creation time
|
||||
fn fmt_ctime(
|
||||
&self,
|
||||
fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get date
|
||||
let datetime: String = fmt_time(
|
||||
fsentry.metadata().ctime,
|
||||
match fmt_extra {
|
||||
Some(fmt) => fmt.as_ref(),
|
||||
None => "%b %d %Y %H:%M",
|
||||
},
|
||||
);
|
||||
// Add to cur str, prefix and the key value
|
||||
format!(
|
||||
"{}{}{:0width$}",
|
||||
cur_str,
|
||||
prefix,
|
||||
datetime,
|
||||
width = fmt_len.unwrap_or(&17)
|
||||
)
|
||||
}
|
||||
|
||||
/// ### fmt_group
|
||||
///
|
||||
/// Format owner group
|
||||
fn fmt_group(
|
||||
&self,
|
||||
fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get username
|
||||
#[cfg(target_family = "unix")]
|
||||
let group: String = match fsentry.metadata().gid {
|
||||
Some(gid) => match get_group_by_gid(gid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
None => gid.to_string(),
|
||||
},
|
||||
None => 0.to_string(),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let group: String = match fsentry.metadata().gid {
|
||||
Some(gid) => gid.to_string(),
|
||||
None => 0.to_string(),
|
||||
};
|
||||
// Add to cur str, prefix and the key value
|
||||
format!(
|
||||
"{}{}{:0width$}",
|
||||
cur_str,
|
||||
prefix,
|
||||
group,
|
||||
width = fmt_len.unwrap_or(&12)
|
||||
)
|
||||
}
|
||||
|
||||
/// ### fmt_mtime
|
||||
///
|
||||
/// Format last change time
|
||||
fn fmt_mtime(
|
||||
&self,
|
||||
fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get date
|
||||
let datetime: String = fmt_time(
|
||||
fsentry.metadata().mtime,
|
||||
match fmt_extra {
|
||||
Some(fmt) => fmt.as_ref(),
|
||||
None => "%b %d %Y %H:%M",
|
||||
},
|
||||
);
|
||||
// Add to cur str, prefix and the key value
|
||||
format!(
|
||||
"{}{}{:0width$}",
|
||||
cur_str,
|
||||
prefix,
|
||||
datetime,
|
||||
width = fmt_len.unwrap_or(&17)
|
||||
)
|
||||
}
|
||||
|
||||
/// ### fmt_name
|
||||
///
|
||||
/// Format file name
|
||||
fn fmt_name(
|
||||
&self,
|
||||
fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get file name (or elide if too long)
|
||||
let file_len: usize = match fmt_len {
|
||||
Some(l) => *l,
|
||||
None => 24,
|
||||
};
|
||||
let name: &str = fsentry.name();
|
||||
let last_idx: usize = match fsentry.is_dir() {
|
||||
// NOTE: For directories is l - 2, since we push '/' to name
|
||||
true => file_len - 2,
|
||||
false => file_len - 1,
|
||||
};
|
||||
let mut name: String = match name.len() >= file_len {
|
||||
false => name.to_string(),
|
||||
true => format!("{}…", &name[0..last_idx]),
|
||||
};
|
||||
if fsentry.is_dir() {
|
||||
name.push('/');
|
||||
}
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:0width$}", cur_str, prefix, name, width = file_len)
|
||||
}
|
||||
|
||||
/// ### fmt_path
|
||||
///
|
||||
/// Format path
|
||||
fn fmt_path(
|
||||
&self,
|
||||
fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
let p = match fmt_extra {
|
||||
None => fsentry.path().to_path_buf(),
|
||||
Some(rel) => diff_paths(fsentry.path(), PathBuf::from(rel.as_str()).as_path())
|
||||
.unwrap_or_else(|| fsentry.path().to_path_buf()),
|
||||
};
|
||||
format!(
|
||||
"{}{}{}",
|
||||
cur_str,
|
||||
prefix,
|
||||
match fmt_len {
|
||||
None => p.display().to_string(),
|
||||
Some(len) => fmt_path_elide(p.as_path(), *len),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// ### fmt_pex
|
||||
///
|
||||
/// Format file permissions
|
||||
fn fmt_pex(
|
||||
&self,
|
||||
fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
_fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Create mode string
|
||||
let mut pex: String = String::with_capacity(10);
|
||||
let file_type: char = match fsentry.metadata().symlink.is_some() {
|
||||
true => 'l',
|
||||
false => match fsentry.is_dir() {
|
||||
true => 'd',
|
||||
false => '-',
|
||||
},
|
||||
};
|
||||
pex.push(file_type);
|
||||
match fsentry.metadata().mode {
|
||||
None => pex.push_str("?????????"),
|
||||
Some(mode) => pex.push_str(
|
||||
format!(
|
||||
"{}{}{}",
|
||||
fmt_pex(mode.user()),
|
||||
fmt_pex(mode.group()),
|
||||
fmt_pex(mode.others())
|
||||
)
|
||||
.as_str(),
|
||||
),
|
||||
}
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:10}", cur_str, prefix, pex)
|
||||
}
|
||||
|
||||
/// ### fmt_size
|
||||
///
|
||||
/// Format file size
|
||||
fn fmt_size(
|
||||
&self,
|
||||
fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
_fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
if fsentry.is_file() {
|
||||
// Get byte size
|
||||
let size: ByteSize = ByteSize(fsentry.metadata().size);
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:10}", cur_str, prefix, size.to_string())
|
||||
} else {
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{} ", cur_str, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt_symlink
|
||||
///
|
||||
/// Format file symlink (if any)
|
||||
fn fmt_symlink(
|
||||
&self,
|
||||
fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get file name (or elide if too long)
|
||||
let file_len: usize = match fmt_len {
|
||||
Some(l) => *l,
|
||||
None => 21,
|
||||
};
|
||||
// Replace `FMT_KEY_NAME` with name
|
||||
match fsentry.metadata().symlink.as_deref() {
|
||||
None => format!("{}{} ", cur_str, prefix),
|
||||
Some(p) => format!(
|
||||
"{}{}-> {:0width$}",
|
||||
cur_str,
|
||||
prefix,
|
||||
fmt_path_elide(p, file_len - 1),
|
||||
width = file_len
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt_user
|
||||
///
|
||||
/// Format owner user
|
||||
fn fmt_user(
|
||||
&self,
|
||||
fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
_fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get username
|
||||
#[cfg(target_family = "unix")]
|
||||
let username: String = match fsentry.metadata().uid {
|
||||
Some(uid) => match get_user_by_uid(uid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
None => uid.to_string(),
|
||||
},
|
||||
None => 0.to_string(),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let username: String = match fsentry.metadata().uid {
|
||||
Some(uid) => uid.to_string(),
|
||||
None => 0.to_string(),
|
||||
};
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:12}", cur_str, prefix, username)
|
||||
}
|
||||
|
||||
/// ### fmt_fallback
|
||||
///
|
||||
/// Fallback function in case the format key is unknown
|
||||
/// It does nothing, just returns cur_str
|
||||
fn fmt_fallback(
|
||||
&self,
|
||||
_fsentry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
_fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Add to cur str and prefix
|
||||
format!("{}{}", cur_str, prefix)
|
||||
}
|
||||
|
||||
// Static
|
||||
|
||||
/// ### make_callchain
|
||||
///
|
||||
/// Make a callchain starting from the fmt str
|
||||
fn make_callchain(fmt_str: &str) -> CallChainBlock {
|
||||
// Init chain block
|
||||
let mut callchain: Option<CallChainBlock> = None;
|
||||
// Track index of the last match found, to get the prefix for each token
|
||||
let mut last_index: usize = 0;
|
||||
// Match fmt str against regex
|
||||
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
|
||||
// Get match index (unwrap is safe, since always exists)
|
||||
let index: usize = fmt_str.find(®ex_match[0]).unwrap();
|
||||
// Get prefix
|
||||
let prefix: String = String::from(&fmt_str[last_index..index]);
|
||||
// Increment last index (sum prefix lenght and the length of the key)
|
||||
last_index += prefix.len() + regex_match[0].len();
|
||||
// Match attributes
|
||||
match FMT_ATTR_REGEX.captures(®ex_match[1]) {
|
||||
Some(regex_match) => {
|
||||
// Match group 0 (which is name)
|
||||
let callback: FmtCallback = match ®ex_match.get(1) {
|
||||
Some(key) => match key.as_str() {
|
||||
FMT_KEY_ATIME => Self::fmt_atime,
|
||||
FMT_KEY_CTIME => Self::fmt_ctime,
|
||||
FMT_KEY_GROUP => Self::fmt_group,
|
||||
FMT_KEY_MTIME => Self::fmt_mtime,
|
||||
FMT_KEY_NAME => Self::fmt_name,
|
||||
FMT_KEY_PATH => Self::fmt_path,
|
||||
FMT_KEY_PEX => Self::fmt_pex,
|
||||
FMT_KEY_SIZE => Self::fmt_size,
|
||||
FMT_KEY_SYMLINK => Self::fmt_symlink,
|
||||
FMT_KEY_USER => Self::fmt_user,
|
||||
_ => Self::fmt_fallback,
|
||||
},
|
||||
None => Self::fmt_fallback,
|
||||
};
|
||||
// Match format length: group 3
|
||||
let fmt_len: Option<usize> = match ®ex_match.get(3) {
|
||||
Some(len) => match len.as_str().parse::<usize>() {
|
||||
Ok(len) => Some(len),
|
||||
Err(_) => None,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
// Match format extra: group 2 + 1
|
||||
let fmt_extra: Option<String> = regex_match
|
||||
.get(5)
|
||||
.as_ref()
|
||||
.map(|extra| extra.as_str().to_string());
|
||||
// Create a callchain or push new element to its back
|
||||
match callchain.as_mut() {
|
||||
None => {
|
||||
callchain =
|
||||
Some(CallChainBlock::new(callback, prefix, fmt_len, fmt_extra))
|
||||
}
|
||||
Some(chain_block) => chain_block.push(callback, prefix, fmt_len, fmt_extra),
|
||||
}
|
||||
}
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
// Finalize and return
|
||||
match callchain {
|
||||
Some(callchain) => callchain,
|
||||
None => CallChainBlock::new(Self::fmt_fallback, String::new(), None, None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use remotefs::fs::{Directory, File, Metadata, UnixPex};
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_callchain() {
|
||||
// Make a dummy formatter
|
||||
let dummy_formatter: Formatter = Formatter::new("");
|
||||
// Make a dummy entry
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let dummy_entry: Entry = Entry::File(File {
|
||||
name: String::from("bar.txt"),
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
extension: Some(String::from("txt")),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
});
|
||||
let prefix: String = String::from("h");
|
||||
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
|
||||
assert!(callchain.next_block.is_none());
|
||||
assert_eq!(callchain.prefix, String::from("h"));
|
||||
// Execute
|
||||
assert_eq!(
|
||||
callchain.next(&dummy_formatter, &dummy_entry, ""),
|
||||
String::from("hA")
|
||||
);
|
||||
// Push 4 new blocks
|
||||
callchain.push(dummy_fmt, String::from("h"), None, None);
|
||||
callchain.push(dummy_fmt, String::from("h"), None, None);
|
||||
callchain.push(dummy_fmt, String::from("h"), None, None);
|
||||
callchain.push(dummy_fmt, String::from("h"), None, None);
|
||||
// Verify
|
||||
assert_eq!(
|
||||
callchain.next(&dummy_formatter, &dummy_entry, ""),
|
||||
String::from("hAhAhAhAhA")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_format_files() {
|
||||
// Make default
|
||||
let formatter: Formatter = Formatter::default();
|
||||
// Experiments :D
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry: Entry = Entry::File(File {
|
||||
name: String::from("bar.txt"),
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
extension: Some(String::from("txt")),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
});
|
||||
#[cfg(target_family = "unix")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// Elide name
|
||||
let entry: Entry = Entry::File(File {
|
||||
name: String::from("piroparoporoperoperupupu.txt"),
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
extension: Some(String::from("txt")),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
});
|
||||
#[cfg(target_family = "unix")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperupup… -rw-r--r-- root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperupup… -rw-r--r-- 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No pex
|
||||
let entry: Entry = Entry::File(File {
|
||||
name: String::from("bar.txt"),
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
extension: Some(String::from("txt")),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: None,
|
||||
},
|
||||
});
|
||||
#[cfg(target_family = "unix")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No user
|
||||
let entry: Entry = Entry::File(File {
|
||||
name: String::from("bar.txt"),
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
extension: Some(String::from("txt")),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: Some(0),
|
||||
mode: None,
|
||||
},
|
||||
});
|
||||
#[cfg(target_family = "unix")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_format_dirs() {
|
||||
// Make default
|
||||
let formatter: Formatter = Formatter::default();
|
||||
// Experiments :D
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry: Entry = Entry::Directory(Directory {
|
||||
name: String::from("projects"),
|
||||
path: PathBuf::from("/home/cvisintin/projects"),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 4096,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
});
|
||||
#[cfg(target_family = "unix")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ drwxr-xr-x root {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ drwxr-xr-x 0 {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No pex, no user
|
||||
let entry: Entry = Entry::Directory(Directory {
|
||||
name: String::from("projects"),
|
||||
path: PathBuf::from("/home/cvisintin/projects"),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 4096,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: Some(0),
|
||||
mode: None,
|
||||
},
|
||||
});
|
||||
#[cfg(target_family = "unix")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ d????????? 0 {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ d????????? 0 {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_all_together_now() {
|
||||
let formatter: Formatter =
|
||||
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
|
||||
// Directory (with symlink)
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry: Entry = Entry::Directory(Directory {
|
||||
name: String::from("projects"),
|
||||
path: PathBuf::from("/home/cvisintin/project"),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 4096,
|
||||
symlink: Some(PathBuf::from("project.info")),
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
// Directory without symlink
|
||||
let entry: Entry = Entry::Directory(Directory {
|
||||
name: String::from("projects"),
|
||||
path: PathBuf::from("/home/cvisintin/project"),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 4096,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o755)),
|
||||
},
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"projects/ 0 0 drwxr-xr-x {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
// File with symlink
|
||||
let entry: Entry = Entry::File(File {
|
||||
name: String::from("bar.txt"),
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
extension: Some(String::from("txt")),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 8192,
|
||||
symlink: Some(PathBuf::from("project.info")),
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
// File without symlink
|
||||
let entry: Entry = Entry::File(File {
|
||||
name: String::from("bar.txt"),
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
extension: Some(String::from("txt")),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 8192,
|
||||
symlink: None,
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_family = "unix")]
|
||||
fn should_fmt_path() {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry: Entry = Entry::File(File {
|
||||
name: String::from("bar.txt"),
|
||||
path: PathBuf::from("/tmp/a/b/c/bar.txt"),
|
||||
extension: Some(String::from("txt")),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
size: 8192,
|
||||
symlink: Some(PathBuf::from("project.info")),
|
||||
uid: None,
|
||||
gid: None,
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
});
|
||||
let formatter: Formatter = Formatter::new("File path: {PATH}");
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry).as_str(),
|
||||
"File path: /tmp/a/b/c/bar.txt"
|
||||
);
|
||||
let formatter: Formatter = Formatter::new("File path: {PATH:8}");
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry).as_str(),
|
||||
"File path: /tmp/…/c/bar.txt"
|
||||
);
|
||||
let formatter: Formatter = Formatter::new("File path: {PATH:128:/tmp/a/b}");
|
||||
assert_eq!(formatter.fmt(&entry).as_str(), "File path: c/bar.txt");
|
||||
}
|
||||
|
||||
/// ### dummy_fmt
|
||||
///
|
||||
/// Dummy formatter, just yelds an 'A' at the end of the current string
|
||||
fn dummy_fmt(
|
||||
_fmt: &Formatter,
|
||||
_entry: &Entry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
_fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
format!("{}{}A", cur_str, prefix)
|
||||
}
|
||||
}
|
||||
707
src/explorer/mod.rs
Normal file
707
src/explorer/mod.rs
Normal file
@@ -0,0 +1,707 @@
|
||||
//! ## Explorer
|
||||
//!
|
||||
//! `explorer` is the module which provides an Helper in handling Directory status through
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// Mods
|
||||
pub(crate) mod builder;
|
||||
mod formatter;
|
||||
// Locals
|
||||
use formatter::Formatter;
|
||||
// Ext
|
||||
use remotefs::fs::Entry;
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
bitflags! {
|
||||
/// ## ExplorerOpts
|
||||
///
|
||||
/// ExplorerOpts are bit options which provides different behaviours to `FileExplorer`
|
||||
pub(crate) struct ExplorerOpts: u32 {
|
||||
const SHOW_HIDDEN_FILES = 0b00000001;
|
||||
}
|
||||
}
|
||||
|
||||
/// ## FileSorting
|
||||
///
|
||||
/// FileSorting defines the criteria for sorting files
|
||||
#[derive(Copy, Clone, PartialEq, std::fmt::Debug)]
|
||||
pub enum FileSorting {
|
||||
Name,
|
||||
ModifyTime,
|
||||
CreationTime,
|
||||
Size,
|
||||
}
|
||||
|
||||
/// ## 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
|
||||
pub(crate) dirstack: VecDeque<PathBuf>, // 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<GroupDirs>, // If Some, defines how to group directories
|
||||
pub(crate) opts: ExplorerOpts, // Explorer options
|
||||
pub(crate) fmt: Formatter, // Entry formatter
|
||||
files: Vec<Entry>, // Files in directory
|
||||
}
|
||||
|
||||
impl Default for FileExplorer {
|
||||
fn default() -> Self {
|
||||
FileExplorer {
|
||||
wrkdir: PathBuf::from("/"),
|
||||
dirstack: VecDeque::with_capacity(16),
|
||||
stack_size: 16,
|
||||
file_sorting: FileSorting::Name,
|
||||
group_dirs: None,
|
||||
opts: ExplorerOpts::empty(),
|
||||
fmt: Formatter::default(),
|
||||
files: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<Entry>) {
|
||||
self.files = files;
|
||||
// Sort
|
||||
self.sort();
|
||||
}
|
||||
|
||||
/// ### del_entry
|
||||
///
|
||||
/// Delete file at provided index
|
||||
pub fn del_entry(&mut self, idx: usize) {
|
||||
if self.files.len() > idx {
|
||||
self.files.remove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// ### 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) -> impl Iterator<Item = &Entry> + '_ {
|
||||
// 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) -> impl Iterator<Item = &Entry> + '_ {
|
||||
Box::new(self.files.iter())
|
||||
}
|
||||
|
||||
/// ### get
|
||||
///
|
||||
/// Get file at relative index
|
||||
pub fn get(&self, idx: usize) -> Option<&Entry> {
|
||||
let opts: ExplorerOpts = self.opts;
|
||||
let filtered = 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
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
filtered.get(idx).copied()
|
||||
}
|
||||
|
||||
// Formatting
|
||||
|
||||
/// ### fmt_file
|
||||
///
|
||||
/// Format a file entry
|
||||
pub fn fmt_file(&self, entry: &Entry) -> String {
|
||||
self.fmt.fmt(entry)
|
||||
}
|
||||
|
||||
// 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<GroupDirs>) {
|
||||
// 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) {
|
||||
// Choose sorting method
|
||||
match &self.file_sorting {
|
||||
FileSorting::Name => self.sort_files_by_name(),
|
||||
FileSorting::CreationTime => self.sort_files_by_creation_time(),
|
||||
FileSorting::ModifyTime => self.sort_files_by_mtime(),
|
||||
FileSorting::Size => self.sort_files_by_size(),
|
||||
}
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### 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: &Entry| x.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(|a: &Entry, b: &Entry| b.metadata().mtime.cmp(&a.metadata().mtime));
|
||||
}
|
||||
|
||||
/// ### 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(|b: &Entry| Reverse(b.metadata().ctime));
|
||||
}
|
||||
|
||||
/// ### sort_files_by_size
|
||||
///
|
||||
/// Sort files by size
|
||||
fn sort_files_by_size(&mut self) {
|
||||
self.files
|
||||
.sort_by_key(|b: &Entry| Reverse(b.metadata().size));
|
||||
}
|
||||
|
||||
/// ### sort_files_directories_first
|
||||
///
|
||||
/// Sort files; directories come first
|
||||
fn sort_files_directories_first(&mut self) {
|
||||
self.files.sort_by_key(|x: &Entry| 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: &Entry| x.is_dir());
|
||||
}
|
||||
|
||||
/// ### toggle_hidden_files
|
||||
///
|
||||
/// Enable/disable hidden files
|
||||
pub fn toggle_hidden_files(&mut self) {
|
||||
self.opts.toggle(ExplorerOpts::SHOW_HIDDEN_FILES);
|
||||
}
|
||||
|
||||
/// ### hidden_files_visible
|
||||
///
|
||||
/// Returns whether hidden files are visible
|
||||
pub fn hidden_files_visible(&self) -> bool {
|
||||
self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)
|
||||
}
|
||||
}
|
||||
|
||||
// Traits
|
||||
|
||||
impl ToString for FileSorting {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(match self {
|
||||
FileSorting::CreationTime => "by_creation_time",
|
||||
FileSorting::ModifyTime => "by_mtime",
|
||||
FileSorting::Name => "by_name",
|
||||
FileSorting::Size => "by_size",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FileSorting {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"by_creation_time" => Ok(FileSorting::CreationTime),
|
||||
"by_mtime" => Ok(FileSorting::ModifyTime),
|
||||
"by_name" => Ok(FileSorting::Name),
|
||||
"by_size" => Ok(FileSorting::Size),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for GroupDirs {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(match self {
|
||||
GroupDirs::First => "first",
|
||||
GroupDirs::Last => "last",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for GroupDirs {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"first" => Ok(GroupDirs::First),
|
||||
"last" => Ok(GroupDirs::Last),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use remotefs::fs::{Directory, File, Metadata, UnixPex};
|
||||
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.group_dirs, None);
|
||||
assert_eq!(explorer.file_sorting, FileSorting::Name);
|
||||
assert_eq!(explorer.get_file_sorting(), FileSorting::Name);
|
||||
}
|
||||
|
||||
#[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();
|
||||
// Don't show hidden files
|
||||
explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES);
|
||||
assert_eq!(explorer.hidden_files_visible(), 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!(explorer.get(0).is_some());
|
||||
assert!(explorer.get(100).is_none());
|
||||
//assert_eq!(explorer.count(), 6);
|
||||
// Verify (files are sorted by name)
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), ".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.hidden_files_visible(), true);
|
||||
assert_eq!(explorer.iter_files().count(), 6); // All files are returned now
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_name() {
|
||||
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("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::Name);
|
||||
// First entry should be "Cargo.lock"
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock");
|
||||
// Last should be "src/"
|
||||
assert_eq!(explorer.files.get(8).unwrap().name(), "src/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_mtime() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
let entry1: Entry = make_fs_entry("README.md", false);
|
||||
// Wait 1 sec
|
||||
sleep(Duration::from_secs(1));
|
||||
let entry2: Entry = 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::ModifyTime);
|
||||
// First entry should be "CODE_OF_CONDUCT.md"
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
|
||||
// Last should be "src/"
|
||||
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_creation_time() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
let entry1: Entry = make_fs_entry("README.md", false);
|
||||
// Wait 1 sec
|
||||
sleep(Duration::from_secs(1));
|
||||
let entry2: Entry = 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::CreationTime);
|
||||
// First entry should be "CODE_OF_CONDUCT.md"
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
|
||||
// Last should be "src/"
|
||||
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_size() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
// Create files (files are then sorted by name)
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry_with_size("README.md", false, 1024),
|
||||
make_fs_entry_with_size("src/", true, 4096),
|
||||
make_fs_entry_with_size("CONTRIBUTING.md", false, 256),
|
||||
]);
|
||||
explorer.sort_by(FileSorting::Size);
|
||||
// Directory has size 4096
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "src/");
|
||||
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
|
||||
assert_eq!(explorer.files.get(2).unwrap().name(), "CONTRIBUTING.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_name_and_dirs_first() {
|
||||
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::Name);
|
||||
explorer.group_dirs_by(Some(GroupDirs::First));
|
||||
// First entry should be "docs"
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "docs/");
|
||||
assert_eq!(explorer.files.get(1).unwrap().name(), "src/");
|
||||
// 3rd is file first for alphabetical order
|
||||
assert_eq!(explorer.files.get(2).unwrap().name(), "Cargo.lock");
|
||||
// Last should be "README.md" (last file for alphabetical ordening)
|
||||
assert_eq!(explorer.files.get(9).unwrap().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::Name);
|
||||
explorer.group_dirs_by(Some(GroupDirs::Last));
|
||||
// Last entry should be "src"
|
||||
assert_eq!(explorer.files.get(8).unwrap().name(), "docs/");
|
||||
assert_eq!(explorer.files.get(9).unwrap().name(), "src/");
|
||||
// first is file for alphabetical order
|
||||
assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock");
|
||||
// Last in files should be "README.md" (last file for alphabetical ordening)
|
||||
assert_eq!(explorer.files.get(7).unwrap().name(), "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_fmt() {
|
||||
let explorer: FileExplorer = FileExplorer::default();
|
||||
// Create fs entry
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry: Entry = Entry::File(File {
|
||||
name: String::from("bar.txt"),
|
||||
path: PathBuf::from("/bar.txt"),
|
||||
extension: Some(String::from("txt")),
|
||||
metadata: Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
size: 8192,
|
||||
mtime: t,
|
||||
symlink: None,
|
||||
uid: Some(0),
|
||||
gid: Some(0),
|
||||
mode: Some(UnixPex::from(0o644)),
|
||||
},
|
||||
});
|
||||
#[cfg(target_family = "unix")]
|
||||
assert_eq!(
|
||||
explorer.fmt_file(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
explorer.fmt_file(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_to_string_from_str_traits() {
|
||||
// File Sorting
|
||||
assert_eq!(FileSorting::CreationTime.to_string(), "by_creation_time");
|
||||
assert_eq!(FileSorting::ModifyTime.to_string(), "by_mtime");
|
||||
assert_eq!(FileSorting::Name.to_string(), "by_name");
|
||||
assert_eq!(FileSorting::Size.to_string(), "by_size");
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_creation_time").ok().unwrap(),
|
||||
FileSorting::CreationTime
|
||||
);
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_mtime").ok().unwrap(),
|
||||
FileSorting::ModifyTime
|
||||
);
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_name").ok().unwrap(),
|
||||
FileSorting::Name
|
||||
);
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_size").ok().unwrap(),
|
||||
FileSorting::Size
|
||||
);
|
||||
assert!(FileSorting::from_str("omar").is_err());
|
||||
// Group dirs
|
||||
assert_eq!(GroupDirs::First.to_string(), "first");
|
||||
assert_eq!(GroupDirs::Last.to_string(), "last");
|
||||
assert_eq!(GroupDirs::from_str("first").ok().unwrap(), GroupDirs::First);
|
||||
assert_eq!(GroupDirs::from_str("last").ok().unwrap(), GroupDirs::Last);
|
||||
assert!(GroupDirs::from_str("omar").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_del_entry() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
// Create files (files are then sorted by name)
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry("CONTRIBUTING.md", false),
|
||||
make_fs_entry("docs/", true),
|
||||
make_fs_entry("src/", true),
|
||||
make_fs_entry("README.md", false),
|
||||
]);
|
||||
explorer.del_entry(0);
|
||||
assert_eq!(explorer.files.len(), 3);
|
||||
assert_eq!(explorer.files[0].name(), "docs/");
|
||||
explorer.del_entry(5);
|
||||
assert_eq!(explorer.files.len(), 3);
|
||||
}
|
||||
|
||||
fn make_fs_entry(name: &str, is_dir: bool) -> Entry {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let metadata = Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
symlink: None,
|
||||
gid: Some(0),
|
||||
uid: Some(0),
|
||||
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
|
||||
size: 64,
|
||||
};
|
||||
match is_dir {
|
||||
false => Entry::File(File {
|
||||
name: name.to_string(),
|
||||
path: PathBuf::from(name),
|
||||
extension: None,
|
||||
metadata,
|
||||
}),
|
||||
true => Entry::Directory(Directory {
|
||||
name: name.to_string(),
|
||||
path: PathBuf::from(name),
|
||||
metadata,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> Entry {
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let metadata = Metadata {
|
||||
atime: t,
|
||||
ctime: t,
|
||||
mtime: t,
|
||||
symlink: None,
|
||||
gid: Some(0),
|
||||
uid: Some(0),
|
||||
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
|
||||
size: size as u64,
|
||||
};
|
||||
match is_dir {
|
||||
false => Entry::File(File {
|
||||
name: name.to_string(),
|
||||
path: PathBuf::from(name),
|
||||
extension: None,
|
||||
metadata,
|
||||
}),
|
||||
true => Entry::Directory(Directory {
|
||||
name: name.to_string(),
|
||||
path: PathBuf::from(name),
|
||||
metadata,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user