diff --git a/src/context/output.rs b/src/context/output.rs index 8b7582b2..79f6c680 100644 --- a/src/context/output.rs +++ b/src/context/output.rs @@ -20,8 +20,9 @@ pub struct ColumnProperties { impl From<&Context> for ColumnProperties { fn from(ctx: &Context) -> Self { let unit_width = match ctx.unit { - PrefixKind::Si => 2, - PrefixKind::Bin => 3, + PrefixKind::Bin if ctx.human => 3, + PrefixKind::Si if ctx.human => 2, + _ => 1, }; Self { diff --git a/src/disk_usage/file_size.rs b/src/disk_usage/file_size.rs deleted file mode 100644 index 7305e8ed..00000000 --- a/src/disk_usage/file_size.rs +++ /dev/null @@ -1,191 +0,0 @@ -use super::units::{BinPrefix, PrefixKind, SiPrefix, UnitPrefix}; -use crate::{ - styles::{self, get_du_theme, get_placeholder_style}, - utils, Context, -}; -use ansi_term::Style; -use clap::ValueEnum; -use filesize::PathExt; -use std::{borrow::Cow, fs::Metadata, ops::AddAssign, path::Path}; - -/// Represents either logical or physical size and handles presentation. -#[derive(Clone, Debug)] -pub struct FileSize { - pub bytes: u64, - #[allow(dead_code)] - disk_usage: DiskUsage, - prefix_kind: PrefixKind, - human_readable: bool, - unpadded_display: Option, - - // Precomputed style to use - style: Option<&'static Style>, - - // Does this file size use `B` without a prefix? - uses_base_unit: Option<()>, - - // How many columns are required for the size (without prefix). - pub size_columns: usize, -} - -/// Determines between logical or physical size for display -#[derive(Copy, Clone, Debug, ValueEnum, Default)] -pub enum DiskUsage { - /// How many bytes does a file contain - Logical, - - /// How much actual space on disk, taking into account sparse files and compression. - #[default] - Physical, -} - -impl FileSize { - /// Initializes a [`FileSize`]. - pub const fn new( - bytes: u64, - disk_usage: DiskUsage, - human_readable: bool, - prefix_kind: PrefixKind, - ) -> Self { - Self { - bytes, - disk_usage, - human_readable, - prefix_kind, - unpadded_display: None, - style: None, - uses_base_unit: None, - size_columns: 0, - } - } - - /// Computes the logical size of a file given its [Metadata]. - pub fn logical(md: &Metadata, prefix_kind: PrefixKind, human_readable: bool) -> Self { - let bytes = md.len(); - Self::new(bytes, DiskUsage::Logical, human_readable, prefix_kind) - } - - /// Computes the physical size of a file given its [Path] and [Metadata]. - pub fn physical( - path: &Path, - md: &Metadata, - prefix_kind: PrefixKind, - human_readable: bool, - ) -> Option { - path.size_on_disk_fast(md) - .ok() - .map(|bytes| Self::new(bytes, DiskUsage::Physical, human_readable, prefix_kind)) - } - - pub fn unpadded_display(&self) -> Option<&str> { - self.unpadded_display.as_deref() - } - - /// Precompute the raw (unpadded) display and sets the number of columns the size (without - /// the prefix) will occupy. Also sets the [Style] to use in advance to style the size output. - pub fn precompute_unpadded_display(&mut self) { - let fbytes = self.bytes as f64; - - match self.prefix_kind { - PrefixKind::Si => { - let unit = SiPrefix::from(fbytes); - let base_value = unit.base_value(); - - if !self.human_readable { - self.unpadded_display = Some(format!("{} B", self.bytes)); - self.size_columns = utils::num_integral(self.bytes); - } else if matches!(unit, SiPrefix::Base) { - self.unpadded_display = Some(format!("{} {unit}", self.bytes)); - self.size_columns = utils::num_integral(self.bytes); - self.uses_base_unit = Some(()); - } else { - let size = fbytes / (base_value as f64); - self.unpadded_display = Some(format!("{size:.2} {unit}")); - self.size_columns = utils::num_integral((size * 100.0).floor() as u64) + 1; - } - - if let Ok(theme) = get_du_theme() { - let style = theme.get(format!("{unit}").as_str()); - self.style = style; - } - } - PrefixKind::Bin => { - let unit = BinPrefix::from(fbytes); - let base_value = unit.base_value(); - - if !self.human_readable { - self.unpadded_display = Some(format!("{} B", self.bytes)); - self.size_columns = utils::num_integral(self.bytes); - } else if matches!(unit, BinPrefix::Base) { - self.unpadded_display = Some(format!("{} {unit}", self.bytes)); - self.size_columns = utils::num_integral(self.bytes); - self.uses_base_unit = Some(()); - } else { - let size = fbytes / (base_value as f64); - self.unpadded_display = Some(format!("{size:.2} {unit}")); - self.size_columns = utils::num_integral((size * 100.0).floor() as u64) + 1; - } - - if let Ok(theme) = get_du_theme() { - let style = theme.get(format!("{unit}").as_str()); - self.style = style; - } - } - } - } - - /// Formats [`FileSize`] for presentation. - pub fn format(&self, max_size_width: usize, max_size_unit_width: usize) -> String { - let out = if self.human_readable { - let mut precomputed = self.unpadded_display().unwrap().split(' '); - let size = precomputed.next().unwrap(); - let unit = precomputed.next().unwrap(); - - if self.uses_base_unit.is_some() { - format!( - "{:>max_size_width$} {unit:>max_size_unit_width$}", - self.bytes - ) - } else { - format!("{size:>max_size_width$} {unit:>max_size_unit_width$}") - } - } else { - format!("{: String { - if ctx.suppress_size || ctx.max_size_width == 0 { - return String::new(); - } - - let placeholder = get_placeholder_style().map_or_else( - |_| Cow::from(styles::PLACEHOLDER), - |style| Cow::from(style.paint(styles::PLACEHOLDER).to_string()), - ); - - let placeholder_padding = placeholder.len() - + ctx.max_size_width - + match ctx.unit { - PrefixKind::Si if ctx.human => 2, - PrefixKind::Bin if ctx.human => 3, - PrefixKind::Si => 0, - PrefixKind::Bin => 1, - }; - - format!("{placeholder:>placeholder_padding$}") - } -} - -impl AddAssign<&Self> for FileSize { - fn add_assign(&mut self, rhs: &Self) { - self.bytes += rhs.bytes; - } -} diff --git a/src/disk_usage/file_size/block.rs b/src/disk_usage/file_size/block.rs new file mode 100644 index 00000000..bcb982ec --- /dev/null +++ b/src/disk_usage/file_size/block.rs @@ -0,0 +1,22 @@ +use std::{ + fmt::{self, Display}, + fs::Metadata, + os::unix::fs::MetadataExt, +}; + +#[derive(Default)] +pub struct Metric { + pub value: u64, +} + +impl Metric { + pub fn init(md: &Metadata) -> Self { + Self { value: md.blocks() } + } +} + +impl Display for Metric { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(&self.value, f) + } +} diff --git a/src/disk_usage/file_size/byte.rs b/src/disk_usage/file_size/byte.rs new file mode 100644 index 00000000..4ecfef78 --- /dev/null +++ b/src/disk_usage/file_size/byte.rs @@ -0,0 +1,172 @@ +use super::super::units::{BinPrefix, PrefixKind, SiPrefix, UnitPrefix}; +use filesize::PathExt; +use std::{ + fmt::{self, Display}, + fs::Metadata, + path::Path, +}; + +/// Concerned with measuring file size in bytes, whether logical or physical determined by `kind`. +/// Binary or SI units used for reporting determined by `prefix_kind`. +pub struct Metric { + pub value: u64, + pub human_readable: bool, + #[allow(dead_code)] + kind: MetricKind, + prefix_kind: PrefixKind, +} + +/// Represents the appropriate method in which to compute bytes. `Logical` represent the total amount +/// of bytes in a file; `Physical` represents how many bytes are actually used to store the file on +/// disk. +pub enum MetricKind { + Logical, + Physical, +} + +impl Metric { + /// Initializes a [Metric] that stores the total amount of bytes in a file. + pub fn init_logical( + metadata: &Metadata, + prefix_kind: PrefixKind, + human_readable: bool, + ) -> Self { + let value = metadata.len(); + let kind = MetricKind::Logical; + + Self { + value, + human_readable, + kind, + prefix_kind, + } + } + + /// Initializes an empty [Metric] used to represent the total amount of bytes of a file. + pub const fn init_empty_logical(human_readable: bool, prefix_kind: PrefixKind) -> Self { + Self { + value: 0, + human_readable, + kind: MetricKind::Logical, + prefix_kind, + } + } + + /// Initializes an empty [Metric] used to represent the total disk space of a file in bytes. + pub const fn init_empty_physical(human_readable: bool, prefix_kind: PrefixKind) -> Self { + Self { + value: 0, + human_readable, + kind: MetricKind::Physical, + prefix_kind, + } + } + + /// Initializes a [Metric] that stores the total amount of bytes used to store a file on disk. + pub fn init_physical( + path: &Path, + metadata: &Metadata, + prefix_kind: PrefixKind, + human_readable: bool, + ) -> Self { + let value = path.size_on_disk_fast(metadata).unwrap_or(metadata.len()); + let kind = MetricKind::Physical; + + Self { + value, + human_readable, + kind, + prefix_kind, + } + } +} + +impl Display for Metric { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let value = self.value as f64; + + match self.prefix_kind { + PrefixKind::Si => { + if !self.human_readable { + return write!(f, "{} {}", self.value, SiPrefix::Base); + } + + let unit = SiPrefix::from(self.value); + + if matches!(unit, SiPrefix::Base) { + write!(f, "{} {unit}", self.value) + } else { + let base_value = unit.base_value(); + let size = value / (base_value as f64); + write!(f, "{size:.2} {unit}") + } + } + PrefixKind::Bin => { + if !self.human_readable { + return write!(f, "{} {}", self.value, BinPrefix::Base); + } + + let unit = BinPrefix::from(self.value); + + if matches!(unit, BinPrefix::Base) { + write!(f, "{} {unit}", self.value) + } else { + let base_value = unit.base_value(); + let size = value / (base_value as f64); + write!(f, "{size:.2} {unit}") + } + } + } + } +} + +#[test] +fn test_metric() { + let metric = Metric { + value: 100, + kind: MetricKind::Logical, + human_readable: false, + prefix_kind: PrefixKind::Bin, + }; + assert_eq!(format!("{}", metric), "100 B"); + + let metric = Metric { + value: 1000, + kind: MetricKind::Logical, + human_readable: true, + prefix_kind: PrefixKind::Si, + }; + assert_eq!(format!("{}", metric), "1.00 KB"); + + let metric = Metric { + value: 1000, + kind: MetricKind::Logical, + human_readable: true, + prefix_kind: PrefixKind::Bin, + }; + assert_eq!(format!("{}", metric), "1000 B"); + + let metric = Metric { + value: 1024, + kind: MetricKind::Logical, + human_readable: true, + prefix_kind: PrefixKind::Bin, + }; + assert_eq!(format!("{}", metric), "1.00 KiB"); + + let metric = Metric { + value: 2_u64.pow(20), + kind: MetricKind::Logical, + human_readable: true, + prefix_kind: PrefixKind::Bin, + }; + assert_eq!(format!("{}", metric), "1.00 MiB"); + + let metric = Metric { + value: 123454, + kind: MetricKind::Logical, + human_readable: false, + prefix_kind: PrefixKind::Bin, + }; + assert_eq!(format!("{}", metric), "123454 B"); +} diff --git a/src/disk_usage/file_size/line_count.rs b/src/disk_usage/file_size/line_count.rs new file mode 100644 index 00000000..1c145315 --- /dev/null +++ b/src/disk_usage/file_size/line_count.rs @@ -0,0 +1,45 @@ +use std::{ + convert::{AsRef, From}, + fmt::{self, Display}, + fs, + path::Path, +}; + +/// Concerned with measuring file size using line count as a metric. +#[derive(Default)] +pub struct Metric { + pub value: u64, +} + +impl Metric { + /// Reads in contents of a file given by `path` and attempts to compute the total number of + /// lines in that file. If a file is not UTF-8 encoded as in the case of a binary jpeg file + /// then `None` will be returned. + pub fn init>(path: P) -> Option { + let data = fs::read_to_string(path.as_ref()).ok()?; + + let lines = data.lines().count(); + + u64::try_from(lines).map(|value| Self { value }).ok() + } +} + +impl From for Metric { + fn from(value: u64) -> Self { + Self { value } + } +} + +impl Display for Metric { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(&self.value, f) + } +} + +#[test] +fn test_line_count() { + let metric = + Metric::init("tests/data/nemesis.txt").expect("Expected 'tests/data/nemesis.txt' to exist"); + + assert_eq!(metric.value, 4); +} diff --git a/src/disk_usage/file_size/mod.rs b/src/disk_usage/file_size/mod.rs new file mode 100644 index 00000000..fd210412 --- /dev/null +++ b/src/disk_usage/file_size/mod.rs @@ -0,0 +1,89 @@ +use crate::context::Context; +use clap::ValueEnum; +use std::{convert::From, ops::AddAssign}; + +/// Concerned with measuring file size in blocks. +#[cfg(unix)] +pub mod block; + +/// Concerned with measuring file size in bytes, logical or physical. +pub mod byte; + +/// Concerned with measuring file size by line count. +pub mod line_count; + +/// Concerned with measuring file size by word count. +pub mod word_count; + +/// Represents all the different ways in which a filesize could be reported using various metrics. +pub enum FileSize { + Word(word_count::Metric), + Line(line_count::Metric), + Byte(byte::Metric), + Block(block::Metric), +} + +/// Determines between logical or physical size for display +#[derive(Copy, Clone, Debug, ValueEnum, Default)] +pub enum DiskUsage { + /// How many bytes does a file contain + Logical, + + /// How much actual space on disk in bytes, taking into account sparse files and compression. + #[default] + Physical, + + /// How many total lines a file contains + Line, + + /// How many total words a file contains + Word, + + /// How many blocks are allocated to store the file + #[cfg(unix)] + Block, +} + +impl FileSize { + /// Extracts the inner value of [`FileSize`] which represents the file size for various metrics. + #[inline] + pub const fn value(&self) -> u64 { + match self { + Self::Byte(metric) => metric.value, + Self::Line(metric) => metric.value, + Self::Word(metric) => metric.value, + + #[cfg(unix)] + Self::Block(metric) => metric.value, + } + } +} + +impl AddAssign<&Self> for FileSize { + fn add_assign(&mut self, rhs: &Self) { + match self { + Self::Byte(metric) => metric.value += rhs.value(), + Self::Line(metric) => metric.value += rhs.value(), + Self::Word(metric) => metric.value += rhs.value(), + + #[cfg(unix)] + Self::Block(metric) => metric.value += rhs.value(), + } + } +} + +impl From<&Context> for FileSize { + fn from(ctx: &Context) -> Self { + use DiskUsage::{Line, Logical, Physical, Word}; + + match ctx.disk_usage { + Logical => Self::Byte(byte::Metric::init_empty_logical(ctx.human, ctx.unit)), + Physical => Self::Byte(byte::Metric::init_empty_physical(ctx.human, ctx.unit)), + Line => Self::Line(line_count::Metric::default()), + Word => Self::Word(word_count::Metric::default()), + + #[cfg(unix)] + DiskUsage::Block => Self::Block(block::Metric::default()), + } + } +} diff --git a/src/disk_usage/file_size/word_count.rs b/src/disk_usage/file_size/word_count.rs new file mode 100644 index 00000000..92ebf8d1 --- /dev/null +++ b/src/disk_usage/file_size/word_count.rs @@ -0,0 +1,47 @@ +use std::{ + convert::{AsRef, From}, + fmt::{self, Display}, + fs, + path::Path, +}; + +/// Concerned with measuring file size using word count as a metric. +#[derive(Default)] +pub struct Metric { + pub value: u64, +} + +impl Metric { + /// Reads in contents of a file given by `path` and attempts to compute the total number of + /// words in that file. If a file is not UTF-8 encoded as in the case of a binary jpeg file + /// then `None` will be returned. + /// + /// Words are UTF-8 encoded byte sequences delimited by Unicode Derived Core Property `White_Space`. + pub fn init>(path: P) -> Option { + let data = fs::read_to_string(path.as_ref()).ok()?; + + let words = data.split_whitespace().count(); + + u64::try_from(words).map(|value| Self { value }).ok() + } +} + +impl From for Metric { + fn from(value: u64) -> Self { + Self { value } + } +} + +impl Display for Metric { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + ::fmt(&self.value, f) + } +} + +#[test] +fn test_line_count() { + let metric = + Metric::init("tests/data/nemesis.txt").expect("Expected 'tests/data/nemesis.txt' to exist"); + + assert_eq!(metric.value, 27); +} diff --git a/src/disk_usage/mod.rs b/src/disk_usage/mod.rs index 5c325e2e..c855943f 100644 --- a/src/disk_usage/mod.rs +++ b/src/disk_usage/mod.rs @@ -1,5 +1,6 @@ /// Binary and SI prefixes pub mod units; -/// Rules to display disk usage for individual files +/// Concerned with all of the different ways to measure file size: bytes, word-count, line-count, +/// blocks (unix), etc.. pub mod file_size; diff --git a/src/disk_usage/units.rs b/src/disk_usage/units.rs index bf605c29..92e1da3f 100644 --- a/src/disk_usage/units.rs +++ b/src/disk_usage/units.rs @@ -64,9 +64,9 @@ impl UnitPrefix for BinPrefix { } /// Get the closest human-readable unit prefix for value. -impl From for BinPrefix { - fn from(value: f64) -> Self { - let log = value.log2(); +impl From for BinPrefix { + fn from(value: u64) -> Self { + let log = (value as f64).log2(); if log < 10. { Self::Base @@ -83,9 +83,9 @@ impl From for BinPrefix { } /// Get the closest human-readable unit prefix for value. -impl From for SiPrefix { - fn from(value: f64) -> Self { - let log = value.log10(); +impl From for SiPrefix { + fn from(value: u64) -> Self { + let log = (value as f64).log10(); if log < 3. { Self::Base diff --git a/src/icons/fs.rs b/src/icons/fs.rs new file mode 100644 index 00000000..b19f08f8 --- /dev/null +++ b/src/icons/fs.rs @@ -0,0 +1,95 @@ +use ansi_term::{ANSIGenericString, Style}; +use ignore::DirEntry; +use std::{borrow::Cow, path::Path}; + +/// Computes a plain, colorless icon with given parameters. +/// +/// The precedent from highest to lowest in terms of which parameters determine the icon used +/// is as followed: file-type, file-extension, and then file-name. If an icon cannot be +/// computed the fall-back default icon is used. +/// +/// If a directory entry is a link and the link target is provided, the link target will be +/// used to determine the icon. +pub fn compute(entry: &DirEntry, link_target: Option<&Path>) -> Cow<'static, str> { + let icon = entry + .file_type() + .and_then(super::icon_from_file_type) + .map(Cow::from); + + if let Some(i) = icon { + return i; + } + + let ext = match link_target { + Some(target) if entry.path_is_symlink() => target.extension(), + _ => entry.path().extension(), + }; + + let icon = ext + .and_then(super::icon_from_ext) + .map(|(_, i)| Cow::from(i)); + + if let Some(i) = icon { + return i; + } + + let icon = super::icon_from_file_name(entry.file_name()).map(Cow::from); + + if let Some(i) = icon { + return i; + } + + Cow::from(super::get_default_icon().1) +} + +/// Computes a plain, colored icon with given parameters. See [compute] for more details. +pub fn compute_with_color( + entry: &DirEntry, + link_target: Option<&Path>, + style: Option