feat(directory): add ellipsis to truncated paths (#1563)

Adds ellipsis in front of truncated paths: …/
Configurable through new config option: directory.truncation_symbol
Fixes #1162, #1626
This commit is contained in:
Jeremy Hilliker 2020-10-03 09:25:21 -07:00 committed by GitHub
parent 2693d125a8
commit 1673d565f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 231 additions and 39 deletions

View File

@ -651,6 +651,7 @@ it would have been `nixpkgs/pkgs`.
| `disabled` | `false` | Disables the `directory` module. |
| `read_only` | `"🔒"` | The symbol indicating current directory is read only. |
| `read_only_style` | `"red"` | The style for the read only symbol. |
| `truncation_symbol` | `""` | The symbol to prefix to truncated paths. eg: "…/" |
<details>
<summary>This module has a few advanced configuration options that control how the directory is displayed.</summary>
@ -694,6 +695,7 @@ a single character. For `fish_style_pwd_dir_length = 2`, it would be `/bu/th/ci/
[directory]
truncation_length = 8
truncation_symbol = "…/"
```
## Docker Context

View File

@ -15,6 +15,7 @@ pub struct DirectoryConfig<'a> {
pub disabled: bool,
pub read_only: &'a str,
pub read_only_style: &'a str,
pub truncation_symbol: &'a str,
}
impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> {
@ -30,6 +31,7 @@ impl<'a> RootModuleConfig<'a> for DirectoryConfig<'a> {
disabled: false,
read_only: "🔒",
read_only_style: "red",
truncation_symbol: "",
}
}
}

View File

