Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Symlink UI #26

Merged
merged 3 commits into from
Mar 4, 2023
Merged
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
13 changes: 11 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use std::{
error::Error as StdError,
fmt::{self, Display, Formatter},
fs,
io,
path::{Path, PathBuf},
usize,
};
Expand Down Expand Up @@ -135,9 +136,9 @@ impl TryFrom<&Clargs> for WalkParallel {
type Error = Error;

fn try_from(clargs: &Clargs) -> Result<Self, Self::Error> {
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)
Expand All @@ -155,13 +156,15 @@ impl TryFrom<&Clargs> for WalkParallel {
pub enum Error {
InvalidGlobPatterns(ignore::Error),
DirNotFound(String),
PathCanonicalizationError(io::Error),
}

impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidGlobPatterns(e) => write!(f, "Invalid glob patterns: {e}"),
Error::DirNotFound(e) => write!(f, "{e}"),
Error::PathCanonicalizationError(e) => write!(f, "{e}"),
}
}
}
Expand All @@ -173,3 +176,9 @@ impl From<ignore::Error> for Error {
Self::InvalidGlobPatterns(value)
}
}

impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Self::PathCanonicalizationError(value)
}
}
103 changes: 68 additions & 35 deletions src/fs/erdtree/node.rs
Original file line number Diff line number Diff line change
@@ -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}
Expand All @@ -9,6 +10,7 @@ use lscolors::Style as LS_Style;
use std::{
convert::From,
fmt::{self, Display, Formatter},
ffi::{OsStr, OsString},
fs::{self, FileType},
path::{Path, PathBuf},
slice::Iter,
Expand All @@ -24,39 +26,38 @@ use std::{
pub struct Node {
pub depth: usize,
pub file_size: Option<u64>,
pub symlink: bool,

children: Option<Vec<Node>>,
file_name: String,
file_name: OsString,
file_type: Option<FileType>,
path: PathBuf,
show_icon: bool,
style: Style,
symlink_target: Option<PathBuf>,
}

impl Node {
/// Initializes a new [Node].
pub fn new(
depth: usize,
file_size: Option<u64>,
symlink: bool,
children: Option<Vec<Node>>,
file_name: String,
file_name: OsString,
file_type: Option<FileType>,
path: PathBuf,
show_icon: bool,
style: Style,
symlink_target: Option<PathBuf>,
) -> Self {
Self {
children,
depth,
symlink,
file_name,
file_size,
file_type,
path,
show_icon,
style,
symlink_target,
}
}

Expand All @@ -70,8 +71,9 @@ impl Node {
self.children.as_ref().map(|children| children.iter())
}

/// Returns a reference to `file_name`.
pub fn file_name(&self) -> &str {
/// 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.file_name
}

Expand All @@ -82,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()
Expand Down Expand Up @@ -126,18 +143,20 @@ impl Node {
fn get_icon(&self) -> Option<String> {
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())
Expand All @@ -146,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<String> {
self.style()
.foreground
.map(|fg| fg.bold().paint(entity).to_string())
.or_else(|| Some(entity.to_string()))
}

fn stylize_link_name(&self) -> Option<String> {
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)
})
}
}

Expand All @@ -173,53 +201,52 @@ impl From<NodePrecursor> 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();

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

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()))
Expand All @@ -230,7 +257,13 @@ impl Display for Node {
.flatten()
.unwrap_or("".to_owned());

let styled_name = self.stylize(file_name);
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!(
"{:<icon_padding$}{:<name_padding$}{size}",
Expand Down
2 changes: 1 addition & 1 deletion src/fs/erdtree/tree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ impl Display for Tree {
let mut new_base = base_prefix.to_owned();

let new_theme =
child.symlink.then(|| ui::get_link_theme()).unwrap_or(theme);
child.is_symlink().then(|| ui::get_link_theme()).unwrap_or(theme);

if last_entry {
new_base.push_str(ui::SEP);
Expand Down
89 changes: 47 additions & 42 deletions src/icons.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,48 +23,52 @@ static FILE_TYPE_ICON_MAP: Lazy<HashMap<&str, &str>> = 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<HashMap<&str, &str>> = Lazy::new(|| {
static FILE_NAME_ICON_MAP: Lazy<HashMap<OsString, &str>> = 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("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}", // 
OsString::from("npmignore") => "\u{e71e}", // 
OsString::from("PKGBUILD") => "\u{f303}", // 
OsString::from("rubydoc") => "\u{e73b}", // 
OsString::from("yarn.lock") => "\u{e718}" // 
)
});

Expand Down Expand Up @@ -169,6 +173,7 @@ static EXT_ICON_MAP: Lazy<HashMap<OsString, String>> = 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}"), // 
Expand Down Expand Up @@ -296,7 +301,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)
}

Expand Down