diff --git a/CHANGELOG.md b/CHANGELOG.md index fde0c9dc4..2fab7f3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Changed - Change size to use btyes in classic mode from [meain](https://github.com/meain) +- Show tree edge before name block or first column if no name block from [zwpaper](https://github.com/zwpaper) [#468](https://github.com/Peltoche/lsd/issues/468) ### Fixed ## [0.20.1] - 2021-03-07 diff --git a/src/color.rs b/src/color.rs index 707f3a5c8..5670e334f 100644 --- a/src/color.rs +++ b/src/color.rs @@ -52,6 +52,8 @@ pub enum Elem { Links { valid: bool, }, + + TreeEdge, } impl Elem { @@ -254,6 +256,8 @@ impl Colors { m.insert(Elem::Links { valid: true }, Colour::Fixed(13)); m.insert(Elem::Links { valid: false }, Colour::Fixed(245)); + // TODO add this after we can use file to configure theme + // m.insert(Elem::TreeEdge, Colour::Fixed(44)); // DarkTurquoise m } } diff --git a/src/display.rs b/src/display.rs index 80d4a7f2b..132341bd0 100644 --- a/src/display.rs +++ b/src/display.rs @@ -10,7 +10,7 @@ use terminal_size::terminal_size; use unicode_width::UnicodeWidthStr; const EDGE: &str = "\u{251c}\u{2500}\u{2500}"; // "├──" -const LINE: &str = "\u{2502} "; // "├ " +const LINE: &str = "\u{2502} "; // "│ " const CORNER: &str = "\u{2514}\u{2500}\u{2500}"; // "└──" const BLANK: &str = " "; @@ -32,7 +32,25 @@ pub fn grid(metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons) -> St } pub fn tree(metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons) -> String { - inner_display_tree(metas, &flags, colors, icons, 0, "") + 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 let Block::Name = block { + index = i; + break; + } + } + + for cell in inner_display_tree(metas, &flags, colors, icons, (0, ""), &padding_rules, index) { + grid.add(cell); + } + + grid.fit_into_columns(flags.blocks.0.len()).to_string() } fn inner_display_grid( @@ -67,7 +85,7 @@ fn inner_display_grid( 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::Directory { .. }) || (matches!(meta.file_type, FileType::SymLink { is_dir: true }) && flags.layout != Layout::OneLine)) { @@ -81,6 +99,7 @@ fn inner_display_grid( &flags, &display_option, &padding_rules, + (0, ""), ); for block in blocks { @@ -143,20 +162,25 @@ fn inner_display_tree( flags: &Flags, colors: &Colors, icons: &Icons, - depth: usize, - prefix: &str, -) -> String { - let mut output = String::new(); + tree_depth_prefix: (usize, &str), + padding_rules: &HashMap, + tree_index: usize, +) -> Vec { + let mut cells = Vec::new(); let last_idx = metas.len(); - let padding_rules = get_padding_rules(&metas, flags); - - let mut grid = Grid::new(GridOptions { - filling: Filling::Spaces(1), - direction: Direction::LeftToRight, - }); + 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 meta in metas.iter() { for block in get_output( &meta, &colors, @@ -164,59 +188,41 @@ fn inner_display_tree( &flags, &DisplayOption::FileName, &padding_rules, + (tree_index, ¤t_prefix), ) { let block_str = block.to_string(); - grid.add(Cell { + cells.push(Cell { width: get_visible_width(&block_str), contents: block_str, }); } - } - - let content = grid.fit_into_columns(flags.blocks.0.len()).to_string(); - let mut lines = content.lines(); - - for (idx, meta) in metas.iter().enumerate() { - let is_last_folder_elem = idx + 1 != last_idx; - - if depth > 0 { - output += prefix; - - if is_last_folder_elem { - output += EDGE; - } else { - output += CORNER; - } - output += " "; - } - - output += &String::from(lines.next().unwrap()); - output += "\n"; if meta.content.is_some() { - let mut new_prefix = String::from(prefix); - - if depth > 0 { - if is_last_folder_elem { - new_prefix += LINE; + 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 { - new_prefix += BLANK; + format!("{}{} ", tree_depth_prefix.1, BLANK) } - } + } else { + tree_depth_prefix.1.to_string() + }; - output += &inner_display_tree( + cells.extend(inner_display_tree( &meta.content.as_ref().unwrap(), &flags, colors, icons, - depth + 1, - &new_prefix, - ); + (tree_depth_prefix.0 + 1, &new_prefix), + padding_rules, + tree_index, + )); } } - output + cells } fn should_display_folder_path(depth: usize, metas: &[Meta], flags: &Flags) -> bool { @@ -252,51 +258,51 @@ fn get_output<'a>( flags: &'a Flags, display_option: &DisplayOption, padding_rules: &HashMap, + tree: (usize, &'a str), ) -> Vec> { let mut strings: Vec = Vec::new(); - for block in flags.blocks.0.iter() { + for (i, block) in flags.blocks.0.iter().enumerate() { + let mut block_vec = if Layout::Tree == flags.layout && tree.0 == i { + // TODO: add color after we have theme configuration + // vec![colors.colorize(ANSIString::from(tree.1).to_string(), &Elem::TreeEdge)] + vec![ANSIString::from(tree.1)] + } else { + Vec::new() + }; + match block { - Block::INode => strings.push(meta.inode.render(colors)), - Block::Links => strings.push(meta.links.render(colors)), + Block::INode => block_vec.push(meta.inode.render(colors)), + Block::Links => block_vec.push(meta.links.render(colors)), Block::Permission => { - let s: &[ColoredString] = &[ + block_vec.extend(vec![ meta.file_type.render(colors), meta.permissions.render(colors), - ]; - let res = ANSIStrings(s).to_string(); - strings.push(ColoredString::from(res)); + ]); } - Block::User => strings.push(meta.owner.render_user(colors)), - Block::Group => strings.push(meta.owner.render_group(colors)), - Block::Size => strings.push(meta.size.render( - colors, - &flags, - padding_rules[&Block::SizeValue], - )), - Block::SizeValue => strings.push(meta.size.render_value(colors, flags)), - Block::Date => strings.push(meta.date.render(colors, &flags)), + Block::User => block_vec.push(meta.owner.render_user(colors)), + Block::Group => block_vec.push(meta.owner.render_group(colors)), + Block::Size => { + let pad = if Layout::Tree == flags.layout && 0 == tree.0 && 0 == i { + None + } else { + Some(padding_rules[&Block::SizeValue]) + }; + block_vec.push(meta.size.render(colors, &flags, pad)) + } + Block::SizeValue => block_vec.push(meta.size.render_value(colors, flags)), + Block::Date => block_vec.push(meta.date.render(colors, &flags)), Block::Name => { - let s: String = - if flags.no_symlink.0 || flags.dereference.0 || flags.layout == Layout::Grid { - ANSIStrings(&[ - meta.name.render(colors, icons, &display_option), - meta.indicator.render(&flags), - ]) - .to_string() - } else { - ANSIStrings(&[ - meta.name.render(colors, icons, &display_option), - meta.indicator.render(&flags), - meta.symlink.render(colors, &flags), - ]) - .to_string() - }; - - strings.push(ColoredString::from(s)); + block_vec.extend(vec![ + meta.name.render(colors, icons, &display_option), + meta.indicator.render(&flags), + ]); + if !(flags.no_symlink.0 || flags.dereference.0 || flags.layout == Layout::Grid) { + block_vec.push(meta.symlink.render(colors, &flags)) + } } }; + strings.push(ColoredString::from(ANSIStrings(&block_vec).to_string())); } - strings } @@ -325,6 +331,15 @@ fn detect_size_lengths(metas: &[Meta], flags: &Flags) -> usize { 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 @@ -347,9 +362,11 @@ mod tests { use super::*; use crate::color; use crate::color::Colors; - use crate::icon; use crate::icon::Icons; use crate::meta::{FileType, Name}; + use crate::Config; + use crate::{app, flags, icon, sort}; + use assert_fs::prelude::*; use std::path::Path; #[test] @@ -485,4 +502,147 @@ mod tests { assert_eq!(get_visible_width(&output), *l); } } + + fn sort(metas: &mut Vec, 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 = vec!["lsd", "--tree", "--all"]; + let matches = app::build().get_matches_from_safe(argv).unwrap(); + let flags = Flags::configure_from(&matches, &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) + .unwrap() + .recurse_into(42, &flags) + .unwrap() + .unwrap(); + sort(&mut metas, &sort::assemble_sorters(&flags)); + let output = tree( + &metas, + &flags, + &Colors::new(color::Theme::NoColor), + &Icons::new(icon::Theme::NoIcon, " ".to_string()), + ); + + 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 = vec!["lsd", "--tree", "--blocks", "size,name"]; + let matches = app::build().get_matches_from_safe(argv).unwrap(); + let flags = Flags::configure_from(&matches, &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) + .unwrap() + .recurse_into(42, &flags) + .unwrap() + .unwrap(); + let output = tree( + &metas, + &flags, + &Colors::new(color::Theme::NoColor), + &Icons::new(icon::Theme::NoIcon, " ".to_string()), + ); + + let length_before_b = |i| -> usize { + output + .lines() + .nth(i) + .unwrap() + .split(|c| c == 'K' || c == 'B') + .nth(0) + .unwrap() + .len() + }; + assert_eq!(length_before_b(0), length_before_b(1)); + assert_eq!( + output.lines().nth(0).unwrap().find("d"), + output.lines().nth(1).unwrap().find("└") + ); + } + + #[test] + #[cfg(unix)] + fn test_tree_size_first_without_name() { + let argv = vec!["lsd", "--tree", "--blocks", "size,permission"]; + let matches = app::build().get_matches_from_safe(argv).unwrap(); + let flags = Flags::configure_from(&matches, &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) + .unwrap() + .recurse_into(42, &flags) + .unwrap() + .unwrap(); + let output = tree( + &metas, + &flags, + &Colors::new(color::Theme::NoColor), + &Icons::new(icon::Theme::NoIcon, " ".to_string()), + ); + + assert_eq!(output.lines().nth(1).unwrap().chars().nth(0).unwrap(), '└'); + assert_eq!( + output + .lines() + .nth(0) + .unwrap() + .chars() + .position(|x| x == 'd'), + output + .lines() + .nth(1) + .unwrap() + .chars() + .position(|x| x == '.'), + ); + } + + #[test] + fn test_tree_edge_before_name() { + let argv = vec!["lsd", "--tree", "--long"]; + let matches = app::build().get_matches_from_safe(argv).unwrap(); + let flags = Flags::configure_from(&matches, &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) + .unwrap() + .recurse_into(42, &flags) + .unwrap() + .unwrap(); + let output = tree( + &metas, + &flags, + &Colors::new(color::Theme::NoColor), + &Icons::new(icon::Theme::NoIcon, " ".to_string()), + ); + + assert!(output.ends_with("└── two\n")); + } } diff --git a/src/meta/date.rs b/src/meta/date.rs index 375e036e3..4bedb8746 100644 --- a/src/meta/date.rs +++ b/src/meta/date.rs @@ -21,16 +21,15 @@ impl Date { pub fn render(&self, colors: &Colors, flags: &Flags) -> ColoredString { let now = Local::now(); - let elem; - if self.0 > now - Duration::hours(1) { - elem = &Elem::HourOld; + let elem = if self.0 > now - Duration::hours(1) { + Elem::HourOld } else if self.0 > now - Duration::days(1) { - elem = &Elem::DayOld; + Elem::DayOld } else { - elem = &Elem::Older; - } + Elem::Older + }; - colors.colorize(self.date_string(&flags), elem) + colors.colorize(self.date_string(&flags), &elem) } pub fn date_string(&self, flags: &Flags) -> String { diff --git a/src/meta/filetype.rs b/src/meta/filetype.rs index e7c275f8c..0787d14dd 100644 --- a/src/meta/filetype.rs +++ b/src/meta/filetype.rs @@ -81,7 +81,10 @@ impl FileType { } pub fn is_dirlike(self) -> bool { - matches!(self, FileType::Directory { .. } | FileType::SymLink { is_dir: true }) + matches!( + self, + FileType::Directory { .. } | FileType::SymLink { is_dir: true } + ) } } diff --git a/src/meta/size.rs b/src/meta/size.rs index f9090b808..13b5fbfd5 100644 --- a/src/meta/size.rs +++ b/src/meta/size.rs @@ -2,6 +2,7 @@ use crate::color::{ColoredString, Colors, Elem}; use crate::flags::{Flags, SizeFlag}; use ansi_term::ANSIStrings; use std::fs::Metadata; +use std::iter::repeat; #[derive(Clone, Debug, PartialEq, Eq)] pub enum Unit { @@ -52,14 +53,22 @@ impl Size { } } - pub fn render(&self, colors: &Colors, flags: &Flags, val_alignment: usize) -> ColoredString { + pub fn render( + &self, + colors: &Colors, + flags: &Flags, + val_alignment: Option, + ) -> ColoredString { let val_content = self.render_value(colors, flags); let unit_content = self.render_unit(colors, flags); - let mut left_pad = String::with_capacity(val_alignment - val_content.len()); - for _ in 0..left_pad.capacity() { - left_pad.push(' '); - } + let left_pad = if let Some(align) = val_alignment { + repeat(" ") + .take(align - val_content.len()) + .collect::() + } else { + "".to_string() + }; let mut strings: Vec = vec![ColoredString::from(left_pad), val_content]; if flags.size != SizeFlag::Short { @@ -318,7 +327,7 @@ mod test { flags.size = SizeFlag::Short; let colors = Colors::new(Theme::NoColor); - assert_eq!(size.render(&colors, &flags, 2).to_string(), "42K"); - assert_eq!(size.render(&colors, &flags, 3).to_string(), " 42K"); + assert_eq!(size.render(&colors, &flags, Some(2)).to_string(), "42K"); + assert_eq!(size.render(&colors, &flags, Some(3)).to_string(), " 42K"); } } diff --git a/tests/integration.rs b/tests/integration.rs index b65ff0353..0bf3b4496 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -426,7 +426,7 @@ fn test_tree() { .arg(tmp.path()) .arg("--tree") .assert() - .stdout(predicate::str::is_match("├── one\n└── one.d\n └── two\n$").unwrap()); + .stdout(predicate::str::is_match("├── one\n└── one.d\n └── two\n$").unwrap()); } #[test] @@ -443,10 +443,25 @@ fn test_tree_all_not_show_self() { .arg("--all") .assert() .stdout( - predicate::str::is_match("├── one\n└── one.d\n ├── .hidden\n └── two\n$").unwrap(), + predicate::str::is_match("├── one\n└── one.d\n ├── .hidden\n └── two\n$") + .unwrap(), ); } +#[test] +fn test_tree_show_edge_before_name() { + let tmp = tempdir(); + tmp.child("one.d").create_dir_all().unwrap(); + tmp.child("one.d/two").touch().unwrap(); + + cmd() + .arg(tmp.path()) + .arg("--tree") + .arg("--long") + .assert() + .stdout(predicate::str::is_match("└── two\n$").unwrap()); +} + #[test] fn test_tree_d() { let tmp = tempdir(); @@ -462,7 +477,7 @@ fn test_tree_d() { .arg("--tree") .arg("-d") .assert() - .stdout(predicate::str::is_match("├── one.d\n│ └── one.d\n└── two.d\n$").unwrap()); + .stdout(predicate::str::is_match("├── one.d\n│ └── one.d\n└── two.d\n$").unwrap()); } fn cmd() -> Command {