mirror of
https://github.com/lsd-rs/lsd.git
synced 2026-02-11 01:34:46 +02:00
991 lines
31 KiB
Rust
991 lines
31 KiB
Rust
use crate::color::{Colors, Elem};
|
||
use crate::flags::blocks::Block;
|
||
use crate::flags::{Display, Flags, HyperlinkOption, Layout};
|
||
use crate::git_theme::GitTheme;
|
||
use crate::icon::Icons;
|
||
use crate::meta::name::DisplayOption;
|
||
use crate::meta::{FileType, Meta, OwnerCache};
|
||
use std::collections::HashMap;
|
||
use term_grid::{Cell, Direction, Filling, Grid, GridOptions};
|
||
use terminal_size::terminal_size;
|
||
use unicode_width::UnicodeWidthStr;
|
||
|
||
const EDGE: &str = "\u{251c}\u{2500}\u{2500}"; // "├──"
|
||
const LINE: &str = "\u{2502} "; // "│ "
|
||
const CORNER: &str = "\u{2514}\u{2500}\u{2500}"; // "└──"
|
||
const BLANK: &str = " ";
|
||
|
||
pub fn grid(
|
||
metas: &[Meta],
|
||
flags: &Flags,
|
||
colors: &Colors,
|
||
icons: &Icons,
|
||
git_theme: &GitTheme,
|
||
) -> String {
|
||
let term_width = terminal_size().map(|(w, _)| w.0 as usize);
|
||
let owner_cache = OwnerCache::default();
|
||
|
||
inner_display_grid(
|
||
&DisplayOption::None,
|
||
metas,
|
||
&owner_cache,
|
||
flags,
|
||
colors,
|
||
icons,
|
||
git_theme,
|
||
0,
|
||
term_width,
|
||
)
|
||
}
|
||
|
||
pub fn tree(
|
||
metas: &[Meta],
|
||
flags: &Flags,
|
||
colors: &Colors,
|
||
icons: &Icons,
|
||
git_theme: &GitTheme,
|
||
) -> String {
|
||
let mut grid = Grid::new(GridOptions {
|
||
filling: Filling::Spaces(1),
|
||
direction: Direction::LeftToRight,
|
||
});
|
||
|
||
let padding_rules = get_padding_rules(metas, flags);
|
||
let mut index = 0;
|
||
for (i, block) in flags.blocks.0.iter().enumerate() {
|
||
if block == &Block::Name {
|
||
index = i;
|
||
break;
|
||
}
|
||
}
|
||
|
||
let owner_cache = OwnerCache::default();
|
||
|
||
for cell in inner_display_tree(
|
||
metas,
|
||
&owner_cache,
|
||
flags,
|
||
colors,
|
||
icons,
|
||
git_theme,
|
||
(0, ""),
|
||
&padding_rules,
|
||
index,
|
||
) {
|
||
grid.add(cell);
|
||
}
|
||
|
||
grid.fit_into_columns(flags.blocks.0.len()).to_string()
|
||
}
|
||
|
||
#[allow(clippy::too_many_arguments)] // should wrap flags, colors, icons, git_theme into one struct
|
||
fn inner_display_grid(
|
||
display_option: &DisplayOption,
|
||
metas: &[Meta],
|
||
owner_cache: &OwnerCache,
|
||
flags: &Flags,
|
||
colors: &Colors,
|
||
icons: &Icons,
|
||
git_theme: &GitTheme,
|
||
depth: usize,
|
||
term_width: Option<usize>,
|
||
) -> String {
|
||
let mut output = String::new();
|
||
let mut cells = Vec::new();
|
||
|
||
let padding_rules = get_padding_rules(metas, flags);
|
||
let mut grid = match flags.layout {
|
||
Layout::OneLine => Grid::new(GridOptions {
|
||
filling: Filling::Spaces(1),
|
||
direction: Direction::LeftToRight,
|
||
}),
|
||
_ => Grid::new(GridOptions {
|
||
filling: Filling::Spaces(2),
|
||
direction: Direction::TopToBottom,
|
||
}),
|
||
};
|
||
|
||
// The first iteration (depth == 0) corresponds to the inputs given by the
|
||
// user. We defer displaying directories given by the user unless we've been
|
||
// asked to display the directory itself (rather than its contents).
|
||
let skip_dirs = (depth == 0) && (flags.display != Display::DirectoryOnly);
|
||
|
||
// print the files first.
|
||
for meta in metas {
|
||
// Maybe skip showing the directory meta now; show its contents later.
|
||
if skip_dirs
|
||
&& (matches!(meta.file_type, FileType::Directory { .. })
|
||
|| (matches!(meta.file_type, FileType::SymLink { is_dir: true }))
|
||
&& flags.blocks.0.len() == 1)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
let blocks = get_output(
|
||
meta,
|
||
owner_cache,
|
||
colors,
|
||
icons,
|
||
git_theme,
|
||
flags,
|
||
display_option,
|
||
&padding_rules,
|
||
(0, ""),
|
||
);
|
||
|
||
for block in blocks {
|
||
cells.push(Cell {
|
||
width: get_visible_width(&block, flags.hyperlink == HyperlinkOption::Always),
|
||
contents: block,
|
||
});
|
||
}
|
||
}
|
||
|
||
// Print block headers
|
||
if flags.header.0 && flags.layout == Layout::OneLine && !cells.is_empty() {
|
||
add_header(flags, &cells, &mut grid);
|
||
}
|
||
|
||
for cell in cells {
|
||
grid.add(cell);
|
||
}
|
||
|
||
if flags.layout == Layout::Grid {
|
||
if let Some(tw) = term_width {
|
||
if let Some(gridded_output) = grid.fit_into_width(tw) {
|
||
output += &gridded_output.to_string();
|
||
} else {
|
||
//does not fit into grid, usually because (some) filename(s)
|
||
//are longer or almost as long as term_width
|
||
//print line by line instead!
|
||
output += &grid.fit_into_columns(1).to_string();
|
||
}
|
||
} else {
|
||
output += &grid.fit_into_columns(1).to_string();
|
||
}
|
||
} else {
|
||
output += &grid.fit_into_columns(flags.blocks.0.len()).to_string();
|
||
}
|
||
|
||
let should_display_folder_path = should_display_folder_path(depth, metas);
|
||
|
||
// print the folder content
|
||
for meta in metas {
|
||
if let Some(content) = &meta.content {
|
||
if should_display_folder_path {
|
||
output += &display_folder_path(meta);
|
||
}
|
||
|
||
let display_option = DisplayOption::Relative {
|
||
base_path: &meta.path,
|
||
};
|
||
|
||
output += &inner_display_grid(
|
||
&display_option,
|
||
content,
|
||
owner_cache,
|
||
flags,
|
||
colors,
|
||
icons,
|
||
git_theme,
|
||
depth + 1,
|
||
term_width,
|
||
);
|
||
}
|
||
}
|
||
|
||
output
|
||
}
|
||
|
||
fn add_header(flags: &Flags, cells: &[Cell], grid: &mut Grid) {
|
||
let num_columns: usize = flags.blocks.0.len();
|
||
|
||
let mut widths = flags
|
||
.blocks
|
||
.0
|
||
.iter()
|
||
.map(|b| get_visible_width(b.get_header(), flags.hyperlink == HyperlinkOption::Always))
|
||
.collect::<Vec<usize>>();
|
||
|
||
// find max widths of each column
|
||
for (index, cell) in cells.iter().enumerate() {
|
||
let index = index % num_columns;
|
||
widths[index] = std::cmp::max(widths[index], cell.width);
|
||
}
|
||
|
||
for (idx, block) in flags.blocks.0.iter().enumerate() {
|
||
// center and underline header
|
||
let underlined_header = crossterm::style::Stylize::attribute(
|
||
format!("{: ^1$}", block.get_header(), widths[idx]),
|
||
crossterm::style::Attribute::Underlined,
|
||
)
|
||
.to_string();
|
||
|
||
grid.add(Cell {
|
||
width: widths[idx],
|
||
contents: underlined_header,
|
||
});
|
||
}
|
||
}
|
||
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn inner_display_tree(
|
||
metas: &[Meta],
|
||
owner_cache: &OwnerCache,
|
||
flags: &Flags,
|
||
colors: &Colors,
|
||
icons: &Icons,
|
||
git_theme: &GitTheme,
|
||
tree_depth_prefix: (usize, &str),
|
||
padding_rules: &HashMap<Block, usize>,
|
||
tree_index: usize,
|
||
) -> Vec<Cell> {
|
||
let mut cells = Vec::new();
|
||
let last_idx = metas.len();
|
||
|
||
for (idx, meta) in metas.iter().enumerate() {
|
||
let current_prefix = if tree_depth_prefix.0 > 0 {
|
||
if idx + 1 != last_idx {
|
||
// is last folder elem
|
||
format!("{}{} ", tree_depth_prefix.1, EDGE)
|
||
} else {
|
||
format!("{}{} ", tree_depth_prefix.1, CORNER)
|
||
}
|
||
} else {
|
||
tree_depth_prefix.1.to_string()
|
||
};
|
||
|
||
for block in get_output(
|
||
meta,
|
||
owner_cache,
|
||
colors,
|
||
icons,
|
||
git_theme,
|
||
flags,
|
||
&DisplayOption::FileName,
|
||
padding_rules,
|
||
(tree_index, ¤t_prefix),
|
||
) {
|
||
cells.push(Cell {
|
||
width: get_visible_width(&block, flags.hyperlink == HyperlinkOption::Always),
|
||
contents: block,
|
||
});
|
||
}
|
||
|
||
if let Some(content) = &meta.content {
|
||
let new_prefix = if tree_depth_prefix.0 > 0 {
|
||
if idx + 1 != last_idx {
|
||
// is last folder elem
|
||
format!("{}{} ", tree_depth_prefix.1, LINE)
|
||
} else {
|
||
format!("{}{} ", tree_depth_prefix.1, BLANK)
|
||
}
|
||
} else {
|
||
tree_depth_prefix.1.to_string()
|
||
};
|
||
|
||
cells.extend(inner_display_tree(
|
||
content,
|
||
owner_cache,
|
||
flags,
|
||
colors,
|
||
icons,
|
||
git_theme,
|
||
(tree_depth_prefix.0 + 1, &new_prefix),
|
||
padding_rules,
|
||
tree_index,
|
||
));
|
||
}
|
||
}
|
||
|
||
cells
|
||
}
|
||
|
||
fn should_display_folder_path(depth: usize, metas: &[Meta]) -> bool {
|
||
if depth > 0 {
|
||
true
|
||
} else {
|
||
let folder_number = metas
|
||
.iter()
|
||
.filter(|x| {
|
||
matches!(x.file_type, FileType::Directory { .. })
|
||
|| (matches!(x.file_type, FileType::SymLink { is_dir: true }))
|
||
})
|
||
.count();
|
||
|
||
folder_number > 1 || folder_number < metas.len()
|
||
}
|
||
}
|
||
|
||
fn display_folder_path(meta: &Meta) -> String {
|
||
format!("\n{}:\n", meta.path.to_string_lossy())
|
||
}
|
||
|
||
#[allow(clippy::too_many_arguments)]
|
||
fn get_output(
|
||
meta: &Meta,
|
||
owner_cache: &OwnerCache,
|
||
colors: &Colors,
|
||
icons: &Icons,
|
||
git_theme: &GitTheme,
|
||
flags: &Flags,
|
||
display_option: &DisplayOption,
|
||
padding_rules: &HashMap<Block, usize>,
|
||
tree: (usize, &str),
|
||
) -> Vec<String> {
|
||
let mut strings: Vec<String> = Vec::new();
|
||
let colorize_missing = |string: &str| colors.colorize(string, &Elem::NoAccess);
|
||
|
||
for (i, block) in flags.blocks.0.iter().enumerate() {
|
||
let mut block_vec = if Layout::Tree == flags.layout && tree.0 == i {
|
||
vec![colors.colorize(tree.1, &Elem::TreeEdge)]
|
||
} else {
|
||
Vec::new()
|
||
};
|
||
|
||
match block {
|
||
Block::INode => block_vec.push(match &meta.inode {
|
||
Some(inode) => inode.render(colors),
|
||
None => colorize_missing("?"),
|
||
}),
|
||
Block::Links => block_vec.push(match &meta.links {
|
||
Some(links) => links.render(colors),
|
||
None => colorize_missing("?"),
|
||
}),
|
||
Block::Permission => {
|
||
block_vec.extend([
|
||
meta.file_type.render(colors),
|
||
match &meta.permissions_or_attributes {
|
||
Some(permissions_or_attributes) => {
|
||
permissions_or_attributes.render(colors, flags)
|
||
}
|
||
None => colorize_missing("?????????"),
|
||
},
|
||
match &meta.access_control {
|
||
Some(access_control) => access_control.render_method(colors),
|
||
None => colorize_missing(""),
|
||
},
|
||
]);
|
||
}
|
||
Block::User => block_vec.push(match &meta.owner {
|
||
Some(owner) => owner.render_user(colors, owner_cache, flags),
|
||
None => colorize_missing("?"),
|
||
}),
|
||
Block::Group => block_vec.push(match &meta.owner {
|
||
Some(owner) => owner.render_group(colors, owner_cache, flags),
|
||
None => colorize_missing("?"),
|
||
}),
|
||
Block::Context => block_vec.push(match &meta.access_control {
|
||
Some(access_control) => access_control.render_context(colors),
|
||
None => colorize_missing("?"),
|
||
}),
|
||
Block::Size => {
|
||
let pad = if Layout::Tree == flags.layout && 0 == tree.0 && 0 == i {
|
||
None
|
||
} else {
|
||
Some(padding_rules[&Block::SizeValue])
|
||
};
|
||
block_vec.push(match &meta.size {
|
||
Some(size) => size.render(colors, flags, pad),
|
||
None => colorize_missing("?"),
|
||
})
|
||
}
|
||
Block::SizeValue => block_vec.push(match &meta.size {
|
||
Some(size) => size.render_value(colors, flags),
|
||
None => colorize_missing("?"),
|
||
}),
|
||
Block::Date => block_vec.push(match &meta.date {
|
||
Some(date) => date.render(colors, flags),
|
||
None => colorize_missing("?"),
|
||
}),
|
||
Block::Name => {
|
||
block_vec.extend([
|
||
meta.name.render(
|
||
colors,
|
||
icons,
|
||
display_option,
|
||
flags.hyperlink,
|
||
flags.literal.0,
|
||
),
|
||
meta.indicator.render(flags),
|
||
]);
|
||
if !(flags.no_symlink.0 || flags.dereference.0 || flags.layout == Layout::Grid) {
|
||
block_vec.push(meta.symlink.render(colors, flags))
|
||
}
|
||
}
|
||
Block::GitStatus => {
|
||
if let Some(_s) = &meta.git_status {
|
||
block_vec.push(_s.render(colors, git_theme));
|
||
}
|
||
}
|
||
};
|
||
strings.push(
|
||
block_vec
|
||
.into_iter()
|
||
.map(|s| s.to_string())
|
||
.collect::<Vec<String>>()
|
||
.join(""),
|
||
);
|
||
}
|
||
strings
|
||
}
|
||
|
||
fn get_visible_width(input: &str, hyperlink: bool) -> usize {
|
||
let mut nb_invisible_char = 0;
|
||
|
||
// If the input has color, do not compute the length contributed by the color to the actual length
|
||
for (idx, _) in input.match_indices("\u{1b}[") {
|
||
let (_, s) = input.split_at(idx);
|
||
|
||
let m_pos = s.find('m');
|
||
if let Some(len) = m_pos {
|
||
// len points to the 'm' character, we must include 'm' to invisible characters
|
||
nb_invisible_char += len + 1;
|
||
}
|
||
}
|
||
|
||
if hyperlink {
|
||
for (idx, _) in input.match_indices("\x1B]8;;") {
|
||
let (_, s) = input.split_at(idx);
|
||
|
||
let m_pos = s.find("\x1B\x5C");
|
||
if let Some(len) = m_pos {
|
||
// len points to the '\x1B' character, we must include both '\x1B' and '\x5C' to invisible characters
|
||
nb_invisible_char += len + 2
|
||
}
|
||
}
|
||
}
|
||
|
||
// `UnicodeWidthStr::width` counts all unicode characters including escape '\u{1b}' and hyperlink '\x1B'
|
||
UnicodeWidthStr::width(input) - nb_invisible_char
|
||
}
|
||
|
||
fn detect_size_lengths(metas: &[Meta], flags: &Flags) -> usize {
|
||
let mut max_value_length: usize = 0;
|
||
|
||
for meta in metas {
|
||
let value_len = match &meta.size {
|
||
Some(size) => size.value_string(flags).len(),
|
||
None => 0,
|
||
};
|
||
|
||
if value_len > max_value_length {
|
||
max_value_length = value_len;
|
||
}
|
||
|
||
if Layout::Tree == flags.layout {
|
||
if let Some(subs) = &meta.content {
|
||
let sub_length = detect_size_lengths(subs, flags);
|
||
if sub_length > max_value_length {
|
||
max_value_length = sub_length;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
max_value_length
|
||
}
|
||
|
||
fn get_padding_rules(metas: &[Meta], flags: &Flags) -> HashMap<Block, usize> {
|
||
let mut padding_rules: HashMap<Block, usize> = HashMap::new();
|
||
|
||
if flags.blocks.0.contains(&Block::Size) {
|
||
let size_val = detect_size_lengths(metas, flags);
|
||
|
||
padding_rules.insert(Block::SizeValue, size_val);
|
||
}
|
||
|
||
padding_rules
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::Config;
|
||
use crate::app::Cli;
|
||
use crate::color;
|
||
use crate::color::Colors;
|
||
use crate::flags::{HyperlinkOption, IconOption, IconTheme as FlagTheme, PermissionFlag};
|
||
use crate::icon::Icons;
|
||
use crate::meta::{FileType, Name};
|
||
use crate::{flags, sort};
|
||
use assert_fs::prelude::*;
|
||
use clap::Parser;
|
||
use std::path::Path;
|
||
use tempfile::tempdir;
|
||
|
||
#[test]
|
||
fn test_display_get_visible_width_without_icons() {
|
||
for (s, l) in [
|
||
("Hello,world!", 22),
|
||
("ASCII1234-_", 11),
|
||
("制作样本。", 10),
|
||
("日本語", 6),
|
||
("샘플은 무료로 드리겠습니다", 28),
|
||
("👩🐩", 4),
|
||
("🔬", 2),
|
||
] {
|
||
let path = Path::new(s);
|
||
let name = Name::new(
|
||
path,
|
||
FileType::File {
|
||
exec: false,
|
||
uid: false,
|
||
},
|
||
);
|
||
let output = name
|
||
.render(
|
||
&Colors::new(color::ThemeOption::NoColor),
|
||
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
|
||
&DisplayOption::FileName,
|
||
HyperlinkOption::Never,
|
||
false,
|
||
)
|
||
.to_string();
|
||
|
||
assert_eq!(get_visible_width(&output, false), l);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_display_get_visible_width_with_icons() {
|
||
for (s, l) in [
|
||
// Add 3 characters for the icons.
|
||
("Hello,world!", 24),
|
||
("ASCII1234-_", 13),
|
||
("File with space", 19),
|
||
("制作样本。", 12),
|
||
("日本語", 8),
|
||
("샘플은 무료로 드리겠습니다", 30),
|
||
("👩🐩", 6),
|
||
("🔬", 4),
|
||
] {
|
||
let path = Path::new(s);
|
||
let name = Name::new(
|
||
path,
|
||
FileType::File {
|
||
exec: false,
|
||
uid: false,
|
||
},
|
||
);
|
||
let output = name
|
||
.render(
|
||
&Colors::new(color::ThemeOption::NoColor),
|
||
&Icons::new(false, IconOption::Always, FlagTheme::Fancy, " ".to_string()),
|
||
&DisplayOption::FileName,
|
||
HyperlinkOption::Never,
|
||
false,
|
||
)
|
||
.to_string();
|
||
|
||
assert_eq!(get_visible_width(&output, false), l);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_display_get_visible_width_with_colors() {
|
||
// crossterm implicitly colors if NO_COLOR is set.
|
||
crossterm::style::force_color_output(true);
|
||
|
||
for (s, l) in [
|
||
("Hello,world!", 22),
|
||
("ASCII1234-_", 11),
|
||
("File with space", 17),
|
||
("制作样本。", 10),
|
||
("日本語", 6),
|
||
("샘플은 무료로 드리겠습니다", 28),
|
||
("👩🐩", 4),
|
||
("🔬", 2),
|
||
] {
|
||
let path = Path::new(s);
|
||
let name = Name::new(
|
||
path,
|
||
FileType::File {
|
||
exec: false,
|
||
uid: false,
|
||
},
|
||
);
|
||
let output = name
|
||
.render(
|
||
&Colors::new(color::ThemeOption::NoLscolors),
|
||
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
|
||
&DisplayOption::FileName,
|
||
HyperlinkOption::Never,
|
||
false,
|
||
)
|
||
.to_string();
|
||
|
||
// check if the color is present.
|
||
assert!(
|
||
output.starts_with("\u{1b}[38;5;"),
|
||
"{output:?} should start with color"
|
||
);
|
||
assert!(output.ends_with("[39m"), "reset foreground color");
|
||
|
||
assert_eq!(get_visible_width(&output, false), l, "visible match");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_display_get_visible_width_without_colors() {
|
||
for (s, l) in [
|
||
("Hello,world!", 22),
|
||
("ASCII1234-_", 11),
|
||
("File with space", 17),
|
||
("制作样本。", 10),
|
||
("日本語", 6),
|
||
("샘플은 무료로 드리겠습니다", 28),
|
||
("👩🐩", 4),
|
||
("🔬", 2),
|
||
] {
|
||
let path = Path::new(s);
|
||
let name = Name::new(
|
||
path,
|
||
FileType::File {
|
||
exec: false,
|
||
uid: false,
|
||
},
|
||
);
|
||
let output = name
|
||
.render(
|
||
&Colors::new(color::ThemeOption::NoColor),
|
||
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
|
||
&DisplayOption::FileName,
|
||
HyperlinkOption::Never,
|
||
false,
|
||
)
|
||
.to_string();
|
||
|
||
// check if the color is present.
|
||
assert!(!output.starts_with("\u{1b}[38;5;"));
|
||
assert!(!output.ends_with("[0m"));
|
||
|
||
assert_eq!(get_visible_width(&output, false), l);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_display_get_visible_width_hypelink_simple() {
|
||
for (s, l) in [
|
||
("Hello,world!", 22),
|
||
("ASCII1234-_", 11),
|
||
("File with space", 15),
|
||
("制作样本。", 10),
|
||
("日本語", 6),
|
||
("샘플은 무료로 드리겠습니다", 26),
|
||
("👩🐩", 4),
|
||
("🔬", 2),
|
||
] {
|
||
// rending name require actual file, so we are mocking that
|
||
let output = format!("\x1B]8;;{}\x1B\x5C{}\x1B]8;;\x1B\x5C", "url://fake-url", s);
|
||
assert_eq!(get_visible_width(&output, true), l);
|
||
}
|
||
}
|
||
|
||
fn sort(metas: &mut Vec<Meta>, sorters: &Vec<(flags::SortOrder, sort::SortFn)>) {
|
||
metas.sort_unstable_by(|a, b| sort::by_meta(sorters, a, b));
|
||
|
||
for meta in metas {
|
||
if let Some(ref mut content) = meta.content {
|
||
sort(content, sorters);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_display_tree_with_all() {
|
||
let argv = ["lsd", "--tree", "--all"];
|
||
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("one.d").create_dir_all().unwrap();
|
||
dir.child("one.d/two").touch().unwrap();
|
||
dir.child("one.d/.hidden").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_eq!("one.d\n├── .hidden\n└── two\n", output);
|
||
}
|
||
|
||
/// Different level of folder may form a different width
|
||
/// we must make sure it is aligned in all level
|
||
///
|
||
/// dir has a bytes size
|
||
/// empty file has an empty size
|
||
/// `---blocks size,name` can help us for this case
|
||
#[test]
|
||
fn test_tree_align_subfolder() {
|
||
let argv = ["lsd", "--tree", "--blocks", "size,name"];
|
||
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("dir").create_dir_all().unwrap();
|
||
dir.child("dir/file").touch().unwrap();
|
||
let metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx)
|
||
.unwrap()
|
||
.recurse_into(42, &flags, None)
|
||
.unwrap()
|
||
.0
|
||
.unwrap();
|
||
let output = tree(
|
||
&metas,
|
||
&flags,
|
||
&Colors::new(color::ThemeOption::NoColor),
|
||
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
|
||
&GitTheme::new(),
|
||
);
|
||
|
||
let length_before_b = |i| -> usize {
|
||
output
|
||
.lines()
|
||
.nth(i)
|
||
.unwrap()
|
||
.split(['K', 'B'])
|
||
.next()
|
||
.unwrap()
|
||
.len()
|
||
};
|
||
assert_eq!(length_before_b(0), length_before_b(1));
|
||
assert_eq!(
|
||
output.lines().next().unwrap().find('d'),
|
||
output.lines().nth(1).unwrap().find('└')
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
#[cfg(unix)]
|
||
fn test_tree_size_first_without_name() {
|
||
let argv = ["lsd", "--tree", "--blocks", "size,permission"];
|
||
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("dir").create_dir_all().unwrap();
|
||
dir.child("dir/file").touch().unwrap();
|
||
let metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx)
|
||
.unwrap()
|
||
.recurse_into(42, &flags, None)
|
||
.unwrap()
|
||
.0
|
||
.unwrap();
|
||
let output = tree(
|
||
&metas,
|
||
&flags,
|
||
&Colors::new(color::ThemeOption::NoColor),
|
||
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
|
||
&GitTheme::new(),
|
||
);
|
||
|
||
assert_eq!(output.lines().nth(1).unwrap().chars().next().unwrap(), '└');
|
||
assert_eq!(
|
||
output
|
||
.lines()
|
||
.next()
|
||
.unwrap()
|
||
.chars()
|
||
.position(|x| x == 'd'),
|
||
output
|
||
.lines()
|
||
.nth(1)
|
||
.unwrap()
|
||
.chars()
|
||
.position(|x| x == '.'),
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_tree_edge_before_name() {
|
||
let argv = ["lsd", "--tree", "--long"];
|
||
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("one.d").create_dir_all().unwrap();
|
||
dir.child("one.d/two").touch().unwrap();
|
||
let metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx)
|
||
.unwrap()
|
||
.recurse_into(42, &flags, None)
|
||
.unwrap()
|
||
.0
|
||
.unwrap();
|
||
let output = tree(
|
||
&metas,
|
||
&flags,
|
||
&Colors::new(color::ThemeOption::NoColor),
|
||
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
|
||
&GitTheme::new(),
|
||
);
|
||
|
||
assert!(output.ends_with("└── two\n"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_grid_all_block_headers() {
|
||
let argv = [
|
||
"lsd",
|
||
"--header",
|
||
"--blocks",
|
||
"permission,user,group,size,date,name,inode,links",
|
||
];
|
||
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("testdir").create_dir_all().unwrap();
|
||
dir.child("test").touch().unwrap();
|
||
let metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx)
|
||
.unwrap()
|
||
.recurse_into(1, &flags, None)
|
||
.unwrap()
|
||
.0
|
||
.unwrap();
|
||
let output = grid(
|
||
&metas,
|
||
&flags,
|
||
&Colors::new(color::ThemeOption::NoColor),
|
||
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
|
||
&GitTheme::new(),
|
||
);
|
||
|
||
dir.close().unwrap();
|
||
|
||
assert!(output.contains("Permissions"));
|
||
assert!(output.contains("User"));
|
||
assert!(output.contains("Group"));
|
||
assert!(output.contains("Size"));
|
||
assert!(output.contains("Date Modified"));
|
||
assert!(output.contains("Name"));
|
||
assert!(output.contains("INode"));
|
||
assert!(output.contains("Links"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_grid_no_header_with_empty_meta() {
|
||
let argv = ["lsd", "--header", "-l"];
|
||
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("testdir").create_dir_all().unwrap();
|
||
let metas = Meta::from_path(Path::new(dir.path()), false, PermissionFlag::Rwx)
|
||
.unwrap()
|
||
.recurse_into(1, &flags, None)
|
||
.unwrap()
|
||
.0
|
||
.unwrap();
|
||
let output = grid(
|
||
&metas,
|
||
&flags,
|
||
&Colors::new(color::ThemeOption::NoColor),
|
||
&Icons::new(false, IconOption::Never, FlagTheme::Fancy, " ".to_string()),
|
||
&GitTheme::new(),
|
||
);
|
||
|
||
dir.close().unwrap();
|
||
|
||
assert!(!output.contains("Permissions"));
|
||
assert!(!output.contains("User"));
|
||
assert!(!output.contains("Group"));
|
||
assert!(!output.contains("Size"));
|
||
assert!(!output.contains("Date Modified"));
|
||
assert!(!output.contains("Name"));
|
||
}
|
||
|
||
#[test]
|
||
fn test_folder_path() {
|
||
let tmp_dir = tempdir().expect("failed to create temp dir");
|
||
|
||
let file_path = tmp_dir.path().join("file");
|
||
std::fs::File::create(&file_path).expect("failed to create the file");
|
||
let file = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap();
|
||
|
||
let dir_path = tmp_dir.path().join("dir");
|
||
std::fs::create_dir(&dir_path).expect("failed to create the dir");
|
||
let dir = Meta::from_path(&dir_path, false, PermissionFlag::Rwx).unwrap();
|
||
|
||
assert_eq!(
|
||
display_folder_path(&dir),
|
||
format!(
|
||
"\n{}{}dir:\n",
|
||
tmp_dir.path().to_string_lossy(),
|
||
std::path::MAIN_SEPARATOR
|
||
)
|
||
);
|
||
|
||
const YES: bool = true;
|
||
const NO: bool = false;
|
||
|
||
assert_eq!(
|
||
should_display_folder_path(0, &[file.clone()]),
|
||
YES // doesn't matter since there is no folder
|
||
);
|
||
assert_eq!(should_display_folder_path(0, &[dir.clone()]), NO);
|
||
assert_eq!(
|
||
should_display_folder_path(0, &[file.clone(), dir.clone()]),
|
||
YES
|
||
);
|
||
assert_eq!(
|
||
should_display_folder_path(0, &[dir.clone(), dir.clone()]),
|
||
YES
|
||
);
|
||
assert_eq!(
|
||
should_display_folder_path(0, &[file.clone(), file.clone()]),
|
||
YES // doesn't matter since there is no folder
|
||
);
|
||
|
||
drop(dir); // to avoid clippy complains about previous .clone()
|
||
drop(file);
|
||
}
|
||
|
||
#[cfg(unix)]
|
||
#[test]
|
||
fn test_folder_path_with_links() {
|
||
let tmp_dir = tempdir().expect("failed to create temp dir");
|
||
|
||
let file_path = tmp_dir.path().join("file");
|
||
std::fs::File::create(&file_path).expect("failed to create the file");
|
||
let file = Meta::from_path(&file_path, false, PermissionFlag::Rwx).unwrap();
|
||
|
||
let dir_path = tmp_dir.path().join("dir");
|
||
std::fs::create_dir(&dir_path).expect("failed to create the dir");
|
||
let dir = Meta::from_path(&dir_path, false, PermissionFlag::Rwx).unwrap();
|
||
|
||
let link_path = tmp_dir.path().join("link");
|
||
std::os::unix::fs::symlink("dir", &link_path).unwrap();
|
||
let link = Meta::from_path(&link_path, false, PermissionFlag::Rwx).unwrap();
|
||
|
||
const YES: bool = true;
|
||
const NO: bool = false;
|
||
|
||
assert_eq!(should_display_folder_path(0, &[link.clone()]), NO);
|
||
|
||
assert_eq!(
|
||
should_display_folder_path(0, &[file.clone(), link.clone()]),
|
||
YES
|
||
);
|
||
|
||
assert_eq!(
|
||
should_display_folder_path(0, &[dir.clone(), link.clone()]),
|
||
YES
|
||
);
|
||
|
||
drop(dir); // to avoid clippy complains about previous .clone()
|
||
drop(file);
|
||
drop(link);
|
||
}
|
||
}
|