use std::io::{self, Write};
use std::path::PathBuf;
use std::vec::IntoIter as VecIntoIter;
use nu_ansi_term::Style;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use log::{debug, trace};
use crate::fs::dir_action::RecurseOptions;
use crate::fs::feature::git::GitCache;
use crate::fs::feature::xattr::Attribute;
use crate::fs::fields::SecurityContextType;
use crate::fs::filter::FileFilter;
use crate::fs::{Dir, File};
use crate::output::cell::TextCell;
use crate::output::color_scale::{ColorScaleInformation, ColorScaleOptions};
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;
#[allow(clippy::struct_excessive_bools)]
#[derive(PartialEq, Eq, Debug)]
pub struct Options {
pub table: Option<TableOptions>,
pub header: bool,
pub xattr: bool,
pub secattr: bool,
pub mounts: bool,
pub color_scale: ColorScaleOptions,
pub follow_links: 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 git_ignoring: bool,
pub git: Option<&'a GitCache>,
pub git_repos: bool,
}
#[rustfmt::skip]
struct Egg<'a> {
table_row: Option<TableRow>,
xattrs: &'a [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<()> {
let mut rows = Vec::new();
let color_scale_info = ColorScaleInformation::from_color_scale(
self.opts.color_scale,
&self.files,
self.filter.dot_filter,
self.git,
self.git_ignoring,
self.recurse,
);
if let Some(ref table) = self.opts.table {
match (self.git, self.dir) {
(Some(g), Some(d)) => {
if !g.has_anything_for(&d.path) {
self.git = None;
}
}
(Some(g), None) => {
if !self.files.iter().any(|f| g.has_anything_for(&f.path)) {
self.git = None;
}
}
(None, _) => { }
}
let mut table = Table::new(table, self.git, self.theme, self.git_repos);
if self.opts.header {
let header = table.header_row();
table.add_widths(&header);
rows.push(self.render_header(header));
}
let mut table = Some(table);
self.add_files_to_table(
&mut table,
&mut rows,
&self.files,
TreeDepth::root(),
color_scale_info,
);
for row in self.iterate_with_table(table.unwrap(), rows) {
writeln!(w, "{}", row.strings())?;
}
} else {
self.add_files_to_table(
&mut None,
&mut rows,
&self.files,
TreeDepth::root(),
color_scale_info,
);
for row in self.iterate(rows) {
writeln!(w, "{}", row.strings())?;
}
}
Ok(())
}
pub fn show_xattr_hint(&self, file: &File<'_>) -> bool {
let xattr_count = file.extended_attributes().len();
let selinux_ctx_shown = self.opts.secattr
&& match file.security_context().context {
SecurityContextType::SELinux(_) => true,
SecurityContextType::None => false,
};
xattr_count > 1 || (xattr_count == 1 && !selinux_ctx_shown)
}
fn add_files_to_table<'dir>(
&self,
table: &mut Option<Table<'a>>,
rows: &mut Vec<Row>,
src: &[File<'dir>],
depth: TreeDepth,
color_scale_info: Option<ColorScaleInformation>,
) {
use crate::fs::feature::xattr;
let mut file_eggs: Vec<_> = src
.par_iter()
.map(|file| {
let mut errors = Vec::new();
let xattrs: &[Attribute] = if xattr::ENABLED && self.opts.xattr {
file.extended_attributes()
} else {
&[]
};
let table_row = table
.as_ref()
.map(|t| t.row_for_file(file, self.show_xattr_hint(file), color_scale_info));
let mut dir = None;
let follow_links = self.opts.follow_links;
if let Some(r) = self.recurse {
if (if follow_links {
file.points_to_directory()
} else {
file.is_directory()
}) && r.tree
&& !r.is_too_deep(depth.0)
{
trace!("matching on read_dir");
match file.read_dir() {
Ok(d) => {
dir = Some(d);
}
Err(e) => {
errors.push((e, None));
}
}
}
}
Egg {
table_row,
xattrs,
errors,
dir,
file,
}
})
.collect();
self.filter.sort_files(&mut file_eggs);
for (tree_params, egg) in depth.iterate_over(file_eggs.into_iter()) {
let mut files = Vec::new();
let 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()
.with_mount_details(self.opts.mounts)
.paint()
.promote();
debug!("file_name {file_name:?}");
let row = Row {
tree: tree_params,
cells: egg.table_row,
name: file_name,
};
rows.push(row);
if let Some(ref dir) = egg.dir {
for file_to_add in dir.files(
self.filter.dot_filter,
self.git,
self.git_ignoring,
egg.file.deref_links,
egg.file.is_recursive_size(),
) {
files.push(file_to_add);
}
self.filter
.filter_child_files(self.recurse.is_some(), &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,
));
}
self.add_files_to_table(table, rows, &files, depth.deeper(), color_scale_info);
continue;
}
}
let count = egg.xattrs.len();
for (index, xattr) in egg.xattrs.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);
}
}
}
#[must_use]
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.unwrap_or_default(), "Name"),
}
}
fn render_error(&self, error: &io::Error, tree: TreeParams, path: Option<PathBuf>) -> Row {
use crate::output::file_name::Colours;
let error_message = if let Some(path) = path {
format!("<{}: {}>", path.display(), error)
} else {
format!("<{error}>")
};
let name = TextCell::paint(self.theme.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.unwrap_or_default().attribute(),
format!("{xattr}"),
);
Row {
cells: None,
name,
tree,
}
}
#[must_use]
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.unwrap_or_default(),
}
}
#[must_use]
pub fn iterate(&'a self, rows: Vec<Row>) -> Iter {
Iter {
tree_trunk: TreeTrunk::default(),
inner: rows.into_iter(),
tree_style: self.theme.ui.punctuation.unwrap_or_default(),
}
}
}
pub struct Row {
pub cells: Option<TableRow>,
pub name: TextCell,
pub tree: TreeParams,
}
#[rustfmt::skip]
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);
}
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);
}
cell.append(row.name);
cell
})
}
}