Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/lsd.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ lsd is a ls command with a lot of pretty colours and some other stuff to enrich
`-I, --ignore-glob <pattern>...`
: Do not display files/directories with names matching the glob pattern(s). More than one can be specified by repeating the argument [default: ]

`--max-shown <num>`
: In tree layout, cap the number of entries shown at each depth level. Remaining entries are rolled up into a `... and N more` summary line

`--permission <permission>...`
: How to display permissions [default: rwx for linux, attributes for windows] [possible values: rwx, octal, attributes, disable]

Expand Down Expand Up @@ -149,6 +152,9 @@ lsd is a ls command with a lot of pretty colours and some other stuff to enrich
`--truncate-owner-marker`
: Truncation marker appended to a truncated user or group name

`--tree-filter <pattern>...`
: In tree layout, only display files matching the glob pattern(s). Directories are always shown. More than one can be specified by repeating the argument

# ARGS

`<FILE>...`
Expand Down
11 changes: 11 additions & 0 deletions doc/samples/config-sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,14 @@ truncate-owner:
after:
# String to be appended to a name if truncated.
marker: ""

# == Max Shown ==
# In tree layout, cap the number of entries shown at each depth level. Entries
# past the cap roll up into a "... and N more" summary line.
# max-shown: 10

# == Tree Filter ==
# In tree layout, only display files matching these globs. Directories are
# always shown. Multiple globs may be listed.
# tree-filter:
# - "*.rs"
8 changes: 8 additions & 0 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,14 @@ pub struct Cli {
#[arg(short = 'N', long)]
pub literal: bool,

/// only show entries matching glob in tree layout (repeatable, dirs always shown)
#[arg(long, value_name = "GLOB")]
pub tree_filter: Vec<String>,

/// max items to show per directory level in tree layout
#[arg(long, value_name = "NUM")]
pub max_shown: Option<usize>,

/// Print help information
#[arg(long, action = ArgAction::Help)]
help: (),
Expand Down
14 changes: 14 additions & 0 deletions src/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ pub struct Config {
pub header: Option<bool>,
pub literal: Option<bool>,
pub truncate_owner: Option<TruncateOwner>,
pub max_shown: Option<usize>,
pub tree_filter: Option<Vec<String>>,
}

#[derive(Eq, PartialEq, Debug, Deserialize)]
Expand Down Expand Up @@ -129,6 +131,8 @@ impl Config {
header: None,
literal: None,
truncate_owner: None,
max_shown: None,
tree_filter: None,
}
}

Expand Down Expand Up @@ -362,6 +366,14 @@ truncate-owner:
after:
# String to be appended to a name if truncated.
marker: ""
# == Max Shown ==
# max number of items to display per directory level in tree layout.
# max-shown: 10

# == Tree Filter ==
# only show entries matching these globs in tree layout. dirs always shown.
# tree-filter:
# - "*.rs"
"#;