@ -15,6 +15,8 @@ use crate::config::RootModuleConfig;
use crate::configs::directory::DirectoryConfig;
use crate::formatter::StringFormatter;
const HOME_SYMBOL: &str = "~";
/// Creates a module with the current directory
///
/// Will perform path contraction, substitution, and truncation.
@ -31,41 +33,15 @@ use crate::formatter::StringFormatter;
/// **Truncation**
/// Paths will be limited in length to `3` path components by default.
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
const HOME_SYMBOL: &str = "~";
let mut module = context.new_module("directory");
let config: DirectoryConfig = DirectoryConfig::try_load(module.config);
// Using environment PWD is the standard approach for determining logical path
// If this is None for any reason, we fall back to reading the os-provided path
let physical_current_dir = if config.use_logical_path {
match context.get_env("PWD") {
Some(x) => Some(PathBuf::from(x)),
None => {
log::debug!("Error getting PWD environment variable!");
None
}
}
} else {
match std::env::current_dir() {
Ok(x) => Some(x),
Err(e) => {
log::debug!("Error getting physical current directory: {}", e);
None
}
}
};
let current_dir = Path::new(
physical_current_dir
.as_ref()
.unwrap_or_else(|| &context.current_dir),
);
let current_dir = &get_current_dir(&context, &config);
let home_dir = dirs_next::home_dir().unwrap();
log::debug!("Current directory: {:?}", current_dir);
let repo = &context.get_repo().ok()?;
let dir_string = match &repo.root {
Some(repo_root) if config.truncate_to_repo && (repo_root != &home_dir) => {
log::debug!("Repo root: {:?}", repo_root);
@ -83,9 +59,10 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
// Truncate the dir string to the maximum number of path components
let truncated_dir_string = truncate(substituted_dir, config.truncation_length as usize);
let prefix = if is_truncated(&truncated_dir_string) {
// Substitutions could have changed the prefix, so don't allow them and
// fish-style path contraction together
let fish_prefix = if config.fish_style_pwd_dir_length > 0 && config.substitutions.is_empty() {
if config.fish_style_pwd_dir_length > 0 && config.substitutions.is_empty() {
// If user is using fish style path, we need to add the segment first
let contracted_home_dir = contract_path(&current_dir, &home_dir, HOME_SYMBOL);
to_fish_style(
@ -93,10 +70,14 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
contracted_home_dir,
&truncated_dir_string,
)
} else {
String::from(config.truncation_symbol)
}
} else {
String::from("")
};
let final_dir_string = format!("{}{}", fish_prefix, truncated_dir_string);
let displayed_path = prefix + &truncated_dir_string;
let lock_symbol = String::from(config.read_only);
let parsed = StringFormatter::new(config.format).and_then(|formatter| {
@ -107,7 +88,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
_ => None,
})
.map(|variable| match variable {
"path" => Some(Ok(&final_dir_string)),
"path" => Some(Ok(&displayed_path)),
"read_only" => {
if is_readonly_dir(&context.current_dir) {
Some(Ok(&lock_symbol))
@ -131,6 +112,35 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
Some(module)
}
fn is_truncated(path: &str) -> bool {
!(path.starts_with(HOME_SYMBOL)
|| PathBuf::from(path).has_root()
|| (cfg!(target_os = "windows") && PathBuf::from(String::from(path) + r"\").has_root()))
}
fn get_current_dir(context: &Context, config: &DirectoryConfig) -> PathBuf {
// Using environment PWD is the standard approach for determining logical path
// If this is None for any reason, we fall back to reading the os-provided path
let physical_current_dir = if config.use_logical_path {
match context.get_env("PWD") {
Some(x) => Some(PathBuf::from(x)),
None => {
log::debug!("Error getting PWD environment variable!");
None
}
}
} else {
match std::env::current_dir() {
Ok(x) => Some(x),
Err(e) => {
log::debug!("Error getting physical current directory: {}", e);
None
}
}
};
physical_current_dir.unwrap_or_else(|| PathBuf::from(&context.current_dir))
}
fn is_readonly_dir(path: &Path) -> bool {
match directory_utils::is_write_allowed(path) {
Ok(res) => !res,
@ -1166,4 +1176,182 @@ mod tests {
assert_eq!(expected, actual);
tmp_dir.close()
}
#[test]
fn truncation_symbol_truncated_root() -> io::Result<()> {
let actual = ModuleRenderer::new("directory")
.config(toml::toml! {
[directory]
truncation_length = 3
truncation_symbol = "…/"
})
.path(Path::new("/a/four/element/path"))
.collect();
let expected = Some(format!(
"{} ",
Color::Cyan.bold().paint("…/four/element/path")
));
assert_eq!(expected, actual);
Ok(())
}
#[test]
fn truncation_symbol_not_truncated_root() -> io::Result<()> {
let actual = ModuleRenderer::new("directory")
.config(toml::toml! {
[directory]
truncation_length = 4
truncation_symbol = "…/"
})
.path(Path::new("/a/four/element/path"))
.collect();
let expected = Some(format!(
"{} ",
Color::Cyan.bold().paint("/a/four/element/path")
));
assert_eq!(expected, actual);
Ok(())
}
#[test]
fn truncation_symbol_truncated_home() -> io::Result<()> {
let (tmp_dir, name) = make_known_tempdir(home_dir().unwrap().as_path())?;
let dir = tmp_dir.path().join("a/subpath");
fs::create_dir_all(&dir)?;
let actual = ModuleRenderer::new("directory")
.config(toml::toml! {
[directory]
truncation_length = 3
truncation_symbol = "…/"
})
.path(dir)
.collect();
let expected = Some(format!(
"{} ",
Color::Cyan.bold().paint(format!("…/{}/a/subpath", name))
));
assert_eq!(expected, actual);
tmp_dir.close()
}
#[test]
fn truncation_symbol_not_truncated_home() -> io::Result<()> {
let (tmp_dir, name) = make_known_tempdir(home_dir().unwrap().as_path())?;
let dir = tmp_dir.path().join("a/subpath");
fs::create_dir_all(&dir)?;
let actual = ModuleRenderer::new("directory")
.config(toml::toml! {
[directory]
truncate_to_repo = false // Necessary if homedir is a git repo
truncation_length = 4
truncation_symbol = "…/"
})
.path(dir)
.collect();
let expected = Some(format!(
"{} ",
Color::Cyan.bold().paint(format!("~/{}/a/subpath", name))
));
assert_eq!(expected, actual);
tmp_dir.close()
}
#[test]
fn truncation_symbol_truncated_in_repo() -> io::Result<()> {
let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?;
let repo_dir = tmp_dir.path().join("above").join("repo");
let dir = repo_dir.join("src/sub/path");
fs::create_dir_all(&dir)?;
init_repo(&repo_dir).unwrap();
let actual = ModuleRenderer::new("directory")
.config(toml::toml! {
[directory]
truncation_length = 3
truncation_symbol = "…/"
})
.path(dir)
.collect();
let expected = Some(format!("{} ", Color::Cyan.bold().paint("…/src/sub/path")));
assert_eq!(expected, actual);
tmp_dir.close()
}
#[test]
fn truncation_symbol_not_truncated_in_repo() -> io::Result<()> {
let (tmp_dir, _) = make_known_tempdir(Path::new("/tmp"))?;
let repo_dir = tmp_dir.path().join("above").join("repo");
let dir = repo_dir.join("src/sub/path");
fs::create_dir_all(&dir)?;
init_repo(&repo_dir).unwrap();
let actual = ModuleRenderer::new("directory")
.config(toml::toml! {
[directory]
truncation_length = 5
truncation_symbol = "…/"
truncate_to_repo = true
})
.path(dir)
.collect();
let expected = Some(format!(
"{} ",
Color::Cyan.bold().paint("…/repo/src/sub/path")
));
assert_eq!(expected, actual);
tmp_dir.close()
}
#[test]
#[cfg(target_os = "windows")]
fn truncation_symbol_windows_root_not_truncated() -> io::Result<()> {
let dir = Path::new("C:\\temp");
let actual = ModuleRenderer::new("directory")
.config(toml::toml! {
[directory]
truncation_length = 2
truncation_symbol = "…/"
})
.path(dir)
.collect();
let expected = Some(format!("{} ", Color::Cyan.bold().paint("C:/temp")));
assert_eq!(expected, actual);
Ok(())
}
#[test]
#[cfg(target_os = "windows")]
fn truncation_symbol_windows_root_truncated() -> io::Result<()> {
let dir = Path::new("C:\\temp");
let actual = ModuleRenderer::new("directory")
.config(toml::toml! {
[directory]
truncation_length = 1
truncation_symbol = "…/"
})
.path(dir)
.collect();
let expected = Some(format!("{} ", Color::Cyan.bold().paint("…/temp")));
assert_eq!(expected, actual);
Ok(())
}
#[test]
#[cfg(target_os = "windows")]
fn truncation_symbol_windows_root_truncated_backslash() -> io::Result<()> {
let dir = Path::new("C:\\temp");
let actual = ModuleRenderer::new("directory")
.config(toml::toml! {
[directory]
truncation_length = 1
truncation_symbol = r"…\"
})
.path(dir)
.collect();
let expected = Some(format!("{} ", Color::Cyan.bold().paint("\\temp")));
assert_eq!(expected, actual);
Ok(())
}
}