From 93b3fb0b666e3474600c209d17cda0112a80823f Mon Sep 17 00:00:00 2001 From: Bertrand Bonnefoy-Claudet Date: Tue, 1 Aug 2023 12:26:35 +0200 Subject: [PATCH] Add configuration and CLI options: truncate owner This adds the following CLI flags: - `--truncate-owner-after` - `--truncate-owner-marker` And the following configuration fields: ```yaml truncate-owner: after: marker: "" ``` The default behavior of LSD is unchanged. The problem this change attempts to solve is the usability of the `-l` flag on systems where some user or group names are long but cannot be changed (e.g. the user is not admin and the account is managed in a central directory). In such cases, even with a decently sized terminal (90+ characters wide), lines often overflow, making the directory listing hard to read. Without this change, the only mitigation would consist in turning off the display of file ownership (via the `blocks` configuration field) which is unsatisfactory because ownership information is very useful. --- CHANGELOG.md | 9 ++- README.md | 9 +++ doc/lsd.md | 6 ++ src/app.rs | 8 +++ src/config_file.rs | 21 +++++++ src/display.rs | 4 +- src/flags.rs | 4 ++ src/flags/truncate_owner.rs | 120 ++++++++++++++++++++++++++++++++++++ src/meta/owner.rs | 71 +++++++++++++++++++-- tests/integration.rs | 18 ++++++ 10 files changed, 262 insertions(+), 8 deletions(-) create mode 100644 src/flags/truncate_owner.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ee01ae..9695645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added +- Add CLI parameters `--truncate-owner-after` and `--truncate-owner-marker` (and equivalent + configuration fields) to truncate user and group names if they exceed a certain number + of characters (disabled by default). + ## [v1.0.0] - 2023-08-25 ### Added @@ -391,7 +398,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Change the component alignment by using term_grid - +[Unreleased]: https://github.com/lsd-rs/lsd/compare/v1.0.0...HEAD [v1.0.0]: https://github.com/lsd-rs/lsd/compare/0.23.1...v1.0.0 [0.23.1]: https://github.com/Peltoche/lsd/compare/0.23.0...0.23.1 [0.23.0]: https://github.com/Peltoche/lsd/compare/0.22.0...0.23.0 diff --git a/README.md b/README.md index f386aee..ae46daa 100644 --- a/README.md +++ b/README.md @@ -228,6 +228,15 @@ symlink-arrow: ⇒ # Whether to display block headers. # Possible values: false, true header: false + +# == Truncate owner == +# How to truncate the username and group names for a file if they exceed a certain +# number of characters. +truncate-owner: + # Number of characters to keep. By default, no truncation is done (empty value). + after: + # String to be appended to a name if truncated. + marker: "" ``` diff --git a/doc/lsd.md b/doc/lsd.md index b86cdd0..636913e 100644 --- a/doc/lsd.md +++ b/doc/lsd.md @@ -140,6 +140,12 @@ lsd is a ls command with a lot of pretty colours and some other stuff to enrich `--header` : Display block headers +`--truncate-owner-after` +: Truncate the user and group names if they exceed a certain number of characters + +`--truncate-owner-marker` +: Truncation marker appended to a truncated user or group name + # ARGS `...` diff --git a/src/app.rs b/src/app.rs index bd6defa..a288eba 100644 --- a/src/app.rs +++ b/src/app.rs @@ -176,6 +176,14 @@ pub struct Cli { #[arg(long)] pub header: bool, + /// Truncate the user and group names if they exceed a certain number of characters + #[arg(long, value_name = "NUM")] + pub truncate_owner_after: Option, + + /// Truncation marker appended to a truncated user or group name + #[arg(long, value_name = "STR")] + pub truncate_owner_marker: Option, + /// Includes files with the windows system protection flag set. /// This is the same as --all on other platforms #[arg(long, hide = !cfg!(windows))] diff --git a/src/config_file.rs b/src/config_file.rs index c9392c8..f6f9d68 100644 --- a/src/config_file.rs +++ b/src/config_file.rs @@ -45,6 +45,7 @@ pub struct Config { pub symlink_arrow: Option, pub hyperlink: Option, pub header: Option, + pub truncate_owner: Option, } #[derive(Eq, PartialEq, Debug, Deserialize)] @@ -74,6 +75,12 @@ pub struct Sorting { pub dir_grouping: Option, } +#[derive(Eq, PartialEq, Debug, Deserialize)] +pub struct TruncateOwner { + pub after: Option, + pub marker: Option, +} + impl Config { /// This constructs a Config struct with all None pub fn with_none() -> Self { @@ -97,6 +104,7 @@ impl Config { symlink_arrow: None, hyperlink: None, header: None, + truncate_owner: None, } } @@ -323,6 +331,15 @@ hyperlink: never # == Symlink arrow == # Specifies how the symlink arrow display, chars in both ascii and utf8 symlink-arrow: ⇒ + +# == Truncate owner == +# How to truncate the username and group name for the file if they exceed a +# certain number of characters. +truncate-owner: + # Number of characters to keep. By default, no truncation is done (empty value). + after: + # String to be appended to a name if truncated. + marker: "" "#; #[cfg(test)] @@ -389,6 +406,10 @@ mod tests { symlink_arrow: Some("⇒".into()), hyperlink: Some(HyperlinkOption::Never), header: None, + truncate_owner: Some(config_file::TruncateOwner { + after: None, + marker: Some("".to_string()), + }), }, c ); diff --git a/src/display.rs b/src/display.rs index 6fea6e9..bf788fa 100644 --- a/src/display.rs +++ b/src/display.rs @@ -355,11 +355,11 @@ fn get_output( ]); } Block::User => block_vec.push(match &meta.owner { - Some(owner) => owner.render_user(colors), + Some(owner) => owner.render_user(colors, flags), None => colorize_missing("?"), }), Block::Group => block_vec.push(match &meta.owner { - Some(owner) => owner.render_group(colors), + Some(owner) => owner.render_group(colors, flags), None => colorize_missing("?"), }), Block::Context => block_vec.push(match &meta.access_control { diff --git a/src/flags.rs b/src/flags.rs index a73ac65..e02deb9 100644 --- a/src/flags.rs +++ b/src/flags.rs @@ -16,6 +16,7 @@ pub mod sorting; pub mod symlink_arrow; pub mod symlinks; pub mod total_size; +pub mod truncate_owner; pub use blocks::Blocks; pub use color::Color; @@ -42,6 +43,7 @@ pub use sorting::Sorting; pub use symlink_arrow::SymlinkArrow; pub use symlinks::NoSymlink; pub use total_size::TotalSize; +pub use truncate_owner::TruncateOwner; use crate::app::Cli; use crate::config_file::Config; @@ -72,6 +74,7 @@ pub struct Flags { pub symlink_arrow: SymlinkArrow, pub hyperlink: HyperlinkOption, pub header: Header, + pub truncate_owner: TruncateOwner, pub should_quote: bool, } @@ -102,6 +105,7 @@ impl Flags { symlink_arrow: SymlinkArrow::configure_from(cli, config), hyperlink: HyperlinkOption::configure_from(cli, config), header: Header::configure_from(cli, config), + truncate_owner: TruncateOwner::configure_from(cli, config), should_quote: true, }) } diff --git a/src/flags/truncate_owner.rs b/src/flags/truncate_owner.rs new file mode 100644 index 0000000..807dc40 --- /dev/null +++ b/src/flags/truncate_owner.rs @@ -0,0 +1,120 @@ +//! This module defines the [TruncateOwner] flag. To set it up from [Cli], a [Config] and its +//! [Default] value, use the [configure_from](Configurable::configure_from) method. + +use super::Configurable; +use crate::app::Cli; + +use crate::config_file::Config; + +/// The flag showing how to truncate user and group names. +#[derive(Clone, Debug, PartialEq, Eq, Default)] +pub struct TruncateOwner { + pub after: Option, + pub marker: Option, +} + +impl Configurable for TruncateOwner { + /// Get a potential `TruncateOwner` value from [Cli]. + /// + /// If the "header" argument is passed, this returns a `TruncateOwner` with value `true` in a + /// [Some]. Otherwise this returns [None]. + fn from_cli(cli: &Cli) -> Option { + match (cli.truncate_owner_after, cli.truncate_owner_marker.clone()) { + (None, None) => None, + (after, marker) => Some(Self { after, marker }), + } + } + + /// Get a potential `TruncateOwner` value from a [Config]. + /// + /// If the `Config::truncate_owner` has value, + /// this returns it as the value of the `TruncateOwner`, in a [Some]. + /// Otherwise this returns [None]. + fn from_config(config: &Config) -> Option { + config.truncate_owner.as_ref().map(|c| Self { + after: c.after, + marker: c.marker.clone(), + }) + } +} + +#[cfg(test)] +mod test { + use clap::Parser; + + use super::TruncateOwner; + + use crate::app::Cli; + use crate::config_file::{self, Config}; + use crate::flags::Configurable; + + #[test] + fn test_from_cli_none() { + let argv = ["lsd"]; + let cli = Cli::try_parse_from(argv).unwrap(); + assert_eq!(None, TruncateOwner::from_cli(&cli)); + } + + #[test] + fn test_from_cli_after_some() { + let argv = ["lsd", "--truncate-owner-after", "1"]; + let cli = Cli::try_parse_from(argv).unwrap(); + assert_eq!( + Some(TruncateOwner { + after: Some(1), + marker: None, + }), + TruncateOwner::from_cli(&cli) + ); + } + + #[test] + fn test_from_cli_marker_some() { + let argv = ["lsd", "--truncate-owner-marker", "…"]; + let cli = Cli::try_parse_from(argv).unwrap(); + assert_eq!( + Some(TruncateOwner { + after: None, + marker: Some("…".to_string()), + }), + TruncateOwner::from_cli(&cli) + ); + } + + #[test] + fn test_from_config_none() { + assert_eq!(None, TruncateOwner::from_config(&Config::with_none())); + } + + #[test] + fn test_from_config_all_fields_none() { + let mut c = Config::with_none(); + c.truncate_owner = Some(config_file::TruncateOwner { + after: None, + marker: None, + }); + assert_eq!( + Some(TruncateOwner { + after: None, + marker: None, + }), + TruncateOwner::from_config(&c) + ); + } + + #[test] + fn test_from_config_all_fields_some() { + let mut c = Config::with_none(); + c.truncate_owner = Some(config_file::TruncateOwner { + after: Some(1), + marker: Some(">".to_string()), + }); + assert_eq!( + Some(TruncateOwner { + after: Some(1), + marker: Some(">".to_string()), + }), + TruncateOwner::from_config(&c) + ); + } +} diff --git a/src/meta/owner.rs b/src/meta/owner.rs index ea48736..4c54df3 100644 --- a/src/meta/owner.rs +++ b/src/meta/owner.rs @@ -1,4 +1,5 @@ use crate::color::{ColoredString, Colors, Elem}; +use crate::Flags; #[cfg(unix)] use std::fs::Metadata; @@ -35,12 +36,72 @@ impl From<&Metadata> for Owner { } } -impl Owner { - pub fn render_user(&self, colors: &Colors) -> ColoredString { - colors.colorize(self.user.clone(), &Elem::User) +fn truncate(input: &str, after: Option, marker: Option) -> String { + let mut output = input.to_string(); + + if let Some(after) = after { + if output.len() > after { + output.truncate(after); + + if let Some(marker) = marker { + output.push_str(&marker); + } + } } - pub fn render_group(&self, colors: &Colors) -> ColoredString { - colors.colorize(self.group.clone(), &Elem::Group) + output +} + +impl Owner { + pub fn render_user(&self, colors: &Colors, flags: &Flags) -> ColoredString { + colors.colorize( + truncate( + &self.user, + flags.truncate_owner.after, + flags.truncate_owner.marker.clone(), + ), + &Elem::User, + ) + } + + pub fn render_group(&self, colors: &Colors, flags: &Flags) -> ColoredString { + colors.colorize( + truncate( + &self.group, + flags.truncate_owner.after, + flags.truncate_owner.marker.clone(), + ), + &Elem::Group, + ) + } +} + +#[cfg(test)] +mod test_truncate { + use crate::meta::owner::truncate; + + #[test] + fn test_none() { + assert_eq!("a", truncate("a", None, None)); + } + + #[test] + fn test_unchanged_without_marker() { + assert_eq!("a", truncate("a", Some(1), None)); + } + + #[test] + fn test_unchanged_with_marker() { + assert_eq!("a", truncate("a", Some(1), Some("…".to_string()))); + } + + #[test] + fn test_truncated_without_marker() { + assert_eq!("a", truncate("ab", Some(1), None)); + } + + #[test] + fn test_truncated_with_marker() { + assert_eq!("a…", truncate("ab", Some(1), Some("…".to_string()))); } } diff --git a/tests/integration.rs b/tests/integration.rs index a43ce35..f200057 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -659,6 +659,24 @@ fn test_upper_case_ext_icon_match() { .stdout(predicate::str::contains("\u{f410}")); } +#[cfg(unix)] +#[test] +fn test_truncate_owner() { + let dir = tempdir(); + dir.child("foo").touch().unwrap(); + + cmd() + .arg("-l") + .arg("--ignore-config") + .arg("--truncate-owner-after") + .arg("1") + .arg("--truncate-owner-marker") + .arg("…") + .arg(dir.path()) + .assert() + .stdout(predicate::str::is_match(" .… .… ").unwrap()); +} + #[cfg(unix)] #[test] fn test_custom_config_file_parsing() {