diff --git a/src/render/context/color.rs b/src/context/color.rs similarity index 100% rename from src/render/context/color.rs rename to src/context/color.rs diff --git a/src/render/context/config.rs b/src/context/config.rs similarity index 100% rename from src/render/context/config.rs rename to src/context/config.rs diff --git a/src/render/context/dir.rs b/src/context/dir.rs similarity index 100% rename from src/render/context/dir.rs rename to src/context/dir.rs diff --git a/src/render/context/error.rs b/src/context/error.rs similarity index 100% rename from src/render/context/error.rs rename to src/context/error.rs diff --git a/src/render/context/file.rs b/src/context/file.rs similarity index 100% rename from src/render/context/file.rs rename to src/context/file.rs diff --git a/src/context/layout.rs b/src/context/layout.rs new file mode 100644 index 00000000..4c7557b0 --- /dev/null +++ b/src/context/layout.rs @@ -0,0 +1,15 @@ +use clap::ValueEnum; + +/// Which layout to use when rendering the tree. +#[derive(Copy, Clone, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord, Default)] +pub enum Type { + /// Outputs the tree with the root node at the bottom of the output + #[default] + Regular, + + /// Outputs the tree with the root node at the top of the output + Inverted, + + /// Outputs a flat layout using paths rather than an ASCII tree. + Flat, +} diff --git a/src/render/context/mod.rs b/src/context/mod.rs similarity index 98% rename from src/render/context/mod.rs rename to src/context/mod.rs index 2770b189..c7e04801 100644 --- a/src/render/context/mod.rs +++ b/src/context/mod.rs @@ -31,6 +31,9 @@ pub mod error; /// Common cross-platform file-types. pub mod file; +/// For determining the output layout. +pub mod layout; + /// Utilities to print output. pub mod output; @@ -67,10 +70,6 @@ pub struct Context { #[arg(short = 'f', long)] pub follow: bool, - /// Print disk usage information in plain format without the ASCII tree - #[arg(short = 'F', long)] - pub flat: bool, - /// Print disk usage in human-readable format #[arg(short = 'H', long)] pub human: bool, @@ -154,9 +153,9 @@ pub struct Context { #[arg(long)] pub dirs_only: bool, - /// Print tree with the root directory at the topmost position - #[arg(long)] - pub inverted: bool, + /// Which kind of layout to use when rendering the output + #[arg(long, value_enum, default_value_t = layout::Type::default())] + pub layout: layout::Type, /// Don't read configuration file #[arg(long)] @@ -346,7 +345,7 @@ impl Context { /// files, directories will always be included since matched files will need to be bridged back /// to the root node somehow. Empty sets not producing an output is handled by [`Tree`]. /// - /// [`Tree`]: crate::render::tree::Tree + /// [`Tree`]: crate::tree::Tree pub fn regex_predicate(&self) -> Predicate { let Some(pattern) = self.pattern.as_ref() else { return Err(Error::PatternNotProvided); diff --git a/src/render/context/output.rs b/src/context/output.rs similarity index 100% rename from src/render/context/output.rs rename to src/context/output.rs diff --git a/src/render/context/sort.rs b/src/context/sort.rs similarity index 99% rename from src/render/context/sort.rs rename to src/context/sort.rs index 05af88db..96e75049 100644 --- a/src/render/context/sort.rs +++ b/src/context/sort.rs @@ -14,7 +14,6 @@ pub enum Type { /// Sort entries by size largest to smallest, bottom to top SizeRev, - // Sort entries by newer to older Accessing Date //Access, diff --git a/src/render/context/test.rs b/src/context/test.rs similarity index 96% rename from src/render/context/test.rs rename to src/context/test.rs index 93fe74bd..7aa792ca 100644 --- a/src/render/context/test.rs +++ b/src/context/test.rs @@ -1,4 +1,4 @@ -use crate::render::context::sort; +use crate::context::sort; use clap::{CommandFactory, FromArgMatches}; use super::{config, Context}; diff --git a/src/render/context/time.rs b/src/context/time.rs similarity index 100% rename from src/render/context/time.rs rename to src/context/time.rs diff --git a/src/render/disk_usage/file_size.rs b/src/disk_usage/file_size.rs similarity index 99% rename from src/render/disk_usage/file_size.rs rename to src/disk_usage/file_size.rs index 9da5b937..7305e8ed 100644 --- a/src/render/disk_usage/file_size.rs +++ b/src/disk_usage/file_size.rs @@ -1,6 +1,6 @@ use super::units::{BinPrefix, PrefixKind, SiPrefix, UnitPrefix}; use crate::{ - render::styles::{self, get_du_theme, get_placeholder_style}, + styles::{self, get_du_theme, get_placeholder_style}, utils, Context, }; use ansi_term::Style; diff --git a/src/render/disk_usage/mod.rs b/src/disk_usage/mod.rs similarity index 100% rename from src/render/disk_usage/mod.rs rename to src/disk_usage/mod.rs diff --git a/src/render/disk_usage/units.rs b/src/disk_usage/units.rs similarity index 100% rename from src/render/disk_usage/units.rs rename to src/disk_usage/units.rs diff --git a/src/fs/permissions/mod.rs b/src/fs/permissions/mod.rs index 24421077..4beea3ba 100644 --- a/src/fs/permissions/mod.rs +++ b/src/fs/permissions/mod.rs @@ -42,6 +42,10 @@ pub struct FileMode { other_permissions: class::Permissions, } +/// Implements [Display] which presents symbolic notation of file permissions with the extended +/// attributes. +pub struct FileModeXAttrs<'a>(pub &'a FileMode); + impl FileMode { /// Constructor for [`FileMode`]. pub const fn new( @@ -96,6 +100,14 @@ impl Display for FileMode { } } +/// For representing file permissions with extended attributes in symbolic notation. +impl Display for FileModeXAttrs<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mode = self.0; + write!(f, "{mode}@") + } +} + /// For the octal representation of permissions impl Octal for FileMode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { diff --git a/src/main.rs b/src/main.rs index e0b37f56..ea69ca84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,27 +18,39 @@ )] use clap::CommandFactory; -use render::{ - context::Context, - tree::{ - display::{Flat, Inverted, Regular}, - Tree, - }, -}; +use context::{layout, Context}; +use render::{Engine, Flat, Inverted, Regular}; use std::{error::Error, io::stdout}; +use tree::Tree; /// Operations to wrangle ANSI escaped strings. mod ansi; +/// CLI rules and definitions as well as context to be injected throughout the entire program. +mod context; + +/// Operations relevant to the computation and presentation of disk usage. +mod disk_usage; + /// Filesystem operations. mod fs; -/// Dev icons. +/// All things related to icons on how to map certain files to the appropriate icons. mod icons; -/// Tools and operations to display root-directory. +/// Concerned with taking an initialized [`Tree`] and its [`Node`]s and rendering the output. +/// +/// [`Tree`]: tree::Tree +/// [`Node`]: tree::node::Node mod render; +/// Global used throughout the program to paint the output. +mod styles; + +/// Houses the primary data structures that are used to virtualize the filesystem, containing also +/// information on how the tree output should be ultimately rendered. +mod tree; + /// Utilities relating to interacting with tty properties. mod tty; @@ -53,17 +65,23 @@ fn main() -> Result<(), Box> { return Ok(()); } - render::styles::init(ctx.no_color()); - - if ctx.flat { - let tree = Tree::::try_init(ctx)?; - println!("{tree}"); - } else if ctx.inverted { - let tree = Tree::::try_init(ctx)?; - println!("{tree}"); - } else { - let tree = Tree::::try_init(ctx)?; - println!("{tree}"); + styles::init(ctx.no_color()); + + let (tree, ctx) = Tree::try_init_and_update_context(ctx)?; + + match ctx.layout { + layout::Type::Flat => { + let render = Engine::::new(tree, ctx); + println!("{render}"); + } + layout::Type::Inverted => { + let render = Engine::::new(tree, ctx); + println!("{render}"); + } + layout::Type::Regular => { + let render = Engine::::new(tree, ctx); + println!("{render}"); + } } Ok(()) diff --git a/src/render/display/mod.rs b/src/render/display/mod.rs deleted file mode 100644 index e54455d5..00000000 --- a/src/render/display/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod regular; - -pub mod flat; - -pub mod inverted; diff --git a/src/render/grid/cell.rs b/src/render/grid/cell.rs new file mode 100644 index 00000000..8a5474f0 --- /dev/null +++ b/src/render/grid/cell.rs @@ -0,0 +1,251 @@ +use crate::{ + context::Context, + disk_usage::file_size::FileSize, + render::theme, + styles::{self, PLACEHOLDER}, + tree::node::Node, +}; +use std::{ + ffi::OsStr, + fmt::{self, Display}, + path::Path, +}; + +/// Constitutes a single cell in a given row of the output. The `kind` field denotes what type of +/// data actually goes into the cell once rendered. Each `kind` which is of type [Kind] has its own +/// rules for rendering. Cell's do not have to be of a consistent width. +pub struct Cell<'a> { + ctx: &'a Context, + node: &'a Node, + kind: Kind<'a>, +} + +/// The type of data that a [Cell] should render. +pub enum Kind<'a> { + FileName { + prefix: Option<&'a str>, + }, + FilePath, + FileSize, + #[cfg(unix)] + Datetime, + #[cfg(unix)] + Ino, + #[cfg(unix)] + Nlink, + #[cfg(unix)] + Blocks, + #[cfg(unix)] + Permissions, +} + +impl<'a> Cell<'a> { + /// Initializes a new [Cell]. + pub const fn new(node: &'a Node, ctx: &'a Context, kind: Kind<'a>) -> Self { + Self { ctx, node, kind } + } + + /// Rules on how to render a file-name with icons and a prefix if applicable. The order in + /// which items are rendered are: prefix-icon-name. + #[inline] + fn fmt_name(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + match self.kind { + Kind::FileName { prefix } => { + let pre = prefix.unwrap_or(""); + let name = theme::stylize_file_name(node); + + if !ctx.icons { + return write!(f, "{pre}{name}"); + } + + let icon = node.compute_icon(ctx.no_color()); + + write!(f, "{pre}{icon} {name}") + } + + _ => unreachable!(), + } + } + + /// Rules on how to render a file's path + #[inline] + fn fmt_path(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + let path = if node.depth() == 0 { + let file_name = node.file_name(); + >::as_ref(file_name).display() + } else { + node.path() + .strip_prefix(ctx.dir_canonical()) + .unwrap_or_else(|_| node.path()) + .display() + }; + + if !ctx.icons { + return write!(f, "{path}"); + } + + let icon = node.compute_icon(ctx.no_color()); + + write!(f, "{icon} {path}") + } + + /// Rules on how to render the file size. + #[inline] + fn fmt_file_size(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + let formatted_size = node.file_size().map_or_else( + || FileSize::placeholder(ctx), + |size| size.format(ctx.max_size_width, ctx.max_size_unit_width), + ); + + write!(f, "{formatted_size}") + } + + /// Rules on how to format block for rendering + #[cfg(unix)] + #[inline] + fn fmt_blocks(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + let max_width = ctx.max_block_width; + + let out = node + .blocks() + .map(|num| format!("{num:>max_width$}")) + .unwrap_or(format!("{PLACEHOLDER:>max_width$}")); + + let formatted_blocks = if let Ok(style) = styles::get_block_style() { + style.paint(out).to_string() + } else { + out + }; + + write!(f, "{formatted_blocks}") + } + + /// Rules on how to format nlink for rendering. + #[cfg(unix)] + #[inline] + fn fmt_nlink(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + let max_width = ctx.max_nlink_width; + + let out = node + .nlink() + .map(|num| format!("{num:>max_width$}")) + .unwrap_or(format!("{PLACEHOLDER:>max_width$}")); + + let formatted_nlink = if let Ok(style) = styles::get_nlink_style() { + style.paint(out).to_string() + } else { + out + }; + + write!(f, "{formatted_nlink}") + } + + /// Rules on how to format ino for rendering. + #[cfg(unix)] + #[inline] + fn fmt_ino(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + let max_width = ctx.max_ino_width; + + let out = node + .ino() + .map(|num| format!("{num:>max_width$}")) + .unwrap_or(format!("{PLACEHOLDER:>max_width$}")); + + let formatted_ino = if let Ok(style) = styles::get_ino_style() { + style.paint(out).to_string() + } else { + out + }; + + write!(f, "{formatted_ino}") + } + + /// Rules on how to format datetime for rendering. + #[cfg(unix)] + #[inline] + fn fmt_datetime(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use crate::context::time; + use chrono::{offset::Local, DateTime}; + + let node = self.node; + let ctx = self.ctx; + + let datetime = match ctx.time() { + time::Stamp::Created => node.created(), + time::Stamp::Accessed => node.accessed(), + time::Stamp::Modified => node.modified(), + }; + + let out = datetime.map(DateTime::::from).map_or_else( + || format!("{PLACEHOLDER:>12}"), + |dt| format!("{:>12}", dt.format("%d %h %H:%M %g")), + ); + + let formatted_datetime = if let Ok(style) = styles::get_datetime_style() { + style.paint(out).to_string() + } else { + out + }; + + write!(f, "{formatted_datetime}") + } + + /// Rules on how to format permissions for rendering + #[cfg(unix)] + #[inline] + fn fmt_permissions(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + let formatted_perms = if ctx.octal { + theme::style_oct_permissions(node) + } else { + theme::style_sym_permissions(node) + }; + + write!(f, "{formatted_perms}") + } +} + +impl Display for Cell<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.kind { + Kind::FileName { prefix: _prefix } => self.fmt_name(f), + Kind::FilePath => self.fmt_path(f), + Kind::FileSize => self.fmt_file_size(f), + + #[cfg(unix)] + Kind::Ino => self.fmt_ino(f), + + #[cfg(unix)] + Kind::Nlink => self.fmt_nlink(f), + + #[cfg(unix)] + Kind::Blocks => self.fmt_blocks(f), + + #[cfg(unix)] + Kind::Datetime => self.fmt_datetime(f), + + #[cfg(unix)] + Kind::Permissions => self.fmt_permissions(f), + } + } +} diff --git a/src/render/grid/mod.rs b/src/render/grid/mod.rs new file mode 100644 index 00000000..2f46a883 --- /dev/null +++ b/src/render/grid/mod.rs @@ -0,0 +1,149 @@ +use crate::{ansi::Escaped, tree::node::Node, Context}; +use cell::Cell; +use std::{ + fmt::{self, Display}, + marker::PhantomData, +}; + +/// Concerned with rules to construct and a single cell in a given row. +pub mod cell; + +pub struct Row<'a, T> { + prefix: Option<&'a str>, + ctx: &'a Context, + node: &'a Node, + layout: PhantomData, +} + +/// For both the [`super::Regular`] and [`super::Inverted`] layout variants. +pub struct Tree; + +/// For the [`super::Flat`] variant. +pub struct Flat; + +impl<'a, T> Row<'a, T> { + pub const fn new(node: &'a Node, ctx: &'a Context, prefix: Option<&'a str>) -> Row<'a, T> { + Self { + prefix, + node, + ctx, + layout: PhantomData, + } + } +} + +#[cfg(unix)] +impl Display for Row<'_, Tree> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + let size = Cell::new(node, ctx, cell::Kind::FileSize); + let name = Cell::new( + node, + ctx, + cell::Kind::FileName { + prefix: self.prefix, + }, + ); + + let row = if ctx.long { + let ino = Cell::new(node, ctx, cell::Kind::Ino); + let perms = Cell::new(node, ctx, cell::Kind::Permissions); + let nlink = Cell::new(node, ctx, cell::Kind::Nlink); + let blocks = Cell::new(node, ctx, cell::Kind::Blocks); + let time = Cell::new(node, ctx, cell::Kind::Datetime); + + format!("{ino} {perms} {nlink} {blocks} {time} {size} {name}") + } else { + format!("{size} {name}") + }; + + if ctx.truncate && ctx.window_width.is_some() { + let window_width = ctx.window_width.unwrap(); + let out = ::truncate(&row, window_width); + write!(f, "{out}") + } else { + write!(f, "{row}") + } + } +} + +#[cfg(not(unix))] +impl Display for Row<'_, Tree> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + let size = Cell::new(node, ctx, cell::Kind::FileSize); + let name = Cell::new( + node, + ctx, + cell::Kind::FileName { + prefix: self.prefix, + }, + ); + + let row = format!("{size} {name}"); + + if ctx.truncate && ctx.window_width.is_some() { + let window_width = ctx.window_width.unwrap(); + let out = ::truncate(&row, window_width); + write!(f, "{out}") + } else { + write!(f, "{row}") + } + } +} + +#[cfg(unix)] +impl Display for Row<'_, Flat> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + let size = Cell::new(node, ctx, cell::Kind::FileSize); + let path = Cell::new(node, ctx, cell::Kind::FilePath); + + let row = if ctx.long { + let ino = Cell::new(node, ctx, cell::Kind::Ino); + let perms = Cell::new(node, ctx, cell::Kind::Permissions); + let nlink = Cell::new(node, ctx, cell::Kind::Nlink); + let blocks = Cell::new(node, ctx, cell::Kind::Blocks); + let time = Cell::new(node, ctx, cell::Kind::Datetime); + + format!("{ino} {perms} {nlink} {blocks} {time} {size} {path}") + } else { + format!("{size} {path}") + }; + + if ctx.truncate && ctx.window_width.is_some() { + let window_width = ctx.window_width.unwrap(); + let out = ::truncate(&row, window_width); + write!(f, "{out}") + } else { + write!(f, "{row}") + } + } +} + +#[cfg(not(unix))] +impl Display for Row<'_, Flat> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let node = self.node; + let ctx = self.ctx; + + let size = Cell::new(node, ctx, cell::Kind::FileSize); + let path = Cell::new(node, ctx, cell::Kind::FilePath); + + let row = format!("{size} {path}"); + + if ctx.truncate && ctx.window_width.is_some() { + let window_width = ctx.window_width.unwrap(); + let out = ::truncate(&row, window_width); + write!(f, "{out}") + } else { + write!(f, "{row}") + } + } +} diff --git a/src/render/display/flat.rs b/src/render/layout/flat.rs similarity index 65% rename from src/render/display/flat.rs rename to src/render/layout/flat.rs index 925440dd..05eea3af 100644 --- a/src/render/display/flat.rs +++ b/src/render/layout/flat.rs @@ -1,11 +1,13 @@ use crate::{ - render::Engine, + render::{ + grid::{self, Row}, + Engine, Flat, + }, tree::{count::FileCount, Tree}, }; +use indextree::NodeEdge; use std::fmt::{self, Display}; -pub struct Flat; - impl Display for Engine { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let ctx = self.context(); @@ -15,16 +17,21 @@ impl Display for Engine { let max_depth = ctx.level(); let mut file_count_data = vec![]; - let descendants = root_id.descendants(arena); - - for node_id in descendants { + for edge in root_id.reverse_traverse(arena) { + let node_id = match edge { + NodeEdge::Start(id) => id, + NodeEdge::End(_) => continue, + }; let node = arena[node_id].get(); if node.depth() > max_depth { continue; } - node.flat_display(f, ctx)?; + let row = Row::::new(node, ctx, None); + + writeln!(f, "{row}")?; + file_count_data.push(Tree::compute_file_count(node_id, arena)); } diff --git a/src/render/display/inverted.rs b/src/render/layout/inverted.rs similarity index 93% rename from src/render/display/inverted.rs rename to src/render/layout/inverted.rs index bb08dee6..80d84225 100644 --- a/src/render/display/inverted.rs +++ b/src/render/layout/inverted.rs @@ -1,13 +1,14 @@ use crate::{ - render::{Engine, theme}, + render::{ + grid::{self, Row}, + theme, Engine, Inverted, + }, styles, tree::{count::FileCount, node::Node, Tree}, }; use indextree::NodeId; use std::fmt::{self, Display}; -pub struct Inverted; - impl Display for Engine { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let ctx = self.context(); @@ -21,9 +22,9 @@ impl Display for Engine { let mut descendants = root_id.descendants(arena).skip(1).peekable(); let mut display_node = |node_id: NodeId, node: &Node, prefix: &str| -> fmt::Result { - node.tree_display(f, prefix, ctx)?; + let row = Row::::new(node, ctx, Some(prefix)); file_count_data.push(Tree::compute_file_count(node_id, arena)); - writeln!(f) + writeln!(f, "{row}") }; display_node(root_id, arena[root_id].get(), "")?; diff --git a/src/render/layout/mod.rs b/src/render/layout/mod.rs new file mode 100644 index 00000000..29b727f8 --- /dev/null +++ b/src/render/layout/mod.rs @@ -0,0 +1,8 @@ +/// See [`super::Regular`] +pub mod regular; + +/// See [`super::Flat`] +pub mod flat; + +/// See [`super::Inverted`] +pub mod inverted; diff --git a/src/render/display/regular.rs b/src/render/layout/regular.rs similarity index 94% rename from src/render/display/regular.rs rename to src/render/layout/regular.rs index 7e0af7a3..d893c7c6 100644 --- a/src/render/display/regular.rs +++ b/src/render/layout/regular.rs @@ -1,13 +1,14 @@ use crate::{ - render::{Engine, theme}, + render::{ + grid::{self, Row}, + theme, Engine, Regular, + }, styles, tree::{count::FileCount, node::Node, Tree}, }; use indextree::{NodeEdge, NodeId}; use std::fmt::{self, Display}; -pub struct Regular; - impl Display for Engine { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let ctx = self.context(); @@ -18,9 +19,9 @@ impl Display for Engine { let mut file_count_data = vec![]; let mut display_node = |node_id: NodeId, node: &Node, prefix: &str| -> fmt::Result { - node.tree_display(f, prefix, ctx)?; + let row = Row::::new(node, ctx, Some(prefix)); file_count_data.push(Tree::compute_file_count(node_id, arena)); - writeln!(f) + writeln!(f, "{row}") }; let mut get_theme = if ctx.follow { diff --git a/src/render/mod.rs b/src/render/mod.rs index 88dbe780..70cd2b5a 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,14 +1,51 @@ -/// CLI rules and definitions and context wherein [`Tree`] will operate. -/// -/// [`Tree`]: tree::Tree -pub mod context; +use crate::{context::Context, tree::Tree}; +use std::marker::PhantomData; -/// Operations that decide how to present info about disk usage. -pub mod disk_usage; +/// Module containing all of the layout variants. +pub mod layout; -/// Encapsulates everything related to the in-memory representation of the root directory and its -/// contents. -pub mod tree; +/// Concerned with how to construct a single row in the output grid. +pub mod grid; -/// Global styles. -pub mod styles; +/// Utility module to fetch the appropriate theme used to paint the box-drawing characters of the +/// output tree. +pub mod theme; + +/// The struct that is generic over T, which is generally expected to be a unit-struct that +/// ultimately determines which variant to use for the output. +pub struct Engine { + ctx: Context, + tree: Tree, + layout: PhantomData, +} + +/// The flat output that is similar to `du`, without the ASCII tree. +pub struct Flat; + +/// The tree output with the root directory at the bottom of the output. +pub struct Regular; + +/// The tree output with the root directory at the top of the output. More like the traditional +/// `tree` command. +pub struct Inverted; + +impl Engine { + /// Initializes a new [Engine]. + pub const fn new(tree: Tree, ctx: Context) -> Self { + Self { + ctx, + tree, + layout: PhantomData, + } + } + + /// Getter for the inner [Context] object. + const fn context(&self) -> &Context { + &self.ctx + } + + /// Getter for the inner [Tree] data structure. + const fn tree(&self) -> &Tree { + &self.tree + } +} diff --git a/src/render/theme.rs b/src/render/theme.rs index d33d1901..ee305ac2 100644 --- a/src/render/theme.rs +++ b/src/render/theme.rs @@ -1,7 +1,10 @@ use crate::{ + fs::permissions::FileModeXAttrs, styles::{self, ThemesMap}, tree::node::Node, }; +use ansi_term::{Color, Style}; +use std::borrow::Cow; type Theme = Box &'static ThemesMap>; @@ -35,3 +38,72 @@ pub fn link_theme_getter() -> Theme { } }) } + +/// Stylizes the input `file_name` with the provided `style`. If `None` is provided then the +/// underlying `String` is returned unmodified as a [Cow]. If the provided [Node] is a symlink then +/// it will be styled accordingly. +pub fn stylize_file_name(node: &Node) -> Cow<'_, str> { + let name = node.file_name(); + let style = node.style(); + + let Some(target_name) = node.symlink_target_file_name() else { + if let Some(Style {foreground: Some(ref fg), .. }) = style { + let file_name = name.to_string_lossy(); + let styled_name = fg.bold().paint(file_name).to_string(); + return Cow::from(styled_name); + } + + return name.to_string_lossy(); + }; + + if style.is_some() { + let styled_name = stylize_file_name(node); + let target_name = Color::Red.paint(format!("\u{2192} {}", target_name.to_string_lossy())); + + return Cow::from(format!("{styled_name} {target_name}")); + } + + let link = name.to_string_lossy(); + let target = target_name.to_string_lossy(); + Cow::from(format!("{link} \u{2192} {target}")) +} + +/// Styles the symbolic notation of file permissions. +#[cfg(unix)] +pub fn style_sym_permissions(node: &Node) -> String { + let perms = node.mode().expect("Expected permissions to be initialized"); + + let symb = if node.has_xattrs() { + let perm_xattr = FileModeXAttrs(&perms); + format!("{perm_xattr}") + } else { + // extra whitespace to align with permissions with extended attrs + format!("{perms} ") + }; + + if let Ok(theme) = styles::get_permissions_theme() { + symb.chars() + .filter_map(|ch| { + theme.get(&ch).map(|color| { + let chstr = ch.to_string(); + color.paint(chstr).to_string() + }) + }) + .collect::() + } else { + symb + } +} + +/// Styles the octal notation of file permissions. +#[cfg(unix)] +pub fn style_oct_permissions(node: &Node) -> String { + let perms = node.mode().expect("Expected permissions to be initialized"); + let oct = format!("{perms:04o}"); + + if let Ok(style) = styles::get_octal_permissions_style() { + style.paint(oct).to_string() + } else { + oct + } +} diff --git a/src/render/tree/display/mod.rs b/src/render/tree/display/mod.rs deleted file mode 100644 index d8d9fbcb..00000000 --- a/src/render/tree/display/mod.rs +++ /dev/null @@ -1,230 +0,0 @@ -use crate::render::{ - styles, - tree::{count::FileCount, node::Node, Tree}, -}; -use indextree::{NodeEdge, NodeId}; -use std::fmt::{self, Display, Formatter}; - -/// Empty trait used to constrain generic parameter `display_variant` of [Tree]. -pub trait TreeVariant {} - -/// For printing output with colored ANSI escapes. -pub struct Regular {} - -/// Prints the invered tree with colored ANSI escapes. -pub struct Inverted {} - -/// For generating plain-text report of disk usage without ASCII tree. -pub struct Flat {} - -impl TreeVariant for Regular {} -impl TreeVariant for Flat {} -impl TreeVariant for Inverted {} - -/// Utilities to pick the appropriate theme to paint box drawing characters. -mod theme; - -impl Display for Tree { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let ctx = self.context(); - let root_id = self.root_id; - let arena = self.arena(); - let max_depth = ctx.level(); - let mut file_count_data = vec![]; - - let mut display_node = |node_id: NodeId, node: &Node, prefix: &str| -> fmt::Result { - node.tree_display(f, prefix, ctx)?; - file_count_data.push(Self::compute_file_count(node_id, arena)); - writeln!(f) - }; - - let mut get_theme = if ctx.follow { - theme::link_theme_getter() - } else { - theme::regular_theme_getter() - }; - - let mut base_prefix_components = vec![""]; - - let mut tree_edges = root_id.reverse_traverse(arena).skip(1).peekable(); - - while let Some(node_edge) = tree_edges.next() { - let current_node_id = match node_edge { - NodeEdge::Start(id) => id, - - NodeEdge::End(id) => { - let current_node = arena[id].get(); - - if !current_node.is_dir() || id.children(arena).count() == 0 { - continue; - } - - let theme = get_theme(current_node); - - let topmost_sibling = id.following_siblings(arena).nth(1).is_none(); - - if topmost_sibling { - base_prefix_components.push(styles::SEP); - } else { - base_prefix_components.push(theme.get("vt").unwrap()); - } - - continue; - } - }; - - let current_node = arena[current_node_id].get(); - - let node_depth = current_node.depth(); - - let topmost_sibling = current_node_id.following_siblings(arena).nth(1).is_none(); - - let theme = get_theme(current_node); - - if node_depth <= max_depth { - if node_depth == 0 { - display_node(current_node_id, current_node, "")?; - } else { - let prefix_part = if topmost_sibling { - theme.get("drt").unwrap() - } else { - theme.get("vtrt").unwrap() - }; - - let mut current_prefix_components = base_prefix_components.clone(); - - current_prefix_components.push(prefix_part); - - let prefix = current_prefix_components.join(""); - - display_node(current_node_id, current_node, &prefix)?; - } - } - - if let Some(NodeEdge::Start(next_id)) = tree_edges.peek() { - let next_node = arena[*next_id].get(); - - if next_node.depth() < node_depth { - base_prefix_components.pop(); - } - } - } - - if !file_count_data.is_empty() { - write!(f, "\n{}", FileCount::from(file_count_data))?; - } - - Ok(()) - } -} - -impl Display for Tree { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let ctx = self.context(); - - let root_id = self.root_id; - let arena = self.arena(); - let level = ctx.level(); - let mut file_count_data = vec![]; - - let mut descendants = root_id.descendants(arena).skip(1).peekable(); - - let mut display_node = |node_id: NodeId, node: &Node, prefix: &str| -> fmt::Result { - node.tree_display(f, prefix, ctx)?; - file_count_data.push(Self::compute_file_count(node_id, arena)); - writeln!(f) - }; - - display_node(root_id, arena[root_id].get(), "")?; - - let mut get_theme = if ctx.follow { - theme::link_theme_getter() - } else { - theme::regular_theme_getter() - }; - - let mut base_prefix_components = vec![""]; - - while let Some(current_node_id) = descendants.next() { - let current_node = arena[current_node_id].get(); - - let current_depth = current_node.depth(); - - let mut siblings = current_node_id.following_siblings(arena).skip(1).peekable(); - - let last_sibling = siblings.peek().is_none(); - - let theme = get_theme(current_node); - - if current_depth <= level { - let prefix_part = if last_sibling { - theme.get("uprt").unwrap() - } else { - theme.get("vtrt").unwrap() - }; - - let mut current_prefix_components = base_prefix_components.clone(); - - current_prefix_components.push(prefix_part); - - let prefix = current_prefix_components.join(""); - - display_node(current_node_id, current_node, &prefix)?; - } - - if let Some(next_id) = descendants.peek() { - let next_node = arena[*next_id].get(); - - let next_depth = next_node.depth(); - - if next_depth == current_depth + 1 { - if last_sibling { - base_prefix_components.push(styles::SEP); - } else { - let prefix = theme.get("vt").unwrap(); - base_prefix_components.push(prefix); - } - } else if next_depth < current_depth { - let depth_delta = current_depth - next_depth; - - base_prefix_components.truncate(base_prefix_components.len() - depth_delta); - } - } - } - - if !file_count_data.is_empty() { - write!(f, "\n{}", FileCount::from(file_count_data))?; - } - - Ok(()) - } -} - -impl Display for Tree { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let tree = self.arena(); - let root_id = self.root_id(); - let ctx = self.context(); - let max_depth = ctx.level(); - let mut file_count_data = vec![]; - - let descendants = root_id.descendants(tree); - - for node_id in descendants { - let node = tree[node_id].get(); - - if node.depth() > max_depth { - continue; - } - - node.flat_display(f, ctx)?; - file_count_data.push(Self::compute_file_count(node_id, tree)); - } - - if !file_count_data.is_empty() { - write!(f, "\n{}", FileCount::from(file_count_data))?; - } - - Ok(()) - } -} diff --git a/src/render/tree/display/theme.rs b/src/render/tree/display/theme.rs deleted file mode 100644 index f8b04de6..00000000 --- a/src/render/tree/display/theme.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::render::{ - styles::{self, ThemesMap}, - tree::Node, -}; - -type Theme = Box &'static ThemesMap>; - -/// Returns a closure that retrieves the regular theme. -pub fn regular_theme_getter() -> Theme { - Box::new(|_node: &Node| styles::get_tree_theme().unwrap()) -} - -/// Returns a closure that can smartly determine when a symlink is being followed and when it is -/// not being followed. When a symlink is being followed, all of its descendents should have tree -/// branches that are colored differently. -pub fn link_theme_getter() -> Theme { - let mut link_depth = None; - - Box::new(move |node: &Node| { - let current_depth = node.depth(); - - if let Some(ldepth) = link_depth { - if current_depth == ldepth { - link_depth = None; - } - } - - if link_depth.is_some() || node.is_symlink() { - if node.is_dir() && link_depth.is_none() { - link_depth = Some(current_depth); - } - styles::get_link_theme().unwrap() - } else { - styles::get_tree_theme().unwrap() - } - }) -} diff --git a/src/render/tree/node/display/mod.rs b/src/render/tree/node/display/mod.rs deleted file mode 100644 index d9062d4a..00000000 --- a/src/render/tree/node/display/mod.rs +++ /dev/null @@ -1,152 +0,0 @@ -use crate::{ - ansi::Escaped, - render::{context::Context, tree::node::Node}, -}; -use std::{ - borrow::Cow, - fmt::{self, Formatter}, -}; - -/// Helpers to prepare each invidual section of the [Node]'s attributes to display. -mod presenters; - -impl Node { - /// Formats the [Node] for the tree view. - #[cfg(unix)] - pub(super) fn tree( - &self, - f: &mut Formatter, - prefix: Option<&str>, - ctx: &Context, - ) -> fmt::Result { - let size = presenters::format_size(self, ctx); - let padded_icon = presenters::format_padded_icon(self, ctx); - let file_name = presenters::file_name(self); - - let pre = prefix.unwrap_or(""); - - let ln = if ctx.long { - let presenters::LongAttrs { - ino, - perms, - nlink, - blocks, - timestamp, - } = presenters::format_long(self, ctx); - - format!( - "{ino:::truncate(&ln, window_width); - write!(f, "{out}") - } else { - write!(f, "{ln}") - } - } - - /// Formats the [Node] for a plain report view. - #[cfg(unix)] - pub(super) fn flat(&self, f: &mut Formatter, ctx: &Context) -> fmt::Result { - use std::{ffi::OsStr, path::Path}; - - let size = presenters::format_size(self, ctx); - - let file = { - let node_path = self.path(); - - if self.depth() == 0 { - node_path.file_name().map_or_else( - || Cow::from(node_path.display().to_string()), - OsStr::to_string_lossy, - ) - } else { - node_path - .strip_prefix(ctx.dir_canonical()) - .map_or_else(|_| self.path().to_string_lossy(), Path::to_string_lossy) - } - }; - - let ln = if ctx.long { - let presenters::LongAttrs { - ino, - perms, - nlink, - blocks, - timestamp, - } = presenters::format_long(self, ctx); - - format!( - "{ino:::truncate(&ln, window_width); - writeln!(f, "{out}") - } else { - writeln!(f, "{ln}") - } - } - - /// Formats the [Node] for a plain report view. - #[cfg(not(unix))] - pub(super) fn flat(&self, f: &mut Formatter, ctx: &Context) -> fmt::Result { - let size = presenters::format_size(self, ctx); - - let file = { - let path = self - .path() - .strip_prefix(ctx.dir_canonical()) - .unwrap_or_else(|_| self.path()); - - Cow::from(path.display().to_string()) - }; - - let ln = format!("{size} {file}"); - - if ctx.truncate && ctx.window_width.is_some() { - let window_width = ctx.window_width.unwrap(); - let out = ::truncate(&ln, window_width); - writeln!(f, "{out}") - } else { - writeln!(f, "{ln}") - } - } - - /// Formats the [Node] for the tree view. - #[cfg(not(unix))] - pub(super) fn tree( - &self, - f: &mut Formatter, - prefix: Option<&str>, - ctx: &Context, - ) -> fmt::Result { - let size = presenters::format_size(self, ctx); - let padded_icon = presenters::format_padded_icon(self, ctx); - let file_name = presenters::file_name(self); - let pre = prefix.unwrap_or(""); - - let ln = format!("{size} {pre}{padded_icon}{file_name}"); - - if ctx.truncate && ctx.window_width.is_some() { - let window_width = ctx.window_width.unwrap(); - let out = ::truncate(&ln, window_width); - write!(f, "{out}") - } else { - write!(f, "{ln}") - } - } -} diff --git a/src/render/tree/node/display/presenters.rs b/src/render/tree/node/display/presenters.rs deleted file mode 100644 index 251afccb..00000000 --- a/src/render/tree/node/display/presenters.rs +++ /dev/null @@ -1,122 +0,0 @@ -use crate::render::{context::Context, disk_usage::file_size::FileSize, tree::Node}; -use std::borrow::Cow; - -#[cfg(unix)] -use crate::render::{ - context::time::Stamp, - styles::{self, error::Error, PLACEHOLDER}, -}; - -#[cfg(unix)] -use std::time::SystemTime; - -#[cfg(unix)] -type StyleGetter = fn() -> Result<&'static ansi_term::Style, Error<'static>>; - -/// Attributes for the long view to be displayed. -#[cfg(unix)] -pub(super) struct LongAttrs { - pub ino: String, - pub perms: String, - pub nlink: String, - pub blocks: String, - pub timestamp: String, -} - -/// Formats the parameters for the long view. -#[cfg(unix)] -#[inline] -pub(super) fn format_long(node: &Node, ctx: &Context) -> LongAttrs { - let file_mode = node.mode().unwrap(); - - let perms = if ctx.octal { - Node::style_octal_permissions(&file_mode) - } else if node.has_xattrs() { - Node::style_sym_permissions(&file_mode, true) - } else { - Node::style_sym_permissions(&file_mode, false) - }; - - let datetime = match ctx.time() { - Stamp::Created => node.created(), - Stamp::Accessed => node.accessed(), - Stamp::Modified => node.modified(), - }; - - let ino = format_num(node.ino(), ctx.max_ino_width, styles::get_ino_style); - let nlink = format_num(node.nlink(), ctx.max_nlink_width, styles::get_nlink_style); - let blocks = format_num(node.blocks(), ctx.max_block_width, styles::get_block_style); - let timestamp = format_datetime(datetime); - - LongAttrs { - ino, - perms, - nlink, - blocks, - timestamp, - } -} - -/// Builds the disk usage portion of the output. -#[inline] -pub(super) fn format_size(node: &Node, ctx: &Context) -> String { - node.file_size().map_or_else( - || FileSize::placeholder(ctx), - |size| size.format(ctx.max_size_width, ctx.max_size_unit_width), - ) -} - -/// Builds the icon portion of the output. -#[inline] -pub(super) fn format_padded_icon(node: &Node, ctx: &Context) -> String { - if ctx.icons { - let icon = node.compute_icon(ctx.no_color()); - let padding = icon.len() - 1; - format!("{icon:, max_width: usize, style_getter: StyleGetter) -> String { - let out = num - .map(|num| format!("{num:>max_width$}")) - .unwrap_or(format!("{PLACEHOLDER:>max_width$}")); - - if let Ok(style) = style_getter() { - style.paint(out).to_string() - } else { - out - } -} - -#[cfg(unix)] -#[inline] -pub(super) fn format_datetime(datetime: Option) -> String { - use chrono::{offset::Local, DateTime}; - - let out = datetime.map(DateTime::::from).map_or_else( - || format!("{PLACEHOLDER:>12}"), - |dt| format!("{:>12}", dt.format("%d %h %H:%M %g")), - ); - - if let Ok(style) = styles::get_datetime_style() { - style.paint(out).to_string() - } else { - out - } -} - -#[inline] -pub(super) fn file_name(node: &Node) -> Cow { - node.symlink_target_file_name().map_or_else( - || Node::stylize(node.file_name(), node.style), - |target_name| { - let link_name = node.file_name(); - Node::stylize_link_name(link_name, target_name, node.style) - }, - ) -} diff --git a/src/render/tree/node/style.rs b/src/render/tree/node/style.rs deleted file mode 100644 index 9bfeb693..00000000 --- a/src/render/tree/node/style.rs +++ /dev/null @@ -1,80 +0,0 @@ -use super::Node; -use ansi_term::{Color, Style}; -use std::{borrow::Cow, ffi::OsStr}; - -#[cfg(unix)] -use crate::{ - fs::permissions::FileMode, - render::styles::{get_octal_permissions_style, get_permissions_theme}, -}; - -impl Node { - /// Stylizes input, `entity` based on `LS_COLORS`. If `style` is `None` then the entity is - /// returned unmodified. - pub(super) fn stylize(file_name: &OsStr, style: Option