use std::borrow::Cow;
use std::fmt;
use std::path::{MAIN_SEPARATOR, MAIN_SEPARATOR_STR, Path};
#[derive(Clone, Debug)]
pub struct RichDisplay<'a> {
path: &'a Path,
trailing_slash: bool,
relative_to: Option<Cow<'a, Path>>,
tilde: Option<Cow<'a, Path>>,
}
impl<'a> RichDisplay<'a> {
fn new(path: &'a Path) -> Self {
Self {
path,
trailing_slash: false,
relative_to: None,
tilde: None,
}
}
#[must_use]
pub fn trailing_slash(self, enabled: bool) -> Self {
Self {
trailing_slash: enabled,
..self
}
}
#[must_use]
pub fn relative_to<P: AsRef<Path> + ?Sized>(self, base: &'a P) -> Self {
Self {
relative_to: Some(Cow::Borrowed(base.as_ref())),
..self
}
}
#[must_use]
pub fn relative_to_cwd(self) -> Self {
match std::env::current_dir() {
Ok(cwd) => Self {
relative_to: Some(Cow::Owned(cwd)),
..self
},
Err(_) => self,
}
}
#[must_use]
pub fn tilde<P: AsRef<Path> + ?Sized>(self, home: &'a P) -> Self {
Self {
tilde: Some(Cow::Borrowed(home.as_ref())),
..self
}
}
#[must_use]
pub fn tilde_me(self) -> Self {
match directories::BaseDirs::new() {
Some(dirs) => Self {
tilde: Some(Cow::Owned(dirs.home_dir().to_owned())),
..self
},
None => self,
}
}
}
impl fmt::Display for RichDisplay<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let maybe_write_terminal_slash = |f: &mut fmt::Formatter<'_>, p: &Path| {
if self.trailing_slash
&& p.as_os_str().as_encoded_bytes().last() != Some(&(MAIN_SEPARATOR as u8))
{
write!(f, "{}", MAIN_SEPARATOR_STR)?;
}
Ok(())
};
if let Some(base) = self.relative_to.as_deref()
&& let Ok(stripped) = self.path.strip_prefix(base)
{
write!(f, "{}", stripped.display())?;
maybe_write_terminal_slash(f, stripped)?;
} else if let Some(home) = self.tilde.as_deref()
&& let Ok(stripped) = self.path.strip_prefix(home)
{
write!(f, "~{}{}", MAIN_SEPARATOR_STR, stripped.display())?;
if !stripped.as_os_str().is_empty() {
maybe_write_terminal_slash(f, stripped)?;
}
} else {
write!(f, "{}", self.path.display())?;
maybe_write_terminal_slash(f, self.path)?;
}
Ok(())
}
}
pub trait DisplayRichExt {
fn display_rich(&self) -> RichDisplay<'_>;
}
impl<T: AsRef<Path> + ?Sized> DisplayRichExt for T {
fn display_rich(&self) -> RichDisplay<'_> {
RichDisplay::new(self.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::DisplayRichExt;
use rstest::rstest;
use std::path::{MAIN_SEPARATOR_STR, Path, PathBuf};
#[rstest]
#[case(Path::new("relative").join("some").join("dir"))]
#[case(Path::new("etc").join("hosts"))]
fn base_output_matches_path_display(#[case] path: PathBuf) {
assert_eq!(path.display_rich().to_string(), path.display().to_string());
}
#[rstest]
#[case::appends_when_missing("foo".to_string(), true, format!("foo{MAIN_SEPARATOR_STR}"))]
#[case::idempotent_when_terminated(
format!("foo{MAIN_SEPARATOR_STR}"),
true,
format!("foo{MAIN_SEPARATOR_STR}")
)]
#[case::disabled_is_base("foo".to_string(), false, "foo".to_string())]
#[case::empty_becomes_bare_separator(String::new(), true, MAIN_SEPARATOR_STR.to_string())]
fn trailing_slash_rendering(
#[case] input: String,
#[case] enabled: bool,
#[case] expected: String,
) {
assert_eq!(
input.display_rich().trailing_slash(enabled).to_string(),
expected
);
}
#[rstest]
fn works_for_all_asref_path_types() {
let expected = format!("bar{MAIN_SEPARATOR_STR}");
assert_eq!(
"bar".display_rich().trailing_slash(true).to_string(),
expected
);
assert_eq!(
String::from("bar")
.display_rich()
.trailing_slash(true)
.to_string(),
expected
);
assert_eq!(
Path::new("bar")
.display_rich()
.trailing_slash(true)
.to_string(),
expected
);
assert_eq!(
PathBuf::from("bar")
.display_rich()
.trailing_slash(true)
.to_string(),
expected
);
}
#[rstest]
#[case::strips_prefix(
Path::new("home").join("user").join("project").join("src"),
Path::new("home").join("user"),
Path::new("project").join("src").display().to_string()
)]
#[case::base_itself_is_empty(
Path::new("home").join("user"),
Path::new("home").join("user"),
String::new()
)]
#[case::passes_through_when_not_under_base(
Path::new("etc").join("hosts"),
Path::new("home").join("user"),
Path::new("etc").join("hosts").display().to_string()
)]
fn relative_to_rendering(
#[case] path: PathBuf,
#[case] base: PathBuf,
#[case] expected: String,
) {
assert_eq!(path.display_rich().relative_to(&base).to_string(), expected);
}
#[rstest]
#[case::abbreviates_home(
Path::new("home").join("user").join("project"),
Path::new("home").join("user"),
format!("~{MAIN_SEPARATOR_STR}project")
)]
#[case::home_root_is_tilde_separator(
Path::new("home").join("user"),
Path::new("home").join("user"),
format!("~{MAIN_SEPARATOR_STR}")
)]
#[case::passes_through_when_not_under_home(
Path::new("etc").join("hosts"),
Path::new("home").join("user"),
Path::new("etc").join("hosts").display().to_string()
)]
fn tilde_rendering(#[case] path: PathBuf, #[case] home: PathBuf, #[case] expected: String) {
assert_eq!(path.display_rich().tilde(&home).to_string(), expected);
}
#[rstest]
fn relative_to_takes_priority_over_tilde() {
let dir = Path::new("home").join("user");
let p = dir.join("proj");
assert_eq!(
p.display_rich().relative_to(&dir).tilde(&dir).to_string(),
"proj"
);
}
#[rstest]
#[case::tilde(
Path::new("home").join("user").join("proj"),
None,
Some(Path::new("home").join("user")),
format!("~{MAIN_SEPARATOR_STR}proj{MAIN_SEPARATOR_STR}")
)]
#[case::tilde_home_root_is_idempotent(
Path::new("home").join("user"),
None,
Some(Path::new("home").join("user")),
format!("~{MAIN_SEPARATOR_STR}")
)]
#[case::relative_to_base_itself(
Path::new("home").join("user"),
Some(Path::new("home").join("user")),
None,
MAIN_SEPARATOR_STR.to_string()
)]
fn composes_with_trailing_slash(
#[case] path: PathBuf,
#[case] relative_to: Option<PathBuf>,
#[case] tilde: Option<PathBuf>,
#[case] expected: String,
) {
let mut d = path.display_rich();
if let Some(base) = &relative_to {
d = d.relative_to(base);
}
if let Some(home) = &tilde {
d = d.tilde(home);
}
assert_eq!(d.trailing_slash(true).to_string(), expected);
}
}