diff options
author | Benjamin Nguyen <benjamin.van.nguyen@gmail.com> | 2023-11-26 12:07:05 -0800 |
---|---|---|
committer | Benjamin Nguyen <benjamin.van.nguyen@gmail.com> | 2023-11-26 12:07:05 -0800 |
commit | 8fa8f72ca6289695515f497c08ba1b6125cb5bce (patch) | |
tree | 2195759bd6c8f89d8bd72c5b52d101ce9b703aa5 | |
parent | f64b58be59a089bcd04e4597da2c096086bdb3bc (diff) |
good checkpoint
-rw-r--r-- | src/disk/mod.rs | 50 | ||||
-rw-r--r-- | src/file/mod.rs | 35 | ||||
-rw-r--r-- | src/file/tree/mod.rs (renamed from src/tree/mod.rs) | 46 | ||||
-rw-r--r-- | src/file/tree/traverse.rs (renamed from src/tree/traverse.rs) | 9 | ||||
-rw-r--r-- | src/main.rs | 15 | ||||
-rw-r--r-- | src/render/mod.rs (renamed from src/tree/display/mod.rs) | 50 | ||||
-rw-r--r-- | src/render/row/long.rs | 159 | ||||
-rw-r--r-- | src/render/row/mod.rs | 49 | ||||
-rw-r--r-- | src/tree/display/column.rs | 52 | ||||
-rw-r--r-- | src/user/args.rs | 6 | ||||
-rw-r--r-- | src/user/column.rs | 84 | ||||
-rw-r--r-- | src/user/mod.rs | 29 |
12 files changed, 443 insertions, 141 deletions
diff --git a/src/disk/mod.rs b/src/disk/mod.rs index 6e0f630..931d661 100644 --- a/src/disk/mod.rs +++ b/src/disk/mod.rs @@ -14,6 +14,18 @@ pub mod prefix; #[cfg(unix)] const BLOCK_SIZE: u64 = 512; +/// Padding between the numerical value the byte-unit, "B" used when reporting bytes. +pub const RAW_PADDING: usize = 2; + +/// Padding between the numerical value and the SI units used when reporting bytes. +pub const SI_PADDING: usize = 3; + +/// Padding between the numerical value and the binary units used when reporting bytes. +pub const BIN_PADDING: usize = 4; + +/// Precision to use when reporting bytes in human-readable format. +pub const FLOAT_PRECISION: usize = 1; + /// Different metrics for reporting file size. #[derive(Debug)] pub enum Usage { @@ -170,22 +182,22 @@ impl Display for Usage { macro_rules! byte_display { ($p:expr, $v:expr) => { match $p { - BytePresentation::Raw => write!(f, "{} B", $v), - BytePresentation::Binary => { + BytePresentation::Raw => write!(f, "{}{:>RAW_PADDING$}", $v, "B"), + BytePresentation::Bin => { let prefix = prefix::Binary::from($v); if matches!(prefix, prefix::Binary::Base) { - write!(f, "{} {:>3}", $v, "B") + write!(f, "{}{:>BIN_PADDING$}", $v, "B") } else { let bytes = ($v as f64) / prefix.base_value(); - write!(f, "{bytes:.1} {prefix}B") + write!(f, "{bytes:.FLOAT_PRECISION$} {prefix}B") } }, - BytePresentation::StandardInternational => { + BytePresentation::Si => { let prefix = prefix::Si::from($v); if matches!(prefix, prefix::Si::Base) { - write!(f, "{} {:>2}", $v, "B") + write!(f, "{}{:>SI_PADDING$}", $v, "B") } else { let bytes = ($v as f64) / prefix.base_value(); write!(f, "{bytes:.1} {prefix}B") @@ -231,91 +243,91 @@ impl AddAssign<u64> for Usage { fn test_bytes_display() { let size = Usage::Physical { value: 998, - presentation: BytePresentation::Binary, + presentation: BytePresentation::Bin, }; assert_eq!(String::from("998 B"), format!("{size}")); let size = Usage::Physical { value: 2_u64.pow(10), - presentation: BytePresentation::Binary, + presentation: BytePresentation::Bin, }; assert_eq!(String::from("1.0 KiB"), format!("{size}")); let size = Usage::Physical { value: 2_u64.pow(20), - presentation: BytePresentation::Binary, + presentation: BytePresentation::Bin, }; assert_eq!(String::from("1.0 MiB"), format!("{size}")); let size = Usage::Physical { value: 2_u64.pow(30), - presentation: BytePresentation::Binary, + presentation: BytePresentation::Bin, }; assert_eq!(String::from("1.0 GiB"), format!("{size}")); let size = Usage::Physical { value: 2_u64.pow(40), - presentation: BytePresentation::Binary, + presentation: BytePresentation::Bin, }; assert_eq!(String::from("1.0 TiB"), format!("{size}")); let size = Usage::Physical { value: 2_u64.pow(50), - presentation: BytePresentation::Binary, + presentation: BytePresentation::Bin, }; assert_eq!(String::from("1.0 PiB"), format!("{size}")); let size = Usage::Physical { value: 2_u64.pow(30), - presentation: BytePresentation::Binary, + presentation: BytePresentation::Bin, }; assert_eq!(String::from("1.0 GiB"), format!("{size}")); let size = Usage::Physical { value: 10_u64.pow(3), - presentation: BytePresentation::StandardInternational, + presentation: BytePresentation::Si, }; assert_eq!(String::from("1.0 KB"), format!("{size}")); let size = Usage::Physical { value: 10_u64.pow(6), - presentation: BytePresentation::StandardInternational, + presentation: BytePresentation::Si, }; assert_eq!(String::from("1.0 MB"), format!("{size}")); let size = Usage::Physical { value: 10_u64.pow(9), - presentation: BytePresentation::StandardInternational, + presentation: BytePresentation::Si, }; assert_eq!(String::from("1.0 GB"), format!("{size}")); let size = Usage::Physical { value: 10_u64.pow(12), - presentation: BytePresentation::StandardInternational, + presentation: BytePresentation::Si, }; assert_eq!(String::from("1.0 TB"), format!("{size}")); let size = Usage::Physical { value: 10_u64.pow(15), - presentation: BytePresentation::StandardInternational, + presentation: BytePresentation::Si, }; assert_eq!(String::from("1.0 PB"), format!("{size}")); let size = Usage::Physical { value: 998, - presentation: BytePresentation::StandardInternational, + presentation: BytePresentation::Si, }; assert_eq!(String::from("998 B"), format!("{size}")); diff --git a/src/file/mod.rs b/src/file/mod.rs index 826273f..805891d 100644 --- a/src/file/mod.rs +++ b/src/file/mod.rs @@ -1,6 +1,9 @@ use crate::{ disk, - user::{args::Metric, Context}, + user::{ + args::{Metric, TimeFormat, TimeStamp}, + Context, + }, }; use ignore::DirEntry; use std::{ @@ -13,6 +16,10 @@ use std::{ pub mod inode; use inode::{INodeError, Inode}; +/// Concerned with the tree data structure that is used to produce the program output. +pub mod tree; +pub use tree::Tree; + /// File attributes specific to Unix systems. #[cfg(unix)] pub mod unix; @@ -109,6 +116,32 @@ impl File { pub fn size(&self) -> &disk::Usage { &self.size } + + #[cfg(unix)] + pub fn unix_attrs(&self) -> &unix::Attrs { + &self.unix_attrs + } + + #[cfg(unix)] + pub fn timestamp_from_ctx(&self, ctx: &Context) -> Option<String> { + use chrono::{DateTime, Local}; + + let system_time = match ctx.time { + TimeStamp::Mod => self.metadata().accessed().ok(), + TimeStamp::Create => self.metadata().created().ok(), + TimeStamp::Access => self.metadata().accessed().ok(), + }; + + system_time + .map(DateTime::<Local>::from) + .map(|local_time| match ctx.time_format { + TimeFormat::Default => local_time.format("%d %h %H:%M %g"), + TimeFormat::Iso => local_time.format("%Y-%m-%d %H:%M:%S"), + TimeFormat::IsoStrict => local_time.format("%Y-%m-%dT%H:%M:%S%Z"), + TimeFormat::Short => local_time.format("%Y-%m-%d"), + }) + .map(|dt| format!("{dt}")) + } } impl Deref for File { diff --git a/src/tree/mod.rs b/src/file/tree/mod.rs index 77489ce..23ccd4c 100644 --- a/src/tree/mod.rs +++ b/src/file/tree/mod.rs @@ -1,39 +1,35 @@ -use crate::{error::prelude::*, file::File, user::Context}; +use crate::{error::prelude::*, file::File, user::{Context, column}}; use ahash::{HashMap, HashSet}; use indextree::{Arena, NodeId}; use std::{fs, ops::Deref, path::PathBuf}; -/// Concerned with how to display user-presentable tree output. -pub mod display; -use display::column; - /// Parallel disk reading mod traverse; /// Representation of the file-tree that is traversed starting from the root directory whose index /// in the underlying `arena` is `root_id`. -pub struct FileTree { +pub struct Tree { root_id: NodeId, arena: Arena<File>, - column_widths: column::Widths, } -/// Errors associated with [`FileTree`]. +/// Errors associated with [`Tree`]. #[derive(Debug, thiserror::Error)] pub enum TreeError { #[error("Failed to extrapolate the root directory")] RootDir, } -impl FileTree { - /// Like [`FileTree::init`] but leverages parallelism for disk-reads and [`File`] initialization. - pub fn init(ctx: &Context) -> Result<Self> { +impl Tree { + /// Like [`Tree::init`] but leverages parallelism for disk-reads and [`File`] initialization. + pub fn init(ctx: &Context) -> Result<(Self, column::Metadata)> { let mut arena = Arena::new(); let mut branches = HashMap::<PathBuf, Vec<NodeId>>::default(); - let mut column_widths = column::Widths::default(); + let mut col_md = column::Metadata::default(); traverse::run(ctx, |file| { - column_widths.update(&file, ctx); + #[cfg(unix)] + col_md.update_unix_attrs_widths(&file, ctx); let node_id = arena.new_node(file); let file = arena[node_id].get(); @@ -86,7 +82,11 @@ impl FileTree { let is_dir = child_node.file_type().is_some_and(|f| f.is_dir()); let size = child_node.size().value(); let inode = match child_node.inode() { - Ok(value) => value, + Ok(value) => { + #[cfg(unix)] + col_md.update_inode_attr_widths(&value); + value + }, Err(err) => { log::warn!( "Failed to query inode of {} which may affect disk usage report: {}", @@ -121,12 +121,14 @@ impl FileTree { *arena[parent_id].get_mut().size_mut() += current_dir_size; } } + col_md.update_size_width(arena[root_id].get(), ctx); + let tree = Self { root_id, arena }; + + Ok((tree, col_md)) + } - Ok(Self { - root_id, - arena, - column_widths, - }) + pub fn init_without_disk_usage() -> Self { + todo!() } pub fn root_id(&self) -> NodeId { @@ -136,13 +138,9 @@ impl FileTree { pub fn arena(&self) -> &Arena<File> { &self.arena } - - pub fn node_is_dir(&self, id: NodeId) -> bool { - self[id].get().file_type().is_some_and(|ft| ft.is_dir()) - } } -impl Deref for FileTree { +impl Deref for Tree { type Target = Arena<File>; fn deref(&self) -> &Self::Target { diff --git a/src/tree/traverse.rs b/src/file/tree/traverse.rs index 4b4f8a7..b4094ff 100644 --- a/src/tree/traverse.rs +++ b/src/file/tree/traverse.rs @@ -1,6 +1,6 @@ use crate::{error::prelude::*, file::File, user::Context}; use ignore::{ - DirEntry, ParallelVisitor, ParallelVisitorBuilder, WalkBuilder, WalkParallel, WalkState, + DirEntry, ParallelVisitor, ParallelVisitorBuilder, WalkState, }; use std::{ ops::Deref, @@ -9,13 +9,6 @@ use std::{ thread, }; -/// Errors that may arise whe reading from Disk. -#[derive(Debug, thiserror::Error)] -pub enum TraverseError { - #[error("Failed to query the root directory")] - RootDirMissing, -} - /// Parallel traversal algorithm. `op` takes in a single argument which is the [`File`] that is /// retrieved from disk, returning a [`Result`]. If `op` returns an `Err` then traversal will /// immediately conclude. diff --git a/src/main.rs b/src/main.rs index 054a75c..6691d3a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,9 +21,8 @@ mod file; /// Concerned with logging throughout the application. mod logging; -/// Virtual file-tree data structure and relevant operations. -mod tree; -use tree::{display, FileTree}; +/// Concerned with rendering the program output. +mod render; fn main() -> ExitCode { if let Err(e) = run() { @@ -34,15 +33,19 @@ fn main() -> ExitCode { } fn run() -> Result<()> { - let ctx = user::Context::init()?; + let mut ctx = user::Context::init()?; let logger = ctx .verbose .then_some(logging::LoggityLog::init()) .transpose()?; - let file_tree = FileTree::init(&ctx)?; - let output = display::tree(&file_tree, &ctx)?; + let file_tree = file::Tree::init(&ctx).and_then(|(tree, column_metadata)| { + ctx.update_column_metadata(column_metadata); + Ok(tree) + })?; + + let output = render::tree(&file_tree, &ctx)?; let mut stdout = stdout().lock(); writeln!(stdout, "{output}").into_report(ErrorCategory::Warning)?; diff --git a/src/tree/display/mod.rs b/src/render/mod.rs index ce98f36..f816da4 100644 --- a/src/tree/display/mod.rs +++ b/src/render/mod.rs @@ -1,13 +1,5 @@ -use super::FileTree; -use crate::{error::prelude::*, user::Context}; +use crate::{error::prelude::*, file, user::Context}; use indextree::{NodeEdge, NodeId}; -use std::fmt::Write; - -/// Concerned with properties of individual columns in the output. -pub mod column; - -/// Used as general placeholder for an empty field. -pub const PLACEHOLDER: &str = "-"; /// Used for padding between tree branches. pub const SEP: &str = " "; @@ -24,10 +16,15 @@ pub const BL_CORNER: &str = "\u{2514}\u{2500} "; /// The `├─` box drawing characters. pub const ROTATED_T: &str = "\u{251C}\u{2500} "; -pub fn tree(file_tree: &FileTree, ctx: &Context) -> Result<String> { +/// Concerned with the presentation of a single [`crate::file::File`] which constitutes a single +/// row in the program output. +mod row; + +pub fn tree(file_tree: &file::Tree, ctx: &Context) -> Result<String> { let arena = file_tree.arena(); let root = file_tree.root_id(); let max_depth = ctx.level(); + let mut buf = String::new(); let is_first_sibling = |node_id: NodeId, depth: usize| { @@ -38,6 +35,8 @@ pub fn tree(file_tree: &FileTree, ctx: &Context) -> Result<String> { let mut inherited_prefix_components = vec![""]; + let mut formatter = row::formatter(&mut buf, ctx); + for node_edge in root.reverse_traverse(arena) { let (node, node_id, depth) = match node_edge { NodeEdge::Start(node_id) => { @@ -73,22 +72,25 @@ pub fn tree(file_tree: &FileTree, ctx: &Context) -> Result<String> { }, }; - let name = node.file_name().to_string_lossy(); - let inherited_prefix = inherited_prefix_components.join(""); - - let prefix = (depth > 0) - .then(|| { - is_first_sibling(node_id, depth) - .then_some(UL_CORNER) - .unwrap_or(ROTATED_T) - }) - .unwrap_or(""); - - writeln!(buf, "{inherited_prefix}{prefix}{name}") - .into_report(ErrorCategory::Internal) - .context(error_source!())?; + let prefix = format!( + "{}{}", + inherited_prefix_components.join(""), + (depth > 0) + .then(|| { + is_first_sibling(node_id, depth) + .then_some(UL_CORNER) + .unwrap_or(ROTATED_T) + }) + .unwrap_or("") + ); + + if let Err(e) = formatter(&node, prefix) { + log::warn!("{e}"); + } } + drop(formatter); + Ok(buf) } diff --git a/src/render/row/long.rs b/src/render/row/long.rs new file mode 100644 index 0000000..637043e --- /dev/null +++ b/src/render/row/long.rs @@ -0,0 +1,159 @@ +use crate::{ + file::{ + inode::{Inode, INodeError}, + unix::{ + permissions::{FileModeXAttrs, SymbolicNotation}, + xattr::ExtendedAttr, + }, + File, + }, + user::{Context, column}, +}; +use std::{ + convert::From, + fmt::{self, Display} +}; + +/// The width of the file-type, permissions of each class, as well as the indicator that there are +/// extended attributes e.g. `drwxrwxrwx@`. +const ATTRS_WIDTH: usize = 11; + +/// e.g. 0744 +const OCTAL_PERMISSIONS_WIDTH: usize = 4; + +/// Use in place of a field that can't be computed in the output. +const PLACEHOLDER: &str = "-"; + +/// Data type whose [`Display`] implementation determines how the ls-like long-format should be +/// presented to the user. +pub struct Format<'a> { + ctx: &'a Context, + file: &'a File, +} + + +impl<'a> Format<'a> { + pub fn new(file: &'a File, ctx: &'a Context) -> Self { + Self { file, ctx } + } +} + +impl From<INodeError> for fmt::Error { + fn from(_e: INodeError) -> Self { + fmt::Error::default() + } +} + +impl Display for Format<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let Context { + group: enable_group, + ino: enable_ino, + nlink: enable_nlink, + octal: enable_octal, + column_metadata, + .. + } = self.ctx; + + let file_mode = self.file + .metadata() + .permissions() + .try_mode_symbolic_notation() + .map_err(|_e| fmt::Error::default())?; + + let attrs = self.file + .has_xattrs() + .then(|| format!("{}", FileModeXAttrs(&file_mode))) + .unwrap_or_else(|| format!("{}", file_mode)); + + let unix_attrs = self.file.unix_attrs(); + let owner = unix_attrs.owner().unwrap_or(PLACEHOLDER); + + match (enable_group, enable_ino, enable_nlink, enable_octal) { + (false, false, false, false) => { + let column::Metadata { max_owner_width, .. } = column_metadata; + write!(f, "{attrs:<ATTRS_WIDTH$} {owner:>max_owner_width$}") + } + (true, false, false, false) => { + let column::Metadata { max_owner_width, max_group_width, .. } = column_metadata; + let group = unix_attrs.group().unwrap_or(PLACEHOLDER); + write!(f, "{attrs:<ATTRS_WIDTH$} {owner:>max_owner_width$} {group:>max_group_width$}") + } + (false, true, false, false) => { + let column::Metadata { max_owner_width, max_ino_width, .. } = column_metadata; + let Inode { ino, .. } = self.file.inode()?; + write!(f, "{ino:max_ino_width$} {attrs:<ATTRS_WIDTH$} {owner:>max_owner_width$}") + } + (false, false, true, false) => { + let column::Metadata { max_owner_width, max_nlink_width, .. } = column_metadata; + let Inode { nlink, .. } = self.file.inode()?; + write!(f, "{attrs:<ATTRS_WIDTH$} {nlink:>max_nlink_width$} {owner:>max_owner_width$}") + } + (true, true, false, false) => { + let column::Metadata { max_owner_width, max_group_width, max_ino_width, .. } = column_metadata; + let group = unix_attrs.group().unwrap_or(PLACEHOLDER); + let Inode { ino, .. } = self.file.inode()?; + write!(f, "{ino:max_ino_width$} {attrs:<ATTRS_WIDTH$} {owner:>max_owner_width$} {group:>max_group_width$}") + } + (true, false, true, false) => { + let column::Metadata { max_owner_width, max_group_width, max_nlink_width, .. } = column_metadata; + let group = unix_attrs.group().unwrap_or(PLACEHOLDER); + let Inode { nlink, .. } = self.file.inode()?; + write!(f, "{attrs:<ATTRS_WIDTH$} {nlink:>max_nlink_width$} {owner:>max_owner_width$} {group:>max_group_width$}") + } + (false, true, true, false) => { + let column::Metadata { max_owner_width, max_ino_width, max_nlink_width, .. } = column_metadata; + let Inode { ino, nlink, .. } = self.file.inode()?; + write!(f, "{ino:max_ino_width$} {attrs:<ATTRS_WIDTH$} {nlink:>max_nlink_width$} {owner:>max_owner_width$}") + } + (true, true, true, false) => { + let column::Metadata { max_owner_width, max_ino_width, max_nlink_width, max_group_width, .. } = column_metadata; + let group = unix_attrs.group().unwrap_or(PLACEHOLDER); + let Inode { ino, nlink, .. } = self.file.inode()?; + write!(f, "{ino:max_ino_width$} {attrs:<ATTRS_WIDTH$} {nlink:>max_nlink_width$} {owner:>max_owner_width$} {group:>max_group_width$}") + } + (false, false, false, true) => { + let column::Metadata { max_owner_width, .. } = column_metadata; + write!(f, "{file_mode:0OCTAL_PERMISSIONS_WIDTH$o} {attrs:<ATTRS_WIDTH$} {owner:>max_owner_width$}") + } + (true, false, false, true) => { + let column::Metadata { max_owner_width, max_group_width, .. } = column_metadata; + let group = unix_attrs.group().unwrap_or(PLACEHOLDER); + write!(f, "{file_mode:0OCTAL_PERMISSIONS_WIDTH$o} {attrs:<ATTRS_WIDTH$} {owner:>max_owner_width$} {group:>max_group_width$}") + } + (true, true, false, true) => { + let column::Metadata { max_owner_width, max_group_width, max_ino_width, .. } = column_metadata; + let group = unix_attrs.group().unwrap_or(PLACEHOLDER); + let Inode { ino, .. } = self.file.inode()?; + write!(f, "{ino:max_ino_width$} {file_mode:0OCTAL_PERMISSIONS_WIDTH$o} {attrs:<ATTRS_WIDTH$} {owner:>max_owner_width$} {group:>max_group_width$}") + } + (true, false, true, true) => { + let column::Metadata { max_owner_width, max_group_width, max_nlink_width, .. } = column_metadata; + let group = unix_attrs.group().unwrap_or(PLACEHOLDER); + let Inode { nlink, .. } = self.file.inode()?; + write!(f, "{file_mode:0OCTAL_PERMISSIONS_WIDTH$o} {attrs:<ATTRS_WIDTH$} {nlink:>max_nlink_width$} {owner:>max_owner_width$} {group:>max_group_width$}") + } + (false, false, true, true) => { + let column::Metadata { max_owner_width, max_nlink_width, .. } = column_metadata; + let Inode { nlink, .. } = self.file.inode()?; + write!(f, "{file_mode:0OCTAL_PERMISSIONS_WIDTH$o} {attrs:<ATTRS_WIDTH$} {nlink:>max_nlink_width$} {owner:>max_owner_width$}") + } + (false, true, false, true) => { + let column::Metadata { max_owner_width, max_ino_width, .. } = column_metadata; + let Inode { ino, .. } = self.file.inode()?; + write!(f, "{ino:max_ino_width$} {file_mode:0OCTAL_PERMISSIONS_WIDTH$o} {attrs:<ATTRS_WIDTH$} {owner:>max_owner_width$}") + } + (false, true, true, true) => { + let column::Metadata { max_owner_width, max_ino_width, max_nlink_width, .. } = column_metadata; + let Inode { ino, nlink, .. } = self.file.inode()?; + write!(f, "{ino:max_ino_width$} {file_mode:0OCTAL_PERMISSIONS_WIDTH$o} {attrs:<ATTRS_WIDTH$} {nlink:>max_nlink_width$} {owner:>max_owner_width$}") + } + (true, true, true, true) => { + let column::Metadata { max_owner_width, max_ino_width, max_nlink_width, max_group_width, .. } = column_metadata; + let group = unix_attrs.group().unwrap_or(PLACEHOLDER); + let Inode { ino, nlink, .. } = self.file.inode()?; + write!(f, "{ino:max_ino_width$} {file_mode:0OCTAL_PERMISSIONS_WIDTH$o} {attrs:<ATTRS_WIDTH$} {nlink:>max_nlink_width$} {owner:>max_owner_width$} {group:>max_group_width$}") + } + } + } +} diff --git a/src/render/row/mod.rs b/src/render/row/mod.rs new file mode 100644 index 0000000..b035906 --- /dev/null +++ b/src/render/row/mod.rs @@ -0,0 +1,49 @@ +use crate::{error::prelude::*, file::File, user::{Context, column}}; +use std::fmt::Write; + +/// Concerned with how to present long-format for a particular file. +#[cfg(unix)] +mod long; + +#[cfg(windows)] +pub fn formatter<'a>( + buf: &'a mut String, + ctx: &'a Context, +) -> Box<dyn FnMut(&File, String) -> Result<()> + 'a> { + Box::new(|file, prefix| { + let size = format!("{}", file.size()); + let name = file.file_name().to_string_lossy(); + let column::Widths { + size: size_width, .. + } = ctx.col_widths(); + writeln!(buf, "{size:>size_width$} {prefix}{name}").into_report(ErrorCategory::Warning) + }) +} + +#[cfg(unix)] +pub fn formatter<'a>( + buf: &'a mut String, + ctx: &'a Context, +) -> Box<dyn FnMut(&File, String) -> Result<()> + 'a> { + if !ctx.long { + return Box::new(|file, prefix| { + let size = format!("{}", file.size()); + let name = file.file_name().to_string_lossy(); + let column::Metadata { + max_size_width, .. + } = ctx.column_metadata; + writeln!(buf, "{size:>max_size_width$} {prefix}{name}").into_report(ErrorCategory::Warning) + }); + } + + Box::new(|file: &File, prefix| { + let size = format!("{}", file.size()); + let name = file.file_name().to_string_lossy(); + 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}") + .into_report(ErrorCategory::Warning) + }) +} diff --git a/src/tree/display/column.rs b/src/tree/display/column.rs deleted file mode 100644 index 89e15bb..0000000 --- a/src/tree/display/column.rs +++ /dev/null @@ -1,52 +0,0 @@ -use crate::{ - file::File, - user::{args::Metric, Context}, -}; - -#[derive(Default)] -pub struct Widths { - size: usize, - name: usize, - attrs: usize, - group: usize, - nlink: usize, - octal: usize, - time: usize, -} - -impl Widths { - pub fn update(&mut self, file: &File, ctx: &Context) { - self.size = Self::get_size_width(file, ctx).max(self.size); - self.name = Self::get_name_width(file).max(self.name); - } - - fn get_name_width(file: &File) -> usize { - file.file_name().to_string_lossy().len() - } - - fn get_size_width(file: &File, ctx: &Context) -> usize { - if !matches!(ctx.metric, Metric::Logical | Metric::Physical) { - return utils::num_integral(file.size().value()); - } - - format!("{}", file.size()).len() - } -} - -mod utils { - /// How many integral digits are there? - #[inline] - pub fn num_integral(value: u64) -> usize { - if value == 0 { - return 0; - } - value.ilog10() as usize + 1 - } - - #[test] - fn test_num_integral() { - assert_eq!(num_integral(1000), 4); - assert_eq!(num_integral(10), 2); - assert_eq!(num_integral(10000), 5); - } -} diff --git a/src/user/args.rs b/src/user/args.rs index 1c7dd9b..269d152 100644 --- a/src/user/args.rs +++ b/src/user/args.rs @@ -29,10 +29,12 @@ pub enum BytePresentation { Raw, /// Reports byte size in binary units e.g. KiB - Binary, + #[value(alias("binary"))] + Bin, /// Reports byte size in SI units e.g. KB - StandardInternational, + #[value(alias("standard-international"))] + Si, } /// Different types of timestamps available in long-view. diff --git a/src/user/column.rs b/src/user/column.rs new file mode 100644 index 0000000..ee7f4d3 --- /dev/null +++ b/src/user/column.rs @@ -0,0 +1,84 @@ +use crate::{ + disk::{BIN_PADDING, FLOAT_PRECISION, RAW_PADDING, SI_PADDING}, + file::File, + user::{ + args::{BytePresentation, Metric}, + Context, + }, +}; + +#[cfg(unix)] +use crate::file::inode::Inode; + +#[derive(Clone, Copy, Debug, Default)] +pub struct Metadata { + pub max_size_width: usize, + #[cfg(unix)] + pub max_group_width: usize, + #[cfg(unix)] + pub max_owner_width: usize, + #[cfg(unix)] + pub max_nlink_width: usize, + #[cfg(unix)] + pub max_ino_width: usize, + #[cfg(unix)] + pub max_time_width: usize, +} + +impl Metadata { + pub fn update_size_width(&mut self, file: &File, ctx: &Context) { + self.max_size_width = Self::get_size_width(file, ctx).max(self.max_size_width); + } + + #[cfg(unix)] + pub fn update_unix_attrs_widths(&mut self, file: &File, ctx: &Context) { + let unix_attrs = file.unix_attrs(); + + self.max_time_width = file + .timestamp_from_ctx(ctx) + .map_or(0, |s| s.len()) + .max(self.max_time_width); + self.max_owner_width = unix_attrs.owner().map_or(0, str::len).max(self.max_owner_width); + self.max_group_width = unix_attrs.group().map_or(0, str::len).max(self.max_group_width); + } + + #[cfg(unix)] + pub fn update_inode_attr_widths(&mut self, inode: &Inode) { + self.max_ino_width = utils::num_integral(inode.ino).max(self.max_ino_width); + self.max_nlink_width = utils::num_integral(inode.nlink).max(self.max_nlink_width); + } + + fn get_size_width(file: &File, ctx: &Context) -> usize { + if !matches!(ctx.metric, Metric::Logical | Metric::Physical) { + return utils::num_integral(file.size().value()); + } + + match ctx.byte_units { + BytePresentation::Raw => utils::num_integral(file.size().value()) + RAW_PADDING, + // Explanation for 4: + // - 123.1 KB, the '4' takes into account the integral numbers plus the '.'. + BytePresentation::Si => 4 + FLOAT_PRECISION + SI_PADDING, + // Explanation for 5: + // Unlike SI units, we can have say 1008.0 KiB for binary units. + BytePresentation::Bin => 5 + FLOAT_PRECISION + BIN_PADDING, + } + } +} + +mod utils { + /// How many integral digits are there? + #[inline] + pub fn num_integral(value: u64) -> usize { + if value == 0 { + return 0; + } + value.ilog10() as usize + 1 + } + + #[test] + fn test_num_integral() { + assert_eq!(num_integral(1000), 4); + assert_eq!(num_integral(10), 2); + assert_eq!(num_integral(10000), 5); + } +} diff --git a/src/user/mod.rs b/src/user/mod.rs index 96b480b..f1c6785 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -2,9 +2,14 @@ use crate::error::prelude::*; use clap::Parser; use std::{env, fs, path::PathBuf}; +#[cfg(unix)] + /// Enum definitions for enumerated command-line arguments. pub mod args; +/// Concerned with properties of columns in the output which is essentially a 2D grid. +pub mod column; + /// Defines the CLI whose purpose is to capture user arguments and reconcile them with arguments /// found with a config file if relevant. #[derive(Parser, Debug)] @@ -64,15 +69,15 @@ pub struct Context { #[arg(long, requires = "long")] pub octal: bool, - /// Which kind of timestamp to use; modified by default + /// Which kind of timestamp to use #[cfg(unix)] - #[arg(long, value_enum, requires = "long")] - pub time: Option<args::TimeStamp>, + #[arg(long, value_enum, requires = "long", default_value_t)] + pub time: args::TimeStamp, /// Which format to use for the timestamp; default by default #[cfg(unix)] - #[arg(long = "time-format", value_enum, requires = "long")] - pub time_format: Option<args::TimeFormat>, + #[arg(long = "time-format", value_enum, requires = "long", default_value_t)] + pub time_format: args::TimeFormat, /// Maximum depth to display #[arg(short = 'L', long, value_name = "NUM")] @@ -93,9 +98,19 @@ pub struct Context { #[arg(short = 'x', long = "one-file-system")] pub same_fs: |