git_prole/
path_display.rs

1use std::fmt::Debug;
2use std::fmt::Display;
3use std::path::Path;
4use std::path::MAIN_SEPARATOR_STR;
5
6use camino::Utf8Path;
7use owo_colors::OwoColorize;
8use owo_colors::Stream;
9use path_absolutize::Absolutize;
10
11use crate::current_dir::current_dir_utf8;
12
13/// A way to display a path "nicely".
14pub trait PathDisplay: Debug + AsRef<Path> {
15    fn display_path_cwd(&self) -> String {
16        current_dir_utf8()
17            .map(|cwd| self.display_path_from(cwd))
18            .unwrap_or_else(|error| {
19                tracing::debug!(
20                    %error,
21                    path=?self,
22                    "Failed to get current working directory for displaying path"
23                );
24                display_backup(self)
25            })
26    }
27
28    fn display_path_from(&self, base: impl AsRef<Utf8Path>) -> String;
29}
30
31impl<P> PathDisplay for P
32where
33    P: AsRef<Path> + Debug,
34{
35    fn display_path_from(&self, base: impl AsRef<Utf8Path>) -> String {
36        let path = self.as_ref();
37        Utf8Path::from_path(path)
38            .and_then(|utf8path| try_display(utf8path, base))
39            .unwrap_or_else(|| display_backup(self))
40    }
41}
42
43fn display_backup(path: impl AsRef<Path>) -> String {
44    make_colorful(path.as_ref().display())
45}
46
47fn make_colorful(path: impl Display) -> String {
48    path.if_supports_color(Stream::Stdout, |text| text.cyan())
49        .to_string()
50}
51
52fn try_display(path: impl AsRef<Utf8Path> + Debug, base: impl AsRef<Utf8Path>) -> Option<String> {
53    try_display_inner(path.as_ref(), base).map(make_colorful)
54}
55
56fn try_display_inner(
57    path: impl AsRef<Utf8Path> + Debug,
58    base: impl AsRef<Utf8Path>,
59) -> Option<String> {
60    let base = base.as_ref();
61    let normal: &Path = path.as_ref().as_ref();
62    let normal = normal.absolutize_from(base).ok()?;
63    let normal = Utf8Path::from_path(&normal)?;
64
65    if let Some(home) = dirs::home_dir() {
66        if let Ok(from_home) = normal.strip_prefix(&home) {
67            return Some(format!("~{MAIN_SEPARATOR_STR}{from_home}"));
68        }
69    }
70
71    let temp_dir = std::env::temp_dir();
72    if let Ok(from_temp) = normal.strip_prefix(&temp_dir) {
73        return Some(format!("$TMPDIR{MAIN_SEPARATOR_STR}{from_temp}"));
74    }
75
76    // Evil: On macOS, `/tmp` and `$TMPDIR` start with symlinks to `/private`, so you need to
77    // follow symlinks to check if a path actually starts with the tempdir.
78    if let Ok(canon_temp_dir) = temp_dir.canonicalize() {
79        if let Ok(from_temp) = normal.strip_prefix(&canon_temp_dir) {
80            return Some(format!("$TMPDIR{MAIN_SEPARATOR_STR}{from_temp}"));
81        }
82    }
83
84    // TODO: Is it worth trying relative paths in some cases?
85    Some(normal.to_string())
86}