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.
This commit is contained in:
Bertrand Bonnefoy-Claudet 2023-08-01 12:26:35 +02:00 committed by Wei Zhang
parent 762e724f2a
commit 93b3fb0b66
10 changed files with 262 additions and 8 deletions

View file

@ -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

View file

@ -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: ""
```
</details>

View file

@ -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
`<FILE>...`

View file

@ -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<usize>,
/// Truncation marker appended to a truncated user or group name
#[arg(long, value_name = "STR")]
pub truncate_owner_marker: Option<String>,
/// Includes files with the windows system protection flag set.
/// This is the same as --all on other platforms
#[arg(long, hide = !cfg!(windows))]

View file

@ -45,6 +45,7 @@ pub struct Config {
pub symlink_arrow: Option<String>,
pub hyperlink: Option<HyperlinkOption>,
pub header: Option<bool>,
pub truncate_owner: Option<TruncateOwner>,
}
#[derive(Eq, PartialEq, Debug, Deserialize)]
@ -74,6 +75,12 @@ pub struct Sorting {
pub dir_grouping: Option<DirGrouping>,
}
#[derive(Eq, PartialEq, Debug, Deserialize)]
pub struct TruncateOwner {
pub after: Option<usize>,
pub marker: Option<String>,
}
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
);

View file

@ -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 {

View file

@ -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,
})
}

120
src/flags/truncate_owner.rs Normal file
View file

@ -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<usize>,
pub marker: Option<String>,
}
impl Configurable<Self> 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<Self> {
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<Self> {
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)
);
}
}

View file

@ -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<usize>, marker: Option<String>) -> 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())));
}
}

View file

@ -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() {