use std::cmp::Ordering;
use std::iter::FromIterator;
use crate::fs::DotFilter;
use crate::fs::File;
#[derive(PartialEq, Eq, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct FileFilter {
pub group_dirs: GroupDirs,
pub sort_field: SortField,
pub sort_by_total_size: bool,
pub reverse: bool,
pub only_dirs: bool,
pub only_files: bool,
pub dot_filter: DotFilter,
pub ignore_patterns: IgnorePatterns,
pub prune_patterns: IgnorePatterns,
pub vcs_ignore: VcsIgnore,
pub symlink_mode: SymlinkMode,
}
#[derive(PartialEq, Eq, Debug, Copy, Clone, Default)]
pub enum SymlinkMode {
#[default]
Show,
Hide,
Follow,
}
impl FileFilter {
pub fn is_pruned(&self, file: &File<'_>) -> bool {
file.is_directory() && self.prune_patterns.is_matched(&file.name)
}
pub fn filter_child_files(&self, files: &mut Vec<File<'_>>) {
files.retain(|f| !self.ignore_patterns.is_matched(&f.name));
match self.symlink_mode {
SymlinkMode::Hide => {
files.retain(|f| !f.is_link());
}
SymlinkMode::Follow => {
for f in files.iter_mut() {
f.deref_link();
}
}
SymlinkMode::Show => {}
}
if self.only_dirs {
files.retain(File::is_directory);
}
if self.only_files {
files.retain(|f| !f.is_directory());
}
}
pub fn filter_argument_files(&self, files: &mut Vec<File<'_>>) {
files.retain(|f| !self.ignore_patterns.is_matched(&f.name));
if self.symlink_mode == SymlinkMode::Follow {
for f in files.iter_mut() {
f.deref_link();
}
}
if self.only_dirs {
files.retain(File::is_directory);
}
if self.only_files {
files.retain(|f| !f.is_directory());
}
}
pub fn sort_files<'a, F>(&self, files: &mut [F], vcs: Option<&dyn crate::fs::feature::VcsCache>)
where
F: AsRef<File<'a>>,
{
use crate::fs::sort_registry::SortFieldDef;
let def = SortFieldDef::for_field(self.sort_field);
if self.sort_by_total_size && self.sort_field == SortField::Size {
use crate::fs::fields::Size;
files.sort_by(|a, b| {
let sa = match a.as_ref().total_size() {
Size::Some(s) => s,
_ => 0,
};
let sb = match b.as_ref().total_size() {
Size::Some(s) => s,
_ => 0,
};
sa.cmp(&sb)
});
} else if def.needs_vcs && vcs.is_some() {
let cache = vcs.unwrap();
files.sort_by(|a, b| {
let sa = cache.get(&a.as_ref().path, a.as_ref().is_directory());
let sb = cache.get(&b.as_ref().path, b.as_ref().is_directory());
Self::vcs_sort_key(sa.unstaged)
.cmp(&Self::vcs_sort_key(sb.unstaged))
.then_with(|| natord::compare(&a.as_ref().name, &b.as_ref().name))
});
} else {
files.sort_by(|a, b| (def.compare)(a.as_ref(), b.as_ref()));
}
if self.reverse {
files.reverse();
}
match self.group_dirs {
GroupDirs::First => {
files.sort_by(|a, b| {
b.as_ref()
.points_to_directory()
.cmp(&a.as_ref().points_to_directory())
});
}
GroupDirs::Last => {
files.sort_by(|a, b| {
a.as_ref()
.points_to_directory()
.cmp(&b.as_ref().points_to_directory())
});
}
GroupDirs::None => {}
}
}
fn vcs_sort_key(status: crate::fs::fields::VcsStatus) -> u8 {
use crate::fs::fields::VcsStatus::*;
match status {
Conflicted => 0,
Modified => 1,
New => 2,
Deleted => 3,
Renamed => 4,
Copied => 5,
TypeChange => 6,
Untracked => 7,
Ignored => 8,
NotModified => 9,
}
}
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum SortField {
Unsorted,
Name(SortCase),
Extension(SortCase),
Size,
#[cfg(unix)]
FileInode,
ModifiedDate,
AccessedDate,
ChangedDate,
CreatedDate,
FileType,
ModifiedAge,
NameMixHidden(SortCase),
#[cfg(unix)]
Permissions,
#[cfg(unix)]
Blocks,
#[cfg(unix)]
HardLinks,
Flags,
#[cfg(unix)]
User(SortCase),
#[cfg(unix)]
Group(SortCase),
#[cfg(unix)]
Uid,
#[cfg(unix)]
Gid,
VcsStatusSort,
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum SortCase {
ABCabc,
AaBbCc,
}
impl SortField {
pub fn compare_files(self, a: &File<'_>, b: &File<'_>) -> Ordering {
(crate::fs::sort_registry::SortFieldDef::for_field(self).compare)(a, b)
}
}
#[derive(PartialEq, Eq, Default, Debug, Clone)]
pub struct IgnorePatterns {
patterns: Vec<glob::Pattern>,
}
impl FromIterator<glob::Pattern> for IgnorePatterns {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = glob::Pattern>,
{
let patterns = iter.into_iter().collect();
Self { patterns }
}
}
impl IgnorePatterns {
pub fn parse_from_iter<'a, I: IntoIterator<Item = &'a str>>(
iter: I,
) -> (Self, Vec<glob::PatternError>) {
let iter = iter.into_iter();
let mut patterns = match iter.size_hint() {
(_, Some(count)) => Vec::with_capacity(count),
_ => Vec::new(),
};
let mut errors = Vec::new();
for input in iter {
match glob::Pattern::new(input) {
Ok(pat) => patterns.push(pat),
Err(e) => errors.push(e),
}
}
(Self { patterns }, errors)
}
pub fn empty() -> Self {
Self {
patterns: Vec::new(),
}
}
pub fn is_matched(&self, file: &str) -> bool {
self.patterns.iter().any(|p| p.matches(file))
}
}
#[derive(PartialEq, Eq, Debug, Copy, Clone, Default)]
pub enum GroupDirs {
First,
Last,
#[default]
None,
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum VcsIgnore {
CheckAndIgnore,
Off,
}
#[cfg(test)]
mod test_ignores {
use super::*;
#[test]
fn empty_matches_nothing() {
let pats = IgnorePatterns::empty();
assert!(!pats.is_matched("nothing"));
assert!(!pats.is_matched("test.mp3"));
}
#[test]
fn ignores_a_glob() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["*.mp3"]);
assert!(fails.is_empty());
assert!(!pats.is_matched("nothing"));
assert!(pats.is_matched("test.mp3"));
}
#[test]
fn ignores_an_exact_filename() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["nothing"]);
assert!(fails.is_empty());
assert!(pats.is_matched("nothing"));
assert!(!pats.is_matched("test.mp3"));
}
#[test]
fn ignores_both() {
let (pats, fails) = IgnorePatterns::parse_from_iter(vec!["nothing", "*.mp3"]);
assert!(fails.is_empty());
assert!(pats.is_matched("nothing"));
assert!(pats.is_matched("test.mp3"));
}
}