diff --git a/src/file/mod.rs b/src/file/mod.rs index d307022..279b1f4 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -43,10 +43,17 @@ pub struct File { unix_attrs: unix::Attrs, } +/// [`Display`] implementation concerned with human-readable presentation of the file-name. pub struct DisplayName<'a> { file: &'a File, } +/// [`Display`] implementation concerned with human-readable presentation of the file-path. +pub struct DisplayPath<'a> { + file: &'a File, + path_prefix: Option<&'a Path>, +} + impl File { /// Plain Jane constructor for [`File`]. pub fn new( @@ -139,6 +146,10 @@ impl File { DisplayName { file: self } } + pub fn display_path<'a>(&'a self, path_prefix: Option<&'a Path>) -> DisplayPath<'a> { + DisplayPath { file: self, path_prefix } + } + #[cfg(unix)] pub fn unix_attrs(&self) -> &unix::Attrs { &self.unix_attrs @@ -181,11 +192,32 @@ impl Deref for File { impl Display for DisplayName<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let file_name = self.file.file_name().to_string_lossy(); + let link_target = self.file.symlink_target().map(|p| p.canonicalize()); - if let Some(link_target) = self.file.symlink_target() { - write!(f, "{file_name} \u{2192} {}", link_target.display()) + if let Some(Ok(target)) = link_target { + write!(f, "{file_name} \u{2192} {}", target.display()) } else { write!(f, "{file_name}") } } } + +impl Display for DisplayPath<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let display = match self.path_prefix { + Some(prefix) => { + let path = self.file.path(); + path.strip_prefix(prefix).map_or_else(|_| path.display(), |p| p.display()) + }, + None => self.file.path().display() + }; + + let link_target = self.file.symlink_target().map(|p| p.canonicalize()); + + if let Some(Ok(target)) = link_target { + write!(f, "{display} \u{2192} {}", target.display()) + } else { + write!(f, "{display}") + } + } +} diff --git a/src/file/order.rs b/src/file/order.rs index 5004e02..de22546 100644 --- a/src/file/order.rs +++ b/src/file/order.rs @@ -1,5 +1,5 @@ use super::File; -use crate::user::{args::{Sort, DirOrder}, Context}; +use crate::user::args::{DirOrder, Sort}; use std::cmp::Ordering; /// Comparator type used to sort [File]s. @@ -7,23 +7,26 @@ pub type FileComparator = dyn Fn(&File, &File) -> Ordering; /// Yields function pointer to the appropriate `File` comparator. pub fn comparator(sort: Sort, dir_order: DirOrder) -> Option> { - if matches!(sort, Sort::None) { - return None; - } - match dir_order { - DirOrder::First => { - Some(Box::new(move |a, b| dir_first_comparator(a, b, base_comparator(sort)))) - }, - DirOrder::Last => { - Some(Box::new(move |a, b| dir_last_comparator(a, b, base_comparator(sort)))) - }, + DirOrder::First if matches!(sort, Sort::None) => Some(Box::new(move |a, b| { + dir_first_comparator(a, b) + })), + DirOrder::First => Some(Box::new(move |a, b| { + dir_first_comparator_with_fallback(a, b, base_comparator(sort)) + })), + DirOrder::Last if matches!(sort, Sort::None) => Some(Box::new(move |a, b| { + dir_last_comparator(a, b) + })), + DirOrder::Last => Some(Box::new(move |a, b| { + dir_last_comparator_with_fallback(a, b, base_comparator(sort)) + })), + DirOrder::None if matches!(sort, Sort::None) => None, DirOrder::None => Some(base_comparator(sort)), } } /// Orders directories first. Provides a fallback if inputs are not directories. -fn dir_first_comparator( +fn dir_first_comparator_with_fallback( a: &File, b: &File, fallback: impl Fn(&File, &File) -> Ordering, @@ -35,8 +38,17 @@ fn dir_first_comparator( } } +/// Orders directories first relative to all other file-types. +fn dir_first_comparator(a: &File, b: &File) -> Ordering { + match (a.is_dir(), b.is_dir()) { + (true, false) => Ordering::Greater, + (false, true) => Ordering::Less, + _ => Ordering::Equal + } +} + /// Orders directories last. Provides a fallback if inputs are not directories. -fn dir_last_comparator( +fn dir_last_comparator_with_fallback( a: &File, b: &File, fallback: impl Fn(&File, &File) -> Ordering, @@ -48,6 +60,18 @@ fn dir_last_comparator( } } +/// Orders directories last relative to all other file-types. +fn dir_last_comparator( + a: &File, + b: &File, +) -> Ordering { + match (a.is_dir(), b.is_dir()) { + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + _ => Ordering::Equal + } +} + /// Grabs the comparator for two non-dir type [File]s. fn base_comparator(sort_type: Sort) -> Box { Box::new(match sort_type { @@ -61,9 +85,7 @@ fn base_comparator(sort_type: Sort) -> Box { Sort::Rcreate => time_stamping::created::rev_comparator, Sort::Mod => time_stamping::modified::comparator, Sort::Rmod => time_stamping::modified::rev_comparator, - - // Hacky... - Sort::None => unreachable!(), + Sort::None => |_: &File, _: &File| Ordering::Equal, }) } @@ -77,8 +99,14 @@ mod time_stamping { /// Comparator that sorts [File]s by Last Access timestamp, newer to older. pub fn comparator(a: &File, b: &File) -> Ordering { - let a_stamp = a.metadata().accessed().unwrap_or_else(|_| SystemTime::now()); - let b_stamp = b.metadata().accessed().unwrap_or_else(|_| SystemTime::now()); + let a_stamp = a + .metadata() + .accessed() + .unwrap_or_else(|_| SystemTime::now()); + let b_stamp = b + .metadata() + .accessed() + .unwrap_or_else(|_| SystemTime::now()); b_stamp.cmp(&a_stamp) } @@ -109,8 +137,14 @@ mod time_stamping { /// Comparator that sorts [File]s by Alteration timestamp, newer to older. pub fn comparator(a: &File, b: &File) -> Ordering { - let a_stamp = a.metadata().modified().unwrap_or_else(|_| SystemTime::now()); - let b_stamp = b.metadata().modified().unwrap_or_else(|_| SystemTime::now()); + let a_stamp = a + .metadata() + .modified() + .unwrap_or_else(|_| SystemTime::now()); + let b_stamp = b + .metadata() + .modified() + .unwrap_or_else(|_| SystemTime::now()); b_stamp.cmp(&a_stamp) } diff --git a/src/file/tree/mod.rs b/src/file/tree/mod.rs index f296fe6..aaf2912 100644 --- a/src/file/tree/mod.rs +++ b/src/file/tree/mod.rs @@ -1,9 +1,9 @@ +use super::order::{self, FileComparator}; use crate::{ error::prelude::*, file::File, - user::{args::Sort, column, Context}, + user::{args::{Layout, SortType, Sort}, column, Context}, }; -use super::order::{self, FileComparator}; use ahash::{HashMap, HashSet}; use indextree::{Arena, NodeId}; use std::{ops::Deref, path::PathBuf}; @@ -108,23 +108,63 @@ impl Tree { } } - for (dir_id, dirsize) in dirsize_map.into_iter() { - let dir = arena[dir_id].get_mut(); - *dir.size_mut() += dirsize; - - if let Some(dirents) = branches.remove(dir.path()) { - for dirent_id in dirents { - dir_id.append(dirent_id, &mut arena); + match order::comparator(ctx.sort, ctx.dir_order) { + Some(comparator) => match ctx.sort_type { + SortType::Flat if matches!(ctx.layout, Layout::Flat) => { + for (dir_id, dirsize) in dirsize_map.into_iter() { + let dir = arena[dir_id].get_mut(); + *dir.size_mut() += dirsize; + } + + let mut all_dirents = branches + .values() + .flatten() + .filter_map(|n| (*n != root_id).then_some(*n)) + .collect::>(); + + all_dirents.sort_by(|id_a, id_b| { + let node_a = arena[*id_a].get(); + let node_b = arena[*id_b].get(); + comparator(node_a, node_b) + }); + + all_dirents.into_iter().for_each(|n| root_id.append(n, &mut arena)); } - } + _ => { + for (dir_id, dirsize) in dirsize_map.into_iter() { + let dir = arena[dir_id].get_mut(); + *dir.size_mut() += dirsize; + + if let Some(mut dirents) = branches.remove(dir.path()) { + dirents.sort_by(|id_a, id_b| { + let node_a = arena[*id_a].get(); + let node_b = arena[*id_b].get(); + comparator(node_a, node_b) + }); + + for dirent_id in dirents { + dir_id.append(dirent_id, &mut arena); + } + } + } + }, + }, + None => { + for (dir_id, dirsize) in dirsize_map.into_iter() { + let dir = arena[dir_id].get_mut(); + *dir.size_mut() += dirsize; + + if let Some(dirents) = branches.remove(dir.path()) { + for dirent_id in dirents { + dir_id.append(dirent_id, &mut arena); + } + } + } + }, } column_metadata.update_size_width(arena[root_id].get(), ctx); - //if let Some(comparator) = order::comparator(ctx.sort, ctx.dir_order) { - //Self::tree_sort(root_id, &mut arena, comparator); - //} - let tree = Self { root_id, arena }; Ok((tree, column_metadata)) @@ -236,9 +276,7 @@ impl Tree { let to_prune = self .root_id .descendants(&self.arena) - .filter(|n| { - self.arena[*n].get().is_dir() && n.children(&self.arena).count() == 0 - }) + .filter(|n| self.arena[*n].get().is_dir() && n.children(&self.arena).count() == 0) .collect::>(); to_prune @@ -258,7 +296,6 @@ impl Tree { pub fn tree_sort(root_id: NodeId, arena: &mut Arena, comparator: Box) { todo!() } - } impl Deref for Tree { diff --git a/src/render/mod.rs b/src/render/mod.rs index 8b66539..7345d84 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -26,13 +26,13 @@ mod row; pub fn output(file_tree: &file::Tree, ctx: &Context) -> Result { match ctx.layout { - Layout::Regular => tree(file_tree, ctx), - Layout::Inverted => inverted_tree(file_tree, ctx), + Layout::Tree => tree(file_tree, ctx), + Layout::InvertedTree => inverted_tree(file_tree, ctx), Layout::Flat => flat(file_tree, ctx), } } -fn tree(file_tree: &file::Tree, ctx: &Context) -> Result { +fn inverted_tree(file_tree: &file::Tree, ctx: &Context) -> Result { let arena = file_tree.arena(); let root = file_tree.root_id(); let max_depth = ctx.level(); @@ -47,7 +47,7 @@ fn tree(file_tree: &file::Tree, ctx: &Context) -> Result { let mut inherited_prefix_components = vec![""]; - let mut formatter = row::formatter(&mut buf, ctx); + let mut formatter = row::formatter(&mut buf, ctx)?; let mut reverse_traverse = root.reverse_traverse(arena); reverse_traverse.next(); @@ -105,7 +105,7 @@ fn tree(file_tree: &file::Tree, ctx: &Context) -> Result { Ok(buf) } -pub fn inverted_tree(file_tree: &file::Tree, ctx: &Context) -> Result { +pub fn tree(file_tree: &file::Tree, ctx: &Context) -> Result { let arena = file_tree.arena(); let root = file_tree.root_id(); let max_depth = ctx.level(); @@ -120,7 +120,7 @@ pub fn inverted_tree(file_tree: &file::Tree, ctx: &Context) -> Result { let mut inherited_prefix_components = vec![""]; - let mut formatter = row::formatter(&mut buf, ctx); + let mut formatter = row::formatter(&mut buf, ctx)?; let mut traverse = root.traverse(arena); traverse.next(); @@ -188,7 +188,7 @@ fn flat(file_tree: &file::Tree, ctx: &Context) -> Result { let max_depth = ctx.level(); let mut buf = String::new(); - let mut formatter = row::formatter(&mut buf, ctx); + let mut formatter = row::formatter(&mut buf, ctx)?; for node_edge in root.traverse(arena) { let node_id = match node_edge { diff --git a/src/render/row/mod.rs b/src/render/row/mod.rs index 0be6391..f392340 100644 --- a/src/render/row/mod.rs +++ b/src/render/row/mod.rs @@ -1,6 +1,7 @@ use crate::{ + error::prelude::*, file::File, - user::{column, Context}, + user::{args::Layout, column, Context}, }; use std::fmt::{self, Write}; @@ -10,51 +11,112 @@ mod long; pub type RowFormatter<'a> = Box fmt::Result + 'a>; -#[cfg(windows)] -pub fn formatter<'a>(buf: &'a mut String, ctx: &'a Context) -> RowFormatter<'a> { - if ctx.suppress_size { - Box::new(|file, prefix| { - let name = file.display_name(); - writeln!(buf, "{prefix}{name}") - }) - } else { - Box::new(|file, prefix| { - let size = format!("{}", file.size()); - let name = file.display_name(); - let column::Metadata { max_size_width, .. } = ctx.column_metadata; - writeln!(buf, "{size:>max_size_width$} {prefix}{name}") - }) +#[cfg(unix)] +pub fn formatter<'a>(buf: &'a mut String, ctx: &'a Context) -> Result> { + match ctx.layout { + Layout::Flat => { + let root = ctx.dir_canonical()?; + + match (ctx.long, ctx.suppress_size) { + (false, false) => Ok(Box::new(move |file, prefix| { + let size = format!("{}", file.size()); + let base = root.ancestors().nth(1); + let name = file.display_path(base); + let column::Metadata { max_size_width, .. } = ctx.column_metadata; + writeln!(buf, "{size:>max_size_width$} {prefix}{name}") + })), + + (false, true) => Ok(Box::new(move |file, prefix| { + let base = root.ancestors().nth(1); + let name = file.display_path(base); + writeln!(buf, "{prefix}{name}") + })), + + (true, false) => Ok(Box::new(move |file, prefix| { + let size = format!("{}", file.size()); + let base = root.ancestors().nth(1); + let name = file.display_path(base); + let col_widths = ctx.column_metadata; + let column::Metadata { max_size_width, .. } = col_widths; + let long_format = long::Format::new(file, ctx); + writeln!(buf, "{long_format} {size:>max_size_width$} {prefix}{name}") + })), + + (true, true) => Ok(Box::new(move |file, prefix| { + let base = root.ancestors().nth(1); + let name = file.display_path(base); + let long_format = long::Format::new(file, ctx); + writeln!(buf, "{long_format} {prefix}{name}") + })), + } + }, + _ => match (ctx.long, ctx.suppress_size) { + (false, false) => Ok(Box::new(|file, prefix| { + let size = format!("{}", file.size()); + let name = file.display_name(); + let column::Metadata { max_size_width, .. } = ctx.column_metadata; + writeln!(buf, "{size:>max_size_width$} {prefix}{name}") + })), + + (false, true) => Ok(Box::new(|file, prefix| { + let name = file.display_name(); + writeln!(buf, "{prefix}{name}") + })), + + (true, false) => Ok(Box::new(|file, prefix| { + let size = format!("{}", file.size()); + let name = file.display_name(); + let col_widths = ctx.column_metadata; + let column::Metadata { max_size_width, .. } = col_widths; + let long_format = long::Format::new(file, ctx); + writeln!(buf, "{long_format} {size:>max_size_width$} {prefix}{name}") + })), + + (true, true) => Ok(Box::new(|file, prefix| { + let name = file.display_name(); + let long_format = long::Format::new(file, ctx); + writeln!(buf, "{long_format} {prefix}{name}") + })), + } } } -#[cfg(unix)] -pub fn formatter<'a>(buf: &'a mut String, ctx: &'a Context) -> RowFormatter<'a> { - match (ctx.long, ctx.suppress_size) { - (false, false) => Box::new(|file, prefix| { - let size = format!("{}", file.size()); - let name = file.display_name(); - let column::Metadata { max_size_width, .. } = ctx.column_metadata; - writeln!(buf, "{size:>max_size_width$} {prefix}{name}") - }), - - (false, true) => Box::new(|file, prefix| { - let name = file.display_name(); - writeln!(buf, "{prefix}{name}") - }), - - (true, false) => Box::new(|file, prefix| { - let size = format!("{}", file.size()); - let name = file.display_name(); - let col_widths = ctx.column_metadata; - let column::Metadata { max_size_width, .. } = col_widths; - let long_format = long::Format::new(file, ctx); - writeln!(buf, "{long_format} {size:>max_size_width$} {prefix}{name}") - }), - - (true, true) => Box::new(|file, prefix| { - let name = file.display_name(); - let long_format = long::Format::new(file, ctx); - writeln!(buf, "{long_format} {prefix}{name}") - }) +#[cfg(windows)] +pub fn formatter<'a>(buf: &'a mut String, ctx: &'a Context) -> Result> { + match ctx.layout { + Layout::Flat => { + let root = ctx.dir_canonical()?; + + if ctx.suppress_size { + Ok(Box::new(move |file, prefix| { + let base = root.ancestors().nth(1); + let name = file.display_path(base); + writeln!(buf, "{prefix}{name}") + })) + } else { + Ok(Box::new(move |file, prefix| { + let size = format!("{}", file.size()); + let base = root.ancestors().nth(1); + let name = file.display_path(base); + let column::Metadata { max_size_width, .. } = ctx.column_metadata; + writeln!(buf, "{size:>max_size_width$} {prefix}{name}") + })) + } + }, + _ => { + if ctx.suppress_size { + Ok(Box::new(|file, prefix| { + let name = file.display_name(); + writeln!(buf, "{prefix}{name}") + })) + } else { + Ok(Box::new(|file, prefix| { + let size = format!("{}", file.size()); + let name = file.display_name(); + let column::Metadata { max_size_width, .. } = ctx.column_metadata; + writeln!(buf, "{size:>max_size_width$} {prefix}{name}") + })) + } + } } } diff --git a/src/user/args.rs b/src/user/args.rs index d4753bb..4484be4 100644 --- a/src/user/args.rs +++ b/src/user/args.rs @@ -71,22 +71,21 @@ pub enum TimeFormat { Default, } -/// Which layout to use when rendering the tree. +/// Which layout to use when rendering the file-tree #[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum Layout { /// Outputs the tree with the root node at the bottom of the output #[default] - Regular, + InvertedTree, /// Outputs the tree with the root node at the top of the output - Inverted, + Tree, /// Outputs a flat layout using paths rather than an ASCII tree Flat, } -/// Order in which to print entries relative to their siblings (tree layouts) or all others (flat -/// layout). +/// Which field to sort directory entries by #[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] pub enum Sort { /// No ordering. @@ -129,6 +128,16 @@ pub enum Sort { Rmod, } +/// Whether to sort directory entries relative either to their siblings or all directory entries +#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] +pub enum SortType { + /// Sort directory entries relative to their siblings + #[default] + Tree, + /// Sort directory entries relative to all directory entries + Flat +} + /// How directories should be ordered relative to regular files. #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, Default)] pub enum DirOrder { diff --git a/src/user/mod.rs b/src/user/mod.rs index e88ad89..86938ad 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -89,10 +89,14 @@ pub struct Context { #[arg(short, long)] pub prune: bool, - /// How to sort entries + /// Field whereby to sort entries #[arg(short, long, value_enum, default_value_t)] pub sort: args::Sort, + /// Sort entries relative either to their siblings or all other entries + #[arg(long, value_enum, default_value_t)] + pub sort_type: args::SortType, + /// Sort directories before or after all other file types #[arg(short, long, value_enum, default_value_t)] pub dir_order: args::DirOrder,