#[cfg(test)]
Expand Down Expand Up @@ -432,6 +444,8 @@ mod tests {
after: None,
marker: Some("".to_string()),
}),
max_shown: None,
tree_filter: None,
},
c
);
Expand Down
159 changes: 153 additions & 6 deletions src/display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,12 +241,38 @@ fn inner_display_tree(
tree_index: usize,
) -> Vec<Cell> {
let mut cells = Vec::new();
let last_idx = metas.len();

for (idx, meta) in metas.iter().enumerate() {
// apply tree filter: dirs always shown, non-dirs must match a glob
let filtered: Vec<&Meta> = if !flags.tree_filter.0.is_empty() {
metas
.iter()
.filter(|m| {
matches!(m.file_type, FileType::Directory { .. })
|| matches!(m.file_type, FileType::SymLink { is_dir: true })
|| flags.tree_filter.0.is_match(m.name.file_name())
})
.collect()
} else {
metas.iter().collect()
};

// truncate to max_shown
let (display_metas, truncated) = if let Some(n) = flags.max_shown.0 {
if filtered.len() > n {
(&filtered[..n], filtered.len() - n)
} else {
(filtered.as_slice(), 0usize)
}
} else {
(filtered.as_slice(), 0usize)
};

let last_idx = display_metas.len();

for (idx, meta) in display_metas.iter().enumerate() {
let is_last = truncated == 0 && idx + 1 == last_idx;
let current_prefix = if tree_depth_prefix.0 > 0 {
if idx + 1 != last_idx {
// is last folder elem
if !is_last {
format!("{}{} ", tree_depth_prefix.1, EDGE)
} else {
format!("{}{} ", tree_depth_prefix.1, CORNER)
Expand Down Expand Up @@ -274,8 +300,7 @@ fn inner_display_tree(

if let Some(content) = &meta.content {
let new_prefix = if tree_depth_prefix.0 > 0 {
if idx + 1 != last_idx {
// is last folder elem
if !is_last {
format!("{}{} ", tree_depth_prefix.1, LINE)
} else {
format!("{}{} ", tree_depth_prefix.1, BLANK)
Expand All @@ -298,6 +323,30 @@ fn inner_display_tree(
}
}

if truncated > 0 {
let prefix = if tree_depth_prefix.0 > 0 {
format!("{}{} ", tree_depth_prefix.1, CORNER)
} else {
tree_depth_prefix.1.to_string()
};
let summary_text = format!("{}... and {} more", prefix, truncated);
let colored_summary = colors.colorize(&summary_text, &Elem::TreeEdge).to_string();

for i in 0..flags.blocks.0.len() {
if i == tree_index {
cells.push(Cell {
width: get_visible_width(&colored_summary, flags.hyperlink == HyperlinkOption::Always),
contents: colored_summary.clone(),
});
} else {
cells.push(Cell {
width: 0,
contents: String::new(),
});
}
}
}

cells
}

Expand Down Expand Up @@ -987,4 +1036,102 @@ mod tests {
drop(file);
drop(link);
}

#[test]
fn test_tree_with_max_shown_truncation() {
let argv = ["lsd", "--tree", "--max-shown", "2"];
let cli = Cli::try_parse_from(argv).unwrap();
let flags = Flags::configure_from(&cli, &Config::with_none()).unwrap();

let dir = assert_fs::TempDir::new().unwrap();
dir.child("a").touch().unwrap();
dir.child("b").touch().unwrap();
dir.child("c").touch().unwrap();
dir.child("d").touch().unwrap();

let mut metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx)
.unwrap()
.recurse_into(42, &flags, None)
.unwrap()
.0
.unwrap();
sort(&mut metas, &sort::assemble_sorters(&flags));

let output = tree(
&metas,
&flags,
&Colors::new(color::ThemeOption::NoColor),
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
&GitTheme::new(),
);

// first 2 items shown, summary line for remaining 2
assert!(output.contains("... and 2 more"), "summary line missing");
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines.len(), 3, "expected 2 items + summary line");
}

#[test]
fn test_tree_with_tree_filter() {
let argv = ["lsd", "--tree", "--tree-filter", "*.rs"];
let cli = Cli::try_parse_from(argv).unwrap();
let flags = Flags::configure_from(&cli, &Config::with_none()).unwrap();

let dir = assert_fs::TempDir::new().unwrap();
dir.child("subdir").create_dir_all().unwrap();
dir.child("main.rs").touch().unwrap();
dir.child("readme.md").touch().unwrap();

let mut metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx)
.unwrap()
.recurse_into(42, &flags, None)
.unwrap()
.0
.unwrap();
sort(&mut metas, &sort::assemble_sorters(&flags));

let output = tree(
&metas,
&flags,
&Colors::new(color::ThemeOption::NoColor),
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
&GitTheme::new(),
);

assert!(output.contains("main.rs"), "matching file should be shown");
assert!(output.contains("subdir"), "dirs should always be shown");
assert!(!output.contains("readme.md"), "non-matching file should be hidden");
}

#[test]
fn test_tree_with_filter_and_max_shown() {
let argv = ["lsd", "--tree", "--tree-filter", "*.rs", "--max-shown", "1"];
let cli = Cli::try_parse_from(argv).unwrap();
let flags = Flags::configure_from(&cli, &Config::with_none()).unwrap();

let dir = assert_fs::TempDir::new().unwrap();
dir.child("a.rs").touch().unwrap();
dir.child("b.rs").touch().unwrap();
dir.child("c.md").touch().unwrap();

let mut metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx)
.unwrap()
.recurse_into(42, &flags, None)
.unwrap()
.0
.unwrap();
sort(&mut metas, &sort::assemble_sorters(&flags));

let output = tree(
&metas,
&flags,
&Colors::new(color::ThemeOption::NoColor),
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
&GitTheme::new(),
);

// c.md excluded by filter; a.rs shown; b.rs in "... and 1 more"
assert!(!output.contains("c.md"), "non-matching file should be hidden");
assert!(output.contains("... and 1 more"), "summary should reflect filtered count");
}
}
9 changes: 9 additions & 0 deletions src/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ pub mod color;
pub mod date;
pub mod dereference;
pub mod display;
mod glob_helpers;
pub mod header;
pub mod hyperlink;
pub mod icons;
pub mod ignore_globs;
pub mod indicators;
pub mod layout;
pub mod literal;
pub mod max_shown;
pub mod permission;
pub mod recursion;
pub mod size;
pub mod sorting;
pub mod symlink_arrow;
pub mod symlinks;
pub mod total_size;
pub mod tree_filter;
pub mod truncate_owner;

pub use blocks::Blocks;
Expand All @@ -34,6 +37,7 @@ pub use ignore_globs::IgnoreGlobs;
pub use indicators::Indicators;
pub use layout::Layout;
pub use literal::Literal;
pub use max_shown::MaxShown;
pub use permission::PermissionFlag;
pub use recursion::Recursion;
pub use size::SizeFlag;
Expand All @@ -44,6 +48,7 @@ pub use sorting::Sorting;
pub use symlink_arrow::SymlinkArrow;
pub use symlinks::NoSymlink;
pub use total_size::TotalSize;
pub use tree_filter::TreeFilter;
pub use truncate_owner::TruncateOwner;

use crate::app::Cli;
Expand Down Expand Up @@ -77,6 +82,8 @@ pub struct Flags {
pub header: Header,
pub literal: Literal,
pub truncate_owner: TruncateOwner,
pub max_shown: MaxShown,
pub tree_filter: TreeFilter,
}

impl Flags {
Expand Down Expand Up @@ -108,6 +115,8 @@ impl Flags {
header: Header::configure_from(cli, config),
literal: Literal::configure_from(cli, config),
truncate_owner: TruncateOwner::configure_from(cli, config),
max_shown: MaxShown::configure_from(cli, config),
tree_filter: TreeFilter::configure_from(cli, config)?,
})
}
}
Expand Down
13 changes: 13 additions & 0 deletions src/flags/glob_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use clap::Error;
use clap::error::ErrorKind;
use globset::{Glob, GlobSet, GlobSetBuilder};

pub fn create_glob(pattern: &str) -> Result<Glob, Error> {
Glob::new(pattern).map_err(|err| Error::raw(ErrorKind::ValueValidation, err))
}

pub fn create_glob_set(builder: &GlobSetBuilder) -> Result<GlobSet, Error> {
builder
.build()
.map_err(|err| Error::raw(ErrorKind::ValueValidation, err))
}
Loading