summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBenjamin Nguyen <benjamin.van.nguyen@gmail.com>2023-11-26 12:07:05 -0800
committerBenjamin Nguyen <benjamin.van.nguyen@gmail.com>2023-11-26 12:07:05 -0800
commit8fa8f72ca6289695515f497c08ba1b6125cb5bce (patch)
tree2195759bd6c8f89d8bd72c5b52d101ce9b703aa5
parentf64b58be59a089bcd04e4597da2c096086bdb3bc (diff)
good checkpoint
-rw-r--r--src/disk/mod.rs50
-rw-r--r--src/file/mod.rs35
-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.rs15
-rw-r--r--src/render/mod.rs (renamed from src/tree/display/mod.rs)50
-rw-r--r--src/render/row/long.rs159
-rw-r--r--src/render/row/mod.rs49
-rw-r--r--src/tree/display/column.rs52
-rw-r--r--src/user/args.rs6
-rw-r--r--src/user/column.rs84
-rw-r--r--src/user/mod.rs29
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: