diff --git a/src/core.rs b/src/core.rs index 4d4a52b..b5ab63b 100644 --- a/src/core.rs +++ b/src/core.rs @@ -95,7 +95,12 @@ impl Core { meta_list.push(meta); } _ => { - match meta.recurse_into(base_path, depth, self.flags.display, &self.flags.ignore_globs) { + match meta.recurse_into( + base_path, + depth, + self.flags.display, + &self.flags.ignore_globs, + ) { Ok(content) => { meta.content = content; meta_list.push(meta); diff --git a/src/display.rs b/src/display.rs index 8ab3398..27d30a3 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,11 +1,10 @@ use crate::color::{ColoredString, Colors}; use crate::flags::{Block, Display, Flags, Layout}; use crate::icon::Icons; -use crate::meta::{FileType, Meta}; use crate::meta::name::DisplayOption; +use crate::meta::{FileType, Meta}; use ansi_term::{ANSIString, ANSIStrings}; use std::collections::HashMap; -use std::path::Path; use term_grid::{Cell, Direction, Filling, Grid, GridOptions}; use terminal_size::terminal_size; use unicode_width::UnicodeWidthStr; @@ -23,7 +22,17 @@ pub fn grid(metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons) -> St let current_dir = std::env::current_dir().unwrap(); - inner_display_grid(¤t_dir, metas, &flags, colors, icons, 0, term_width) + inner_display_grid( + &DisplayOption::Relative { + base_path: ¤t_dir, + }, + metas, + &flags, + colors, + icons, + 0, + term_width, + ) } pub fn tree(metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons) -> String { @@ -31,7 +40,7 @@ pub fn tree(metas: &[Meta], flags: &Flags, colors: &Colors, icons: &Icons) -> St } fn inner_display_grid( - base_path: &Path, + display_option: &DisplayOption, metas: &[Meta], flags: &Flags, colors: &Colors, @@ -59,15 +68,20 @@ fn inner_display_grid( let skip_dirs = (depth == 0) && (flags.display != Display::DisplayDirectoryItself); // print the files first. - for (index, meta) in metas.iter().enumerate() { + for meta in metas.iter() { // Maybe skip showing the directory meta now; show its contents later. if let (true, FileType::Directory { .. }) = (skip_dirs, meta.file_type) { continue; } - let display_option = DisplayOption::Relative{ base_path }; - - let blocks = get_output(&meta, &colors, &icons, &flags, &display_option, &padding_rules); + let blocks = get_output( + &meta, + &colors, + &icons, + &flags, + &display_option, + &padding_rules, + ); for block in blocks { let block_str = block.to_string(); @@ -105,8 +119,16 @@ fn inner_display_grid( output += &display_folder_path(&meta); } + let display_option = if should_display_folder_path { + DisplayOption::FileName + } else { + DisplayOption::Relative { + base_path: &meta.path, + } + }; + output += &inner_display_grid( - &meta.path, + &display_option, meta.content.as_ref().unwrap(), &flags, colors, @@ -139,7 +161,14 @@ fn inner_display_tree( }); for meta in metas.iter() { - for block in get_output(&meta, &colors, &icons, &flags, &DisplayOption::FileName, &padding_rules) { + for block in get_output( + &meta, + &colors, + &icons, + &flags, + &DisplayOption::FileName, + &padding_rules, + ) { let block_str = block.to_string(); grid.add(Cell { @@ -346,7 +375,7 @@ mod tests { let output = name.render( &Colors::new(color::Theme::NoColor), &Icons::new(icon::Theme::NoIcon), - &DisplayOption::FileName + &DisplayOption::FileName, ); assert_eq!(get_visible_width(&output), *l); @@ -378,7 +407,7 @@ mod tests { .render( &Colors::new(color::Theme::NoColor), &Icons::new(icon::Theme::Fancy), - &DisplayOption::FileName + &DisplayOption::FileName, ) .to_string(); @@ -410,7 +439,7 @@ mod tests { .render( &Colors::new(color::Theme::NoLscolors), &Icons::new(icon::Theme::NoIcon), - &DisplayOption::FileName + &DisplayOption::FileName, ) .to_string(); @@ -446,7 +475,7 @@ mod tests { .render( &Colors::new(color::Theme::NoColor), &Icons::new(icon::Theme::NoIcon), - &DisplayOption::FileName + &DisplayOption::FileName, ) .to_string(); diff --git a/src/icon.rs b/src/icon.rs index b237dcb..88d6c7c 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -96,8 +96,10 @@ impl Icons { res += icon; res += ICON_SPACE; res - } else if let Some(icon) = name.extension() - .and_then(|extension| self.icons_by_extension.get(extension)){ + } else if let Some(icon) = name + .extension() + .and_then(|extension| self.icons_by_extension.get(extension)) + { // Use the known extensions. res += icon; res += ICON_SPACE; diff --git a/src/meta/name.rs b/src/meta/name.rs index 71ad509..080c9c6 100644 --- a/src/meta/name.rs +++ b/src/meta/name.rs @@ -2,12 +2,13 @@ use crate::color::{ColoredString, Colors, Elem}; use crate::icon::Icons; use crate::meta::filetype::FileType; use std::cmp::{Ordering, PartialOrd}; -use std::path::{Path, PathBuf, Component}; use std::ffi::OsStr; +use std::path::{Component, Path, PathBuf}; +#[derive(Debug)] pub enum DisplayOption<'a> { FileName, - Relative{base_path: &'a Path}, + Relative { base_path: &'a Path }, } #[derive(Clone, Debug, Eq)] @@ -23,18 +24,43 @@ impl Name { self.path.file_name().and_then(OsStr::to_str).unwrap() } - fn relative_path(&self, base_path: &Path) -> std::borrow::Cow<'_, str> { - if self.path.starts_with(Component::ParentDir) { - self.path.to_string_lossy() - } else if self.path.starts_with(base_path) { - let relative_path = self.path.strip_prefix(base_path).unwrap().to_string_lossy(); - if relative_path == "" { std::borrow::Cow::Borrowed(".") - } else { - relative_path - } + fn relative_path + Clone>(&self, base_path: T) -> PathBuf { + use std::borrow::Cow; + let target_path = if self.path.is_absolute() { + Cow::Borrowed(&self.path) } else { - std::borrow::Cow::Borrowed(&self.display_name) + Cow::Owned(self.path.canonicalize().unwrap()) + }; + + let base_path = if base_path.as_ref().is_absolute() { + Cow::Borrowed(base_path.as_ref()) + } else { + Cow::Owned(base_path.as_ref().canonicalize().unwrap()) + }; + + if target_path == base_path { + return PathBuf::from(""); } + + let shared_components: PathBuf = target_path + .components() + .zip(base_path.components()) + .take_while(|(target_component, base_component)| target_component == base_component) + .map(|tuple| tuple.0) + .collect(); + + base_path + .strip_prefix(&shared_components) + .unwrap() + .components() + .map(|_| Component::ParentDir) + .chain( + target_path + .strip_prefix(&shared_components) + .unwrap() + .components(), + ) + .collect() } pub fn new(path: &Path, file_type: FileType) -> Self { @@ -50,15 +76,28 @@ impl Name { Self { path: PathBuf::from(path), extension, - display_name: path.file_name().and_then(OsStr::to_str).unwrap_or("?").to_owned(), + display_name: path + .file_name() + .and_then(OsStr::to_str) + .unwrap_or("?") + .to_owned(), file_type, } } - pub fn render(&self, colors: &Colors, icons: &Icons, display_option: &DisplayOption) -> ColoredString { + pub fn render( + &self, + colors: &Colors, + icons: &Icons, + display_option: &DisplayOption, + ) -> ColoredString { let content = match display_option { DisplayOption::FileName => format!("{}{}", icons.get(self), self.file_name()), - DisplayOption::Relative{base_path} => format!("{}{}", icons.get(self), self.relative_path(base_path)), + DisplayOption::Relative { base_path } => format!( + "{}{}", + icons.get(self), + self.relative_path(base_path).to_string_lossy() + ), }; let elem = match self.file_type { @@ -86,24 +125,31 @@ impl Name { impl Ord for Name { fn cmp(&self, other: &Self) -> Ordering { - self.display_name.cmp(&other.display_name) + self.display_name + .to_lowercase() + .cmp(&other.display_name.to_lowercase()) } } impl PartialOrd for Name { fn partial_cmp(&self, other: &Self) -> Option { - self.display_name.partial_cmp(&other.display_name) + self.display_name + .to_lowercase() + .partial_cmp(&other.display_name.to_lowercase()) } } impl PartialEq for Name { fn eq(&self, other: &Self) -> bool { - self.path.eq(&other.path) + self.display_name + .to_lowercase() + .eq(&other.display_name.to_lowercase()) } } #[cfg(test)] mod test { + use super::DisplayOption; use super::Name; use crate::color::{self, Colors}; use crate::icon::{self, Icons}; @@ -116,7 +162,7 @@ mod test { use std::fs::{self, File}; #[cfg(unix)] use std::os::unix::fs::symlink; - use std::path::Path; + use std::path::{Path, PathBuf}; #[cfg(unix)] use std::process::Command; use tempfile::tempdir; @@ -138,7 +184,7 @@ mod test { assert_eq!( Colour::Fixed(184).paint(" file.txt"), - name.render(&colors, &icons) + name.render(&colors, &icons, &DisplayOption::FileName) ); } @@ -156,7 +202,7 @@ mod test { assert_eq!( Colour::Fixed(33).paint(" directory"), - meta.name.render(&colors, &icons) + meta.name.render(&colors, &icons, &DisplayOption::FileName) ); } @@ -179,11 +225,11 @@ mod test { let colors = Colors::new(color::Theme::NoLscolors); let file_type = FileType::new(&meta, &Permissions::from(&meta)); - let name = Name::new(&tmp_dir.into_path(), &symlink_path, file_type); + let name = Name::new(&symlink_path, file_type); assert_eq!( Colour::Fixed(44).paint(" target.tmp"), - name.render(&colors, &icons) + name.render(&colors, &icons, &DisplayOption::FileName) ); } @@ -205,11 +251,11 @@ mod test { let colors = Colors::new(color::Theme::NoLscolors); let file_type = FileType::new(&meta, &Permissions::from(&meta)); - let name = Name::new(&tmp_dir.into_path(), &pipe_path, file_type); + let name = Name::new(&pipe_path, file_type); assert_eq!( Colour::Fixed(184).paint(" pipe.tmp"), - name.render(&colors, &icons) + name.render(&colors, &icons, &DisplayOption::FileName) ); } @@ -227,7 +273,10 @@ mod test { assert_eq!( "file.txt", - meta.name.render(&colors, &icons).to_string().as_str() + meta.name + .render(&colors, &icons, &DisplayOption::FileName) + .to_string() + .as_str() ); } @@ -263,7 +312,7 @@ mod test { #[test] fn test_order_impl_is_case_insensitive() { - let path_1 = Path::new("AAAA"); + let path_1 = Path::new("/AAAA"); let name_1 = Name::new( &path_1, FileType::File { @@ -272,7 +321,7 @@ mod test { }, ); - let path_2 = Path::new("aaaa"); + let path_2 = Path::new("/aaaa"); let name_2 = Name::new( &path_2, FileType::File { @@ -286,7 +335,7 @@ mod test { #[test] fn test_partial_order_impl() { - let path_a = Path::new("aaaa"); + let path_a = Path::new("/aaaa"); let name_a = Name::new( &path_a, FileType::File { @@ -295,7 +344,7 @@ mod test { }, ); - let path_z = Path::new("zzzz"); + let path_z = Path::new("/zzzz"); let name_z = Name::new( &path_z, FileType::File { @@ -375,4 +424,52 @@ mod test { assert_eq!(true, name_1 == name_2); } + + #[test] + fn test_parent_relative_path() { + let name = Name::new( + Path::new("/home/parent1/child"), + FileType::File { + uid: false, + exec: false, + }, + ); + let base_path = Path::new("/home/parent2"); + + assert_eq!( + PathBuf::from("../parent1/child"), + name.relative_path(base_path), + ) + } + + #[test] + fn test_current_relative_path() { + let name = Name::new( + Path::new("/home/parent1/child"), + FileType::File { + uid: false, + exec: false, + }, + ); + let base_path = PathBuf::from("/home/parent1"); + + assert_eq!(PathBuf::from("child"), name.relative_path(base_path),) + } + + #[test] + fn test_grand_parent_relative_path() { + let name = Name::new( + Path::new("/home/grand-parent1/parent1/child"), + FileType::File { + uid: false, + exec: false, + }, + ); + let base_path = PathBuf::from("/home/grand-parent2/parent1"); + + assert_eq!( + PathBuf::from("../../grand-parent1/parent1/child"), + name.relative_path(base_path), + ) + } }