use std::io::{self, Write};
use std::path::PathBuf;
use std::vec::IntoIter as VecIntoIter;
use log::error;
use nu_ansi_term::Style;
use crate::fs::dir_action::RecurseOptions;
use crate::fs::feature::VcsCache;
use crate::fs::feature::xattr::{Attribute, FileAttributes};
use crate::fs::filter::FileFilter;
use crate::fs::{Dir, File};
use crate::output::cell::TextCell;
use crate::output::file_name::Options as FileStyle;
use crate::output::table::{Options as TableOptions, Row as TableRow, Table};
use crate::output::tree::{TreeDepth, TreeParams, TreeTrunk};
use crate::theme::Theme;
#[derive(PartialEq, Eq, Debug)]
pub struct Options {
pub table: Option<TableOptions>,
pub header: bool,
pub xattr: bool,
pub xattr_indicator: bool,
}
pub struct Render<'a> {
pub dir: Option<&'a Dir>,
pub files: Vec<File<'a>>,
pub theme: &'a Theme,
pub file_style: &'a FileStyle,
pub opts: &'a Options,
pub recurse: Option<RecurseOptions>,
pub filter: &'a FileFilter,
pub vcs_ignoring: bool,
pub vcs: Option<&'a dyn VcsCache>,
}
struct Egg<'a> {
table_row: Option<TableRow>,
xattrs: Vec<Attribute>,
errors: Vec<(io::Error, Option<PathBuf>)>,
dir: Option<Dir>,
file: &'a File<'a>,
}
impl<'a> AsRef<File<'a>> for Egg<'a> {
fn as_ref(&self) -> &File<'a> {
self.file
}
}
impl<'a> Render<'a> {
pub fn render<W: Write>(mut self, w: &mut W) -> io::Result<(usize, u64)> {
let mut rows = Vec::new();
if let Some(ref table) = self.opts.table {
match (self.vcs, self.dir) {
(Some(g), Some(d)) => {
if !g.has_anything_for(&d.path) {
self.vcs = None;
}
}
(Some(g), None) => {
if !self.files.iter().any(|f| g.has_anything_for(&f.path)) {
self.vcs = None;
}
}
(None, _) => { }
}
let mut table = Table::new(table, self.vcs, self.theme);
if self.opts.header {
let header = table.header_row();
table.add_widths(&header);
rows.push(self.render_header(header));
}
let mut table = Some(table);
let rows_before = rows.len();
let mut size_total = 0u64;
self.add_files_to_table(
&mut table,
&mut rows,
&self.files,
TreeDepth::root(),
None,
&mut size_total,
);
let file_count = rows.len() - rows_before;
for row in self.iterate_with_table(table.unwrap(), rows) {
writeln!(w, "{}", row.strings())?;
}
Ok((file_count, size_total))
} else {
let mut size_total = 0u64;
self.add_files_to_table(
&mut None,
&mut rows,
&self.files,
TreeDepth::root(),
None,
&mut size_total,
);
let file_count = rows.len();
for row in self.iterate(rows) {
writeln!(w, "{}", row.strings())?;
}
Ok((file_count, size_total))
}
}
fn add_files_to_table<'dir>(
&self,
table: &mut Option<Table<'a>>,
rows: &mut Vec<Row>,
src: &[File<'dir>],
depth: TreeDepth,
root_dev: Option<u64>,
size_total: &mut u64,
) {
use super::table::Column;
use crate::fs::feature::xattr;
let total_size_active = table
.as_ref()
.is_some_and(super::table::Table::total_size_active);
if total_size_active && self.recurse.is_some() {
use rayon::prelude::*;
src.par_iter().for_each(|f| {
if f.is_directory() {
let _ = f.total_size();
}
});
}
let needs_xattr_full = self.opts.xattr;
let needs_xattr_probe = !needs_xattr_full
&& self.opts.xattr_indicator
&& self
.opts
.table
.as_ref()
.is_some_and(|t| t.columns.contains(&Column::Permissions));
let mut file_eggs = src
.iter()
.map(|file| {
let mut errors = Vec::new();
let mut xattrs = Vec::new();
let mut has_xattrs = false;
if xattr::ENABLED && needs_xattr_full {
match file.path.attributes() {
Ok(xs) => {
has_xattrs = !xs.is_empty();
xattrs = xs;
}
Err(e) => {
errors.push((e, None));
}
}
} else if xattr::ENABLED && needs_xattr_probe {
match file.path.has_attributes() {
Ok(h) => {
has_xattrs = h;
}
Err(e) => {
error!("Error probing xattr for {}: {e}", file.path.display());
}
}
}
let table_row = table.as_ref().map(|t| t.row_for_file(file, has_xattrs));
let mut dir = None;
let entry_is_dir =
file.is_directory() || (depth.0 == 0 && file.points_to_directory());
if let Some(r) = self.recurse
&& entry_is_dir
&& r.tree
&& !r.is_too_deep(depth.0)
&& !self.filter.is_pruned(file)
&& self.allow_descent(file, depth, root_dev)
{
match file.to_dir() {
Ok(d) => {
dir = Some(d);
}
Err(e) => {
errors.push((e, None));
}
}
}
Egg {
table_row,
xattrs,
errors,
dir,
file,
}
})
.collect::<Vec<_>>();
self.filter.sort_files(&mut file_eggs, self.vcs);
for (tree_params, egg) in depth.iterate_over(file_eggs.into_iter()) {
let mut files = Vec::new();
let mut errors = egg.errors;
if let (Some(ref mut t), Some(row)) = (table.as_mut(), egg.table_row.as_ref()) {
t.add_widths(row);
}
let file_name = self
.file_style
.for_file(egg.file, self.theme)
.with_link_paths()
.paint()
.promote();
let row = Row {
tree: tree_params,
cells: egg.table_row,
name: file_name,
};
rows.push(row);
if egg.file.is_directory() {
if egg.dir.is_none() {
let total_size_active = table
.as_ref()
.is_some_and(super::table::Table::total_size_active);
if total_size_active
&& let crate::fs::fields::Size::Some(s) = egg.file.total_size()
{
*size_total += s;
}
}
} else if egg.file.is_file() {
*size_total += egg.file.metadata().len();
}
if let Some(ref dir) = egg.dir {
for file_to_add in dir.files(self.filter.dot_filter, self.vcs, self.vcs_ignoring) {
match file_to_add {
Ok(f) => {
files.push(f);
}
Err((path, e)) => {
errors.push((e, Some(path)));
}
}
}
self.filter.filter_child_files(&mut files);
if !files.is_empty() {
for xattr in egg.xattrs {
rows.push(
self.render_xattr(&xattr, TreeParams::new(depth.deeper(), false)),
);
}
for (error, path) in errors {
rows.push(self.render_error(
&error,
TreeParams::new(depth.deeper(), false),
path,
));
}
let subtree_root_dev = if depth.0 == 0 {
use std::os::unix::fs::MetadataExt;
Some(egg.file.metadata().dev())
} else {
root_dev
};
self.add_files_to_table(
table,
rows,
&files,
depth.deeper(),
subtree_root_dev,
size_total,
);
continue;
}
}
let count = egg.xattrs.len();
for (index, xattr) in egg.xattrs.into_iter().enumerate() {
let params =
TreeParams::new(depth.deeper(), errors.is_empty() && index == count - 1);
let r = self.render_xattr(&xattr, params);
rows.push(r);
}
let count = errors.len();
for (index, (error, path)) in errors.into_iter().enumerate() {
let params = TreeParams::new(depth.deeper(), index == count - 1);
let r = self.render_error(&error, params, path);
rows.push(r);
}
}
}
pub fn render_header(&self, header: TableRow) -> Row {
Row {
tree: TreeParams::new(TreeDepth::root(), false),
cells: Some(header),
name: TextCell::paint_str(self.theme.ui.header, "Name"),
}
}
fn allow_descent(&self, file: &File<'_>, depth: TreeDepth, root_dev: Option<u64>) -> bool {
use crate::fs::dir_action::Filesystem;
use crate::fs::feature::filesystem::is_network_fs;
use std::os::unix::fs::MetadataExt;
let Some(r) = self.recurse else { return true };
if r.filesystem == Filesystem::All {
return true;
}
if depth.0 == 0 {
return true;
}
let Some(root) = root_dev else {
return true;
};
if file.metadata().dev() == root {
return true;
}
match r.filesystem {
Filesystem::Same => false,
Filesystem::Local => !is_network_fs(&file.path),
Filesystem::All => true, }
}
fn render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row {
let error_message = if let Some(path) = path {
format!("<{}: {}>", path.display(), error)
} else {
format!("<{error}>")
};
let name = TextCell::paint(self.theme.ui.broken_symlink, error_message);
Row {
cells: None,
name,
tree,
}
}
fn render_xattr(&self, xattr: &Attribute, tree: TreeParams) -> Row {
let name = TextCell::paint(
self.theme.ui.perms.attribute,
format!("{} (len {})", xattr.name, xattr.size),
);
Row {
cells: None,
name,
tree,
}
}
pub fn render_file(&self, cells: TableRow, name: TextCell, tree: TreeParams) -> Row {
Row {
cells: Some(cells),
name,
tree,
}
}
pub fn iterate_with_table(&'a self, table: Table<'a>, rows: Vec<Row>) -> TableIter<'a> {
TableIter {
tree_trunk: TreeTrunk::default(),
total_width: table.widths().total(),
table,
inner: rows.into_iter(),
tree_style: self.theme.ui.punctuation,
}
}
pub fn iterate(&'a self, rows: Vec<Row>) -> Iter {
Iter {
tree_trunk: TreeTrunk::default(),
inner: rows.into_iter(),
tree_style: self.theme.ui.punctuation,
}
}
}
pub struct Row {
pub cells: Option<TableRow>,
pub name: TextCell,
pub tree: TreeParams,
}
pub struct TableIter<'a> {
inner: VecIntoIter<Row>,
table: Table<'a>,
total_width: usize,
tree_style: Style,
tree_trunk: TreeTrunk,
}
impl Iterator for TableIter<'_> {
type Item = TextCell;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|row| {
let mut cell = if let Some(cells) = row.cells {
self.table.render(cells)
} else {
let mut cell = TextCell::default();
cell.add_spaces(self.total_width);
cell
};
for tree_part in self.tree_trunk.new_row(row.tree) {
cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
}
if !row.tree.is_at_root() {
cell.add_spaces(1);
}
cell.append(row.name);
cell
})
}
}
pub struct Iter {
tree_trunk: TreeTrunk,
tree_style: Style,
inner: VecIntoIter<Row>,
}
impl Iterator for Iter {
type Item = TextCell;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|row| {
let mut cell = TextCell::default();
for tree_part in self.tree_trunk.new_row(row.tree) {
cell.push(self.tree_style.paint(tree_part.ascii_art()), 4);
}
if !row.tree.is_at_root() {
cell.add_spaces(1);
}
cell.append(row.name);
cell
})
}
}