From 1f02a02b9478dfa769cd9be747c1130883938e3e Mon Sep 17 00:00:00 2001 From: Benjamin Nguyen Date: Thu, 2 Mar 2023 21:52:11 -0800 Subject: [PATCH 1/3] fix root path display --- src/cli.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 38263e9d..7f0099af 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -8,6 +8,7 @@ use std::{ error::Error as StdError, fmt::{self, Display, Formatter}, fs, + io, path::{Path, PathBuf}, usize, }; @@ -135,9 +136,9 @@ impl TryFrom<&Clargs> for WalkParallel { type Error = Error; fn try_from(clargs: &Clargs) -> Result { - let root = clargs.dir(); + let root = fs::canonicalize(clargs.dir())?; - fs::metadata(root) + fs::metadata(&root) .map_err(|e| Error::DirNotFound(format!("{}: {e}", root.display())))?; Ok(WalkBuilder::new(root) @@ -155,6 +156,7 @@ impl TryFrom<&Clargs> for WalkParallel { pub enum Error { InvalidGlobPatterns(ignore::Error), DirNotFound(String), + PathCanonicalizationError(io::Error), } impl Display for Error { @@ -162,6 +164,7 @@ impl Display for Error { match self { Error::InvalidGlobPatterns(e) => write!(f, "Invalid glob patterns: {e}"), Error::DirNotFound(e) => write!(f, "{e}"), + Error::PathCanonicalizationError(e) => write!(f, "{e}"), } } } @@ -173,3 +176,9 @@ impl From for Error { Self::InvalidGlobPatterns(value) } } + +impl From for Error { + fn from(value: io::Error) -> Self { + Self::PathCanonicalizationError(value) + } +} From 1ceb413b2ef9894d90f22d8516c2e2332a9584eb Mon Sep 17 00:00:00 2001 From: Benjamin Nguyen Date: Fri, 3 Mar 2023 21:24:15 -0800 Subject: [PATCH 2/3] prefer OsStr --- src/fs/erdtree/node.rs | 18 ++++----- src/icons.rs | 84 +++++++++++++++++++++--------------------- 2 files changed, 49 insertions(+), 53 deletions(-) diff --git a/src/fs/erdtree/node.rs b/src/fs/erdtree/node.rs index 6df1ad7e..1d6e8d88 100644 --- a/src/fs/erdtree/node.rs +++ b/src/fs/erdtree/node.rs @@ -9,6 +9,7 @@ use lscolors::Style as LS_Style; use std::{ convert::From, fmt::{self, Display, Formatter}, + ffi::OsStr, fs::{self, FileType}, path::{Path, PathBuf}, slice::Iter, @@ -27,7 +28,6 @@ pub struct Node { pub symlink: bool, children: Option>, - file_name: String, file_type: Option, path: PathBuf, show_icon: bool, @@ -41,7 +41,6 @@ impl Node { file_size: Option, symlink: bool, children: Option>, - file_name: String, file_type: Option, path: PathBuf, show_icon: bool, @@ -51,7 +50,6 @@ impl Node { children, depth, symlink, - file_name, file_size, file_type, path, @@ -71,8 +69,8 @@ impl Node { } /// Returns a reference to `file_name`. - pub fn file_name(&self) -> &str { - &self.file_name + pub fn file_name(&self) -> &OsStr { + self.path().file_name().unwrap() } /// Returns `true` if node is a directory. @@ -173,8 +171,6 @@ impl From for Node { let depth = dir_entry.depth(); - let file_name = dir_entry.file_name().to_string_lossy().into_owned(); - let file_type = dir_entry.file_type(); let metadata = dir_entry.metadata().ok(); @@ -207,7 +203,6 @@ impl From for Node { file_size, symlink, children, - file_name, file_type, path, show_icon, @@ -218,8 +213,6 @@ impl From for Node { impl Display for Node { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let file_name = self.file_name(); - let size = self.file_size .map(|size| format!("({})", FileSize::new(size))) .or_else(|| Some("".to_owned())) @@ -230,7 +223,10 @@ impl Display for Node { .flatten() .unwrap_or("".to_owned()); - let styled_name = self.stylize(file_name); + let styled_name = self.file_name() + .to_str() + .map(|name| self.stylize(name)) + .unwrap(); let output = format!( "{:> = Lazy::new(|| { /// will take on the color properties of their associated file which is based on `LS_COLORS`. /// /// Dev icons sourced from [`exa`](https://github.com/ogham/exa/blob/master/src/output/icons.rs) -static FILE_NAME_ICON_MAP: Lazy> = Lazy::new(|| { +static FILE_NAME_ICON_MAP: Lazy> = Lazy::new(|| { hash!( - ".Trash" => "\u{f1f8}", //  - ".atom" => "\u{e764}", //  - ".bashprofile" => "\u{e615}", //  - ".bashrc" => "\u{f489}", //  - ".git" => "\u{f1d3}", //  - ".gitattributes" => "\u{f1d3}", //  - ".gitconfig" => "\u{f1d3}", //  - ".github" => "\u{f408}", //  - ".gitignore" => "\u{f1d3}", //  - ".gitmodules" => "\u{f1d3}", //  - ".rvm" => "\u{e21e}", //  - ".vimrc" => "\u{e62b}", //  - ".vscode" => "\u{e70c}", //  - ".zshrc" => "\u{f489}", //  - "Cargo.lock" => "\u{e7a8}", //  - "bin" => "\u{e5fc}", //  - "config" => "\u{e5fc}", //  - "docker-compose.yml" => "\u{f308}", //  - "Dockerfile" => "\u{f308}", //  - ".DS_Store" => "\u{f179}", //  - "gitignore_global" => "\u{f1d3}", //  - "go.mod" => "\u{e626}", //  - "go.sum" => "\u{e626}", //  - "gradle" => "\u{e256}", //  - "gruntfile.coffee" => "\u{e611}", //  - "gruntfile.js" => "\u{e611}", //  - "gruntfile.ls" => "\u{e611}", //  - "gulpfile.coffee" => "\u{e610}", //  - "gulpfile.js" => "\u{e610}", //  - "gulpfile.ls" => "\u{e610}", //  - "hidden" => "\u{f023}", //  - "include" => "\u{e5fc}", //  - "lib" => "\u{f121}", //  - "localized" => "\u{f179}", //  - "Makefile" => "\u{f489}", //  - "node_modules" => "\u{e718}", //  - "npmignore" => "\u{e71e}", //  - "PKGBUILD" => "\u{f303}", //  - "rubydoc" => "\u{e73b}", //  - "yarn.lock" => "\u{e718}" //  + OsString::from(".Trash") => "\u{f1f8}", //  + OsString::from(".atom") => "\u{e764}", //  + OsString::from(".bashprofile") => "\u{e615}", //  + OsString::from(".bashrc") => "\u{f489}", //  + OsString::from(".git") => "\u{f1d3}", //  + OsString::from(".gitattributes") => "\u{f1d3}", //  + OsString::from(".gitconfig") => "\u{f1d3}", //  + OsString::from(".github") => "\u{f408}", //  + OsString::from(".gitignore") => "\u{f1d3}", //  + OsString::from(".gitmodules") => "\u{f1d3}", //  + OsString::from(".rvm") => "\u{e21e}", //  + OsString::from(".vimrc") => "\u{e62b}", //  + OsString::from(".vscode") => "\u{e70c}", //  + OsString::from(".zshrc") => "\u{f489}", //  + OsString::from("Cargo.lock") => "\u{e7a8}", //  + OsString::from("bin") => "\u{e5fc}", //  + OsString::from("config") => "\u{e5fc}", //  + OsString::from("docker-compose.yml") => "\u{f308}", //  + OsString::from("Dockerfile") => "\u{f308}", //  + OsString::from(".DS_Store") => "\u{f179}", //  + OsString::from("gitignore_global") => "\u{f1d3}", //  + OsString::from("go.mod") => "\u{e626}", //  + OsString::from("go.sum") => "\u{e626}", //  + OsString::from("gradle") => "\u{e256}", //  + OsString::from("gruntfile.coffee") => "\u{e611}", //  + OsString::from("gruntfile.js") => "\u{e611}", //  + OsString::from("gruntfile.ls") => "\u{e611}", //  + OsString::from("gulpfile.coffee") => "\u{e610}", //  + OsString::from("gulpfile.js") => "\u{e610}", //  + OsString::from("gulpfile.ls") => "\u{e610}", //  + OsString::from("hidden") => "\u{f023}", //  + OsString::from("include") => "\u{e5fc}", //  + OsString::from("lib") => "\u{f121}", //  + OsString::from("localized") => "\u{f179}", //  + OsString::from("Makefile") => "\u{f489}", //  + OsString::from("node_modules") => "\u{e718}", //  + OsString::from("npmignore") => "\u{e71e}", //  + OsString::from("PKGBUILD") => "\u{f303}", //  + OsString::from("rubydoc") => "\u{e73b}", //  + OsString::from("yarn.lock") => "\u{e718}" //  ) }); @@ -296,7 +296,7 @@ pub fn icon_from_file_type(ft: &FileType) -> Option<&str> { None } -pub fn icon_from_file_name(name: &str) -> Option<&str> { +pub fn icon_from_file_name(name: &OsStr) -> Option<&str> { FILE_NAME_ICON_MAP.get(name).map(|i| *i) } From 3ce3a892b886433df1eee5f7c987bd51ceb4e95e Mon Sep 17 00:00:00 2001 From: Benjamin Nguyen Date: Sat, 4 Mar 2023 10:41:46 -0800 Subject: [PATCH 3/3] improved UI for symlinks --- src/fs/erdtree/node.rs | 103 +++++++++++++++++++++++++------------ src/fs/erdtree/tree/mod.rs | 2 +- src/icons.rs | 5 ++ 3 files changed, 76 insertions(+), 34 deletions(-) diff --git a/src/fs/erdtree/node.rs b/src/fs/erdtree/node.rs index 1d6e8d88..1c957ad4 100644 --- a/src/fs/erdtree/node.rs +++ b/src/fs/erdtree/node.rs @@ -1,4 +1,5 @@ use super::get_ls_colors; +use ansi_term::Color; use crate::{ fs::file_size::FileSize, icons::{self, icon_from_ext, icon_from_file_name, icon_from_file_type} @@ -9,7 +10,7 @@ use lscolors::Style as LS_Style; use std::{ convert::From, fmt::{self, Display, Formatter}, - ffi::OsStr, + ffi::{OsStr, OsString}, fs::{self, FileType}, path::{Path, PathBuf}, slice::Iter, @@ -25,13 +26,13 @@ use std::{ pub struct Node { pub depth: usize, pub file_size: Option, - pub symlink: bool, - children: Option>, + file_name: OsString, file_type: Option, path: PathBuf, show_icon: bool, style: Style, + symlink_target: Option, } impl Node { @@ -39,22 +40,24 @@ impl Node { pub fn new( depth: usize, file_size: Option, - symlink: bool, children: Option>, + file_name: OsString, file_type: Option, path: PathBuf, show_icon: bool, style: Style, + symlink_target: Option, ) -> Self { Self { children, depth, - symlink, + file_name, file_size, file_type, path, show_icon, style, + symlink_target, } } @@ -68,9 +71,10 @@ impl Node { self.children.as_ref().map(|children| children.iter()) } - /// Returns a reference to `file_name`. + /// Returns a reference to `file_name`. If file is a symlink then `file_name` is the name of + /// the symlink not the target. pub fn file_name(&self) -> &OsStr { - self.path().file_name().unwrap() + &self.file_name } /// Returns `true` if node is a directory. @@ -80,6 +84,21 @@ impl Node { .unwrap_or(false) } + /// Is the Node a symlink. + pub fn is_symlink(&self) -> bool { + self.symlink_target.is_some() + } + + /// Path to symlink target. + pub fn symlink_target_path(&self) -> Option<&Path> { + self.symlink_target.as_ref().map(PathBuf::as_path) + } + + /// Returns the file name of the symlink target if [Node] represents a symlink. + pub fn symlink_target_file_name(&self) -> Option<&OsStr> { + self.symlink_target_path().map(|path| path.file_name()).flatten() + } + /// Returns reference to underlying [FileType]. pub fn file_type(&self) -> Option<&FileType> { self.file_type.as_ref() @@ -124,18 +143,20 @@ impl Node { fn get_icon(&self) -> Option { if !self.show_icon { return None } - let s = |i| Some(self.stylize(i)); + let path = self.symlink_target_path().unwrap_or_else(|| self.path()); - if let Some(icon) = self.path().extension().map(icon_from_ext).flatten() { - return s(icon) + if let Some(icon) = path.extension().map(icon_from_ext).flatten() { + return self.stylize(icon) } if let Some(icon) = self.file_type().map(icon_from_file_type).flatten() { - return s(icon); + return self.stylize(icon); } - if let Some(icon) = icon_from_file_name(self.file_name()) { - return s(icon); + let file_name = self.symlink_target_file_name().unwrap_or_else(|| self.file_name()); + + if let Some(icon) = icon_from_file_name(file_name) { + return self.stylize(icon); } Some(icons::get_default_icon().to_owned()) @@ -144,11 +165,20 @@ impl Node { /// Stylizes input, `entity` based on [`LS_COLORS`] /// /// [`LS_COLORS`]: super::tree::ui::LS_COLORS - fn stylize(&self, entity: &str) -> String { - self.style().foreground.map_or_else( - || entity.to_string(), - |fg| fg.bold().paint(entity).to_string() - ) + fn stylize(&self, entity: &str) -> Option { + self.style() + .foreground + .map(|fg| fg.bold().paint(entity).to_string()) + .or_else(|| Some(entity.to_string())) + } + + fn stylize_link_name(&self) -> Option { + self.symlink_target_file_name() + .map(|name| { + let file_name = self.file_name().to_str().map(|s| self.stylize(s)).flatten().unwrap(); + let target_name = Color::Red.paint(format!("\u{2192} {}", name.to_str().unwrap())); + format!("{} {}", file_name, target_name) + }) } } @@ -175,38 +205,42 @@ impl From for Node { let metadata = dir_entry.metadata().ok(); - let path = dir_entry.into_path(); + let path = dir_entry.path(); + + let symlink_target = dir_entry + .path_is_symlink() + .then(|| fs::read_link(path)) + .transpose() + .ok() + .flatten(); + + let file_name = path.file_name().map(|os_str| os_str.to_owned()).unwrap(); let style = get_ls_colors() - .style_for_path_with_metadata(&path, metadata.as_ref()) + .style_for_path_with_metadata(path, metadata.as_ref()) .map(LS_Style::to_ansi_term_style) .unwrap_or_default(); let mut file_size = None; - let mut symlink = false; if let Some(ref ft) = file_type { if ft.is_file() { if let Some(md) = metadata { file_size = Some(md.len()); - symlink = md.is_symlink(); } - } else if ft.is_dir() { - symlink = fs::symlink_metadata(&path) - .map(|md| md.is_symlink()) - .unwrap_or(false); } }; Self::new( depth, file_size, - symlink, children, + file_name, file_type, - path, + path.into(), show_icon, - style + style, + symlink_target, ) } } @@ -223,10 +257,13 @@ impl Display for Node { .flatten() .unwrap_or("".to_owned()); - let styled_name = self.file_name() - .to_str() - .map(|name| self.stylize(name)) - .unwrap(); + let styled_name = self.stylize_link_name().unwrap_or_else(|| { + self.file_name() + .to_str() + .map(|name| self.stylize(name)) + .flatten() + .unwrap() + }); let output = format!( "{:> = Lazy::new(|| { OsString::from("hidden") => "\u{f023}", //  OsString::from("include") => "\u{e5fc}", //  OsString::from("lib") => "\u{f121}", //  + OsString::from("license") => "\u{e60a}", //  + OsString::from("LICENSE") => "\u{e60a}", //  + OsString::from("licence") => "\u{e60a}", //  + OsString::from("LICENCE") => "\u{e60a}", //  OsString::from("localized") => "\u{f179}", //  OsString::from("Makefile") => "\u{f489}", //  OsString::from("node_modules") => "\u{e718}", //  @@ -169,6 +173,7 @@ static EXT_ICON_MAP: Lazy> = Lazy::new(|| { OsString::from("less") => col(60, "\u{e614}"), //  OsString::from("lhs") => col(140, "\u{e61f}"), //  OsString::from("license") => col(185, "\u{e60a}"), //  + OsString::from("licence") => col(185, "\u{e60a}"), //  OsString::from("lock") => col(250, "\u{f13e}"), //  OsString::from("log") => col(255, "\u{f831}"), //  OsString::from("lua") => col(74, "\u{e620}"), // 