use std::{
collections::HashSet,
fs::{DirEntry, read_dir},
io::{BufWriter, Result, Write, stdout},
os::unix::fs::PermissionsExt,
path::Path,
time::SystemTime,
};
use crate::cmd::{
display::{format_date, format_entry_line, format_permissions},
root::Opts,
};
struct EntryInfo {
entry: DirEntry,
last_modify: Result<SystemTime>,
}
fn check_valid_entry(path: &Path, name: Option<&str>, opts: &Opts, depth: usize) -> bool {
let is_hidden = name.map(|name| name.starts_with('.')).unwrap_or(false);
if !opts.show_hidden && is_hidden {
return false;
}
if opts.dir_only && !path.is_dir() {
return false;
}
if let Some(max_level) = opts.level
&& depth > max_level as usize
{
return false;
}
for exclude_pattern in opts.exclude_patterns.iter() {
if name.is_some_and(|name| exclude_pattern.matches(name)) {
return false;
}
}
true
}
fn pre_process_tree(
root: &DirEntry,
opts: &Opts,
depth: usize,
has_ancestors_matched: bool,
display_entries: &mut HashSet<String>,
highlight_entries: &mut HashSet<String>,
) -> bool {
let path = root.path();
let name = path.file_name().and_then(|name| name.to_str());
if !check_valid_entry(&path, name, opts, depth) {
return false;
}
let mut should_display = true;
let mut this_dir_matches = has_ancestors_matched;
if !opts.patterns.is_empty() {
for pattern in opts.patterns.iter() {
if name.is_some_and(|name| pattern.matches(name)) {
this_dir_matches = true;
if opts.highlight {
highlight_entries.insert(path.display().to_string());
}
break;
}
}
if !this_dir_matches {
should_display = has_ancestors_matched;
}
}
if path.is_dir()
&& let read_dir = read_dir(&path)
&& let Ok(reader) = read_dir
{
reader.filter_map(Result::ok).for_each(|dir| {
should_display |= pre_process_tree(
&dir,
opts,
depth + 1,
this_dir_matches,
display_entries,
highlight_entries,
);
});
}
if should_display && !opts.patterns.is_empty() {
display_entries.insert(path.display().to_string());
}
should_display
}
#[allow(clippy::too_many_arguments)]
fn traverse_directory(
writer: &mut dyn Write,
path: &Path,
opts: &Opts,
display_entries: &HashSet<String>,
highlight_entries: &HashSet<String>,
depth: usize,
first_matched_ancestor: usize,
stats: &mut (u64, u64),
indent_state: &[bool],
) -> Result<()> {
let mut entries_info: Vec<EntryInfo> = read_dir(path)?
.filter_map(Result::ok)
.filter(|entry| {
if opts.patterns.is_empty() {
let path = entry.path();
let name = path.file_name().and_then(|name| name.to_str());
check_valid_entry(&path, name, opts, depth + 1)
} else {
display_entries.contains(&entry.path().display().to_string())
}
})
.map(|entry| {
let last_modify = entry.metadata().and_then(|m| m.modified());
if let Err(e) = &last_modify {
eprintln!(
"Warning: Could not get metadata/last_modify for {:?}: {}",
entry.path(),
e
);
}
EntryInfo { entry, last_modify }
})
.collect();
let (mut dirs, mut files): (Vec<EntryInfo>, Vec<EntryInfo>) = std::mem::take(&mut entries_info)
.into_iter()
.partition(|info| {
info.entry
.file_type()
.map(|ft| ft.is_dir())
.unwrap_or(false)
});
let sort_comparison = |a: &EntryInfo, b: &EntryInfo| {
if opts.sort_by_time {
let time_a = a.last_modify.as_ref().unwrap_or(&SystemTime::UNIX_EPOCH);
let time_b = b.last_modify.as_ref().unwrap_or(&SystemTime::UNIX_EPOCH);
time_a
.cmp(time_b)
.then_with(|| a.entry.file_name().cmp(&b.entry.file_name()))
} else {
a.entry.file_name().cmp(&b.entry.file_name())
}
};
dirs.sort_unstable_by(sort_comparison);
files.sort_unstable_by(sort_comparison);
entries_info.append(&mut dirs);
entries_info.append(&mut files);
let last_idx = entries_info.len().saturating_sub(1);
for (idx, info) in entries_info.into_iter().enumerate() {
let entry = info.entry;
let path = entry.path();
let is_last_entry = idx == last_idx;
let should_highlight = highlight_entries.contains(&path.display().to_string());
let first_matched_ancestor = if should_highlight {
first_matched_ancestor.min(depth)
} else {
first_matched_ancestor
};
let line = format_entry_line(
&entry,
opts,
indent_state,
is_last_entry,
should_highlight,
first_matched_ancestor,
)?;
writeln!(writer, "{line}")?;
if entry.file_type()?.is_dir() {
stats.0 += 1;
let mut next_indent_state = indent_state.to_vec();
next_indent_state.push(is_last_entry);
traverse_directory(
writer,
&path,
opts,
display_entries,
highlight_entries,
depth + 1,
first_matched_ancestor,
stats,
&next_indent_state,
)?;
} else {
stats.1 += 1;
}
}
Ok(())
}
pub fn print_tree(path: &Path, opts: &Opts) -> Result<()> {
let mut writer = Box::new(BufWriter::new(stdout()));
print_tree_with_writer(path, opts, &mut writer)
}
pub fn print_tree_with_writer(path: &Path, opts: &Opts, writer: &mut dyn Write) -> Result<()> {
let metadata = std::fs::metadata(path)?;
let mut display_path = String::new();
if opts.print_permissions {
let mode = metadata.permissions().mode();
let perms_str = format_permissions(mode, metadata.file_type().is_dir());
display_path.push_str(&perms_str);
display_path.push(' ');
}
if opts.last_modify {
match metadata.modified() {
Ok(mod_time) => {
let date_str = format!("[{}] ", format_date(mod_time));
display_path.push_str(&date_str);
}
Err(e) => {
eprintln!(
"Warning: Could not get modification date for {:?}: {}",
path, e
);
}
}
}
if opts.full_path {
display_path.push_str(&path.canonicalize()?.display().to_string());
} else {
display_path.push_str(
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("."),
);
};
writeln!(writer, "{display_path}")?;
let mut display_entries = HashSet::new();
let mut highlight_entries = HashSet::new();
if !opts.patterns.is_empty() {
match read_dir(path) {
Ok(reader) => reader,
Err(e) => {
eprintln!("Error reading directory {path:?}: {e}");
return Err(e);
}
}
.filter_map(Result::ok)
.for_each(|entry| {
pre_process_tree(
&entry,
opts,
1,
false,
&mut display_entries,
&mut highlight_entries,
);
});
}
let mut stats = (0, 0);
traverse_directory(
writer,
path,
opts,
&display_entries,
&highlight_entries,
0,
usize::MAX,
&mut stats,
&[],
)?;
let dir_str = if stats.0 == 1 {
"directory"
} else {
"directories"
};
let file_str = if stats.1 == 1 { "file" } else { "files" };
writeln!(
writer,
"\n{} {}, {} {}",
stats.0, dir_str, stats.1, file_str
)?;
Ok(())
}