Skip to content

Commit

Permalink
Add configuration and CLI options: truncate owner
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bbc2 authored and zwpaper committed Sep 17, 2023
1 parent 762e724 commit 93b3fb0
Show file tree
Hide file tree
Showing 10 changed files with 261 additions and 7 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
6 changes: 6 additions & 0 deletions doc/lsd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>...`
Expand Down
8 changes: 8 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))]
Expand Down
21 changes: 21 additions & 0 deletions src/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 {
Expand All @@ -97,6 +104,7 @@ impl Config {
symlink_arrow: None,
hyperlink: None,
header: None,
truncate_owner: None,
}
}

Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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
);
Expand Down
4 changes: 2 additions & 2 deletions src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
}

Expand Down Expand Up @@ -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,
})
}
Expand Down
120 changes: 120 additions & 0 deletions src/flags/truncate_owner.rs
Original file line number Diff line number Diff line change
@@ -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)
);
}
}
69 changes: 65 additions & 4 deletions src/meta/owner.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::color::{ColoredString, Colors, Elem};
use crate::Flags;
#[cfg(unix)]
use std::fs::Metadata;

Expand Down Expand Up @@ -35,12 +36,72 @@ impl From<&Metadata> for Owner {
}
}

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);
}
}
}

output
}

impl Owner {
pub fn render_user(&self, colors: &Colors) -> ColoredString {
colors.colorize(self.user.clone(), &Elem::User)
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));
}

pub fn render_group(&self, colors: &Colors) -> ColoredString {
colors.colorize(self.group.clone(), &Elem::Group)
#[test]
fn test_truncated_with_marker() {
assert_eq!("a…", truncate("ab", Some(1), Some("…".to_string())));
}
}
18 changes: 18 additions & 0 deletions tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 93b3fb0

Please sign in to comment